한 화면의 콘텐츠를 탭 단위로 구분하고, 탭 선택이나 좌우 스와이프로 전환하는 컴포넌트입니다.
import "./styles";
import { root, useState } from "@lynx-js/react";
import { Tabs, useSeedClassName } from "@seed-design/lynx-react";
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [value, setValue] = useState("one");
function handleValueChange(nextValue: string) {
"background only";
setValue(nextValue);
}
return (
<page className={seedClassName}>
<view className="tabs-preview">
<text className="tabs-preview__status">선택된 탭: {value}</text>
<Tabs.Root value={value} onValueChange={handleValueChange} triggerLayout="fill">
<Tabs.List>
<Tabs.Trigger value="one">첫 번째</Tabs.Trigger>
<Tabs.Trigger value="two">두 번째</Tabs.Trigger>
<Tabs.Trigger value="three">세 번째</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content className="tabs-preview__content" value="one">
<text className="tabs-preview__content-text">첫 번째 콘텐츠</text>
</Tabs.Content>
<Tabs.Content className="tabs-preview__content" value="two">
<text className="tabs-preview__content-text">두 번째 콘텐츠</text>
</Tabs.Content>
<Tabs.Content className="tabs-preview__content" value="three">
<text className="tabs-preview__content-text">세 번째 콘텐츠</text>
</Tabs.Content>
</Tabs.Root>
</view>
</page>
);
}
root.render(<Root />);
npx @seed-design/cli add ui:tabs
pnpm dlx @seed-design/cli add ui:tabs
yarn dlx @seed-design/cli add ui:tabs
bun x @seed-design/cli add ui:tabs
의존성 설치
npm install @seed-design/lynx-react
yarn add @seed-design/lynx-react
pnpm add @seed-design/lynx-react
bun add @seed-design/lynx-react
아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:tabs
* @requires @seed-design/lynx-react@>=0.1.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.1.0 <1.0.0
**/
import {
Tabs as SeedTabs,
type TabsCarouselProps as SeedTabsCarouselProps,
type TabsContentProps as SeedTabsContentProps,
type TabsListProps as SeedTabsListProps,
type TabsRootProps as SeedTabsRootProps,
type TabsTriggerProps as SeedTabsTriggerProps,
} from "@seed-design/lynx-react";
import { forwardRef } from "@lynx-js/react";
export interface TabsRootProps extends SeedTabsRootProps {}
/**
* @see https://seed-design.io/lynx/components/tabs
*/
export const TabsRoot = forwardRef<unknown, TabsRootProps>((props, ref) => {
return <SeedTabs.Root ref={ref} {...props} />;
});
TabsRoot.displayName = "TabsRoot";
export interface TabsListProps extends SeedTabsListProps {}
export const TabsList = forwardRef<unknown, TabsListProps>((props, ref) => {
const { children, ...otherProps } = props;
return (
<SeedTabs.List ref={ref} {...otherProps}>
{children}
<SeedTabs.Indicator />
</SeedTabs.List>
);
});
TabsList.displayName = "TabsList";
export interface TabsTriggerProps extends SeedTabsTriggerProps {}
export const TabsTrigger = SeedTabs.Trigger;
export interface TabsCarouselProps extends SeedTabsCarouselProps {}
export const TabsCarousel = forwardRef<unknown, TabsCarouselProps>((props, ref) => {
const { children, ...otherProps } = props;
return (
<SeedTabs.Carousel ref={ref} {...otherProps}>
<SeedTabs.CarouselCamera>{children}</SeedTabs.CarouselCamera>
</SeedTabs.Carousel>
);
});
TabsCarousel.displayName = "TabsCarousel";
export interface TabsContentProps extends SeedTabsContentProps {}
export const TabsContent = SeedTabs.Content;
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
value?string | undefined
defaultValue?string | undefined
onValueChange?((value: string) => void) | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
valuestring
disabled?boolean | undefined
accessibility-label?string | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
swipeable?boolean | undefined
onSettle?(() => void) | undefined
onSwipeStart?(() => void) | undefined
onSwipeEnd?(() => void) | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
valuestring
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
import {
TabsCarousel,
TabsContent,
TabsList,
TabsRoot,
TabsTrigger,
} from "@/components/ui/tabs";
export function App() {
return (
<TabsRoot defaultValue="one" triggerLayout="fill">
<TabsList>
<TabsTrigger value="one">첫 번째</TabsTrigger>
<TabsTrigger value="two">두 번째</TabsTrigger>
</TabsList>
<TabsCarousel swipeable className="tabs-carousel">
<TabsContent value="one">
<text>첫 번째 콘텐츠</text>
</TabsContent>
<TabsContent value="two">
<text>두 번째 콘텐츠</text>
</TabsContent>
</TabsCarousel>
</TabsRoot>
);
}
Registry의 TabsList는 Tabs.Indicator를 자동으로 추가합니다. TabsCarousel도 내부에서 Tabs.CarouselCamera를 조립하므로 각 TabsContent만 자식으로 전달하면 됩니다.
TabsCarousel은 Lynx 네이티브 <viewpager>를 사용합니다. Carousel에 앱 레이아웃에 맞는 높이를 지정해야 하며, swipeable을 설정하면 좌우 스와이프로 탭을 바꿀 수 있습니다.
iOS에서는 화면 왼쪽 32px 안에서 시작하는 뒤로가기 제스처를 우선합니다. 앱의 제스처 영역과 맞춰야 한다면 iosBackGestureEdgeWidth로 너비를 조정하세요.
Trigger를 탭하거나 콘텐츠를 좌우로 밀면 선택 값과 Indicator가 함께 이동합니다.
import "./styles";
import { root, useState } from "@lynx-js/react";
import { Tabs, useSeedClassName } from "@seed-design/lynx-react";
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [value, setValue] = useState("one");
function handleValueChange(nextValue: string) {
"background only";
setValue(nextValue);
}
return (
<page className={seedClassName}>
<view className="tabs-preview">
<text className="tabs-preview__status">선택된 탭: {value}</text>
<Tabs.Root value={value} onValueChange={handleValueChange}>
<Tabs.List>
<Tabs.Trigger value="one">첫 번째</Tabs.Trigger>
<Tabs.Trigger value="two">두 번째</Tabs.Trigger>
<Tabs.Trigger value="three">세 번째</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Carousel swipeable className="tabs-preview__carousel">
<Tabs.CarouselCamera>
<Tabs.Content className="tabs-preview__content" value="one">
<text className="tabs-preview__content-text">왼쪽이나 오른쪽으로 밀어보세요.</text>
</Tabs.Content>
<Tabs.Content className="tabs-preview__content" value="two">
<text className="tabs-preview__content-text">두 번째 콘텐츠</text>
</Tabs.Content>
<Tabs.Content className="tabs-preview__content" value="three">
<text className="tabs-preview__content-text">세 번째 콘텐츠</text>
</Tabs.Content>
</Tabs.CarouselCamera>
</Tabs.Carousel>
</Tabs.Root>
</view>
</page>
);
}
root.render(<Root />);
disabled Trigger는 탭할 수 없습니다. Carousel이 비활성 탭에 도달하면 다음 활성 탭으로 이동합니다.
import "./styles";
import { root } from "@lynx-js/react";
import { Tabs, useSeedClassName } from "@seed-design/lynx-react";
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
<page className={seedClassName}>
<view className="tabs-preview">
<Tabs.Root defaultValue="one" triggerLayout="hug">
<Tabs.List>
<Tabs.Trigger value="one">첫 번째</Tabs.Trigger>
<Tabs.Trigger value="two" disabled>
비활성 탭
</Tabs.Trigger>
<Tabs.Trigger value="three">세 번째</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content className="tabs-preview__content" value="one">
<text className="tabs-preview__content-text">첫 번째 콘텐츠</text>
</Tabs.Content>
<Tabs.Content className="tabs-preview__content" value="two">
<text className="tabs-preview__content-text">비활성 콘텐츠</text>
</Tabs.Content>
<Tabs.Content className="tabs-preview__content" value="three">
<text className="tabs-preview__content-text">세 번째 콘텐츠</text>
</Tabs.Content>
</Tabs.Root>
</view>
</page>
);
}
root.render(<Root />);
triggerLayout="fill"은 Trigger를 같은 너비로 배치합니다. triggerLayout="hug"은 레이블 너비에 맞춰 배치하고, 화면을 넘으면 List를 가로로 스크롤합니다.
import "./styles";
import { root } from "@lynx-js/react";
import { Tabs, useSeedClassName } from "@seed-design/lynx-react";
function TabsExample({ triggerLayout }: { triggerLayout: "fill" | "hug" }) {
return (
<view className="tabs-preview__layout-section">
<text className="tabs-preview__status">triggerLayout="{triggerLayout}"</text>
<Tabs.Root defaultValue="one" triggerLayout={triggerLayout}>
<Tabs.List>
<Tabs.Trigger value="one">첫 번째</Tabs.Trigger>
<Tabs.Trigger value="two">길이가 긴 두 번째 탭</Tabs.Trigger>
<Tabs.Trigger value="three">세 번째</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
</Tabs.Root>
</view>
);
}
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
<page className={seedClassName}>
<view className="tabs-preview tabs-preview--layout">
<TabsExample triggerLayout="fill" />
<TabsExample triggerLayout="hug" />
</view>
</page>
);
}
root.render(<Root />);
size는 small과 medium을 지원합니다. 긴 레이블과 큰 글자 크기에서도 한 줄을 유지하며 hug List의 가로 스크롤로 처리합니다.
| 항목 | React Web | Lynx |
|---|
| 렌더링 요소 | HTML div, button | 네이티브 view, text, scroll-view, viewpager |
| 탭 선택 이벤트 | onClick 기반 | bindtap 기반, 공개 상태 이벤트는 onValueChange |
| Carousel | Embla Carousel | 네이티브 <viewpager> |
| 가로 List | CSS overflow | 네이티브 <scroll-view> |
| 제어 방식 | value, defaultValue | 동일 |
| 접근성 | ARIA tablist, tab, tabpanel | Lynx accessibility-* 속성 |
| Indicator | DOM 측정과 CSS 변수 | bindlayoutchange 측정과 viewpager offset 보간 |
현재 Lynx 구현은 다음 Web 기능을 지원하지 않습니다.
- 세로
orientation
- 키보드 방향키, Home/End, roving focus
- RTL 순서와 스와이프 방향 반전
asChild
lazyMount, unmountOnExit
- Carousel의
loop, autoHeight, dragThreshold
carouselPreventDrag
Lynx의 native viewpager는 page slot을 유지해야 하므로 Carousel 콘텐츠를 lazy unmount하지 않습니다. 높이가 다른 콘텐츠는 autoHeight 대신 Carousel 컨테이너 높이를 앱에서 정하세요.