Tabs
한 화면의 콘텐츠를 탭 단위로 구분하고, 탭 선택이나 좌우 스와이프로 전환하는 컴포넌트입니다.
<viewpager><viewpager-item>import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="1" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
);
}Installation
npx @seed-design/cli add ui:tabspnpm dlx @seed-design/cli add ui:tabsyarn dlx @seed-design/cli add ui:tabsbun x @seed-design/cli add ui:tabs의존성 설치
npm install @seed-design/lynx-reactyarn add @seed-design/lynx-reactpnpm add @seed-design/lynx-reactbun add @seed-design/lynx-react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:tabs
* @requires @seed-design/lynx-react@^1.0.0
* @requires @seed-design/lynx-css@^1.0.0
**/
import {
NotificationBadge,
NotificationBadgePositioner,
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 Omit<SeedTabsTriggerProps, "notification"> {
notification?: boolean;
}
export const TabsTrigger = forwardRef<unknown, TabsTriggerProps>((props, ref) => {
const { children, notification, ...otherProps } = props;
return (
<SeedTabs.Trigger
ref={ref}
{...otherProps}
notification={
notification ? (
<NotificationBadgePositioner size="small" attach="text">
<NotificationBadge accessibility-elements-hidden={true} />
</NotificationBadgePositioner>
) : undefined
}
>
{children}
</SeedTabs.Trigger>
);
});
TabsTrigger.displayName = "TabsTrigger";
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.
*/
Props
TabsRoot
Prop
Type
value?string | undefineddefaultValue?string | undefinedonValueChange?((value: string) => void) | undefinedTabsList
Prop
Type
TabsTrigger
Prop
Type
notification?boolean | undefineddisabled?boolean | undefinedchildrenstring | numbervaluestringTabsCarousel
Prop
Type
swipeable?boolean | undefinedonSettle?(() => void) | undefinedTabsContent
Prop
Type
valuestringUsage
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>
);
}TabsCarousel에는 TabsContent를 자식으로 전달합니다. 앱 레이아웃에 맞는 높이를 지정하고, swipeable을 설정하면 좌우 스와이프로 탭을 바꿀 수 있습니다.
iOS의 화면 왼쪽 가장자리에서 시작하는 뒤로 가기 제스처 범위는 iosBackGestureEdgeWidth로 조절할 수 있습니다.
TabsList의 scrollAlign은 선택한 탭을 가로 목록에 맞추는 방식입니다. "nearest", "start", "center", "end"를 지원하며 기본값은 "start"입니다.
Examples
Layout Fill (Default)
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="2" triggerLayout="fill" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
);
}Layout Hug
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="1" triggerLayout="hug" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
);
}Size Medium
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="1" size="medium" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
);
}Size Small (Default)
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="1" size="small" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
);
}Transition
Lynx의 TabsCarousel은 탭을 선택할 때 native viewpager의 selectTab을 smooth: false로 호출하므로 콘텐츠가 즉시 바뀝니다. 전환 효과가 필요하면 TabsCarousel 대신 선택 값을 기준으로 콘텐츠를 직접 렌더링하고 앱에서 애니메이션을 적용하세요.
Swipeable
Trigger를 탭하거나 콘텐츠를 좌우로 밀면 선택 값과 Indicator가 함께 이동합니다.
import "./styles";
import { TabsCarousel, TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="2" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsCarousel swipeable className="tabs-preview__carousel">
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsCarousel>
</TabsRoot>
);
}Carousel Prevent Drag
Lynx는 carouselPreventDrag를 지원하지 않습니다. Carousel 안에서 드래그와 충돌하는 조작 요소는 Carousel 밖에 배치하세요.
Disabled
disabled Trigger는 탭할 수 없습니다. Carousel이 비활성 탭에 도달하면 다음 활성 탭으로 이동합니다.
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="1" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2" disabled>
라벨2
</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
);
}Notification
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<TabsRoot defaultValue="1" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2" notification>
라벨2
</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
);
}Sticky List
탭이 전체 화면을 차지하고 TabsList가 화면 위에 고정되어야 할 때 사용합니다.
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<scroll-view className="tabs-preview__scroll" scroll-orientation="vertical">
<TabsRoot defaultValue="1" size="medium" stickyList style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent className="tabs-preview__sticky-content" value="1">
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent className="tabs-preview__sticky-content" value="2">
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent className="tabs-preview__sticky-content" value="3">
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
</scroll-view>
);
}Standalone
TabsContent를 사용하지 않고 콘텐츠 영역을 직접 구성할 때 사용합니다.
이 구성에서는 Tabs가 제공하는 스와이프 기능을 사용할 수 없습니다.
import "./styles";
import { useState } from "@lynx-js/react";
import { TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
const [activeTab, setActiveTab] = useState("1");
function handleValueChange(value: string) {
"background only";
setActiveTab(value);
}
return (
<TabsRoot defaultValue="1" onValueChange={handleValueChange} style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
{activeTab === "1" && (
<view className="tabs-preview__content">
<text className="tabs-preview__content-text">Content 1</text>
</view>
)}
{activeTab === "2" && (
<view className="tabs-preview__content">
<text className="tabs-preview__content-text">Content 2</text>
</view>
)}
{activeTab === "3" && (
<view className="tabs-preview__content">
<text className="tabs-preview__content-text">Content 3</text>
</view>
)}
</TabsRoot>
);
}Dynamic Height
TabsCarousel의 autoHeight, lazyMount, unmountOnExit 없이도 TabsContent를 Carousel 밖에 배치하면 선택된 콘텐츠 높이에 맞춰 아래 콘텐츠의 위치를 바꿀 수 있습니다. 이 구성은 스와이프를 제공하지 않습니다.
import "./styles";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
export default function Example() {
return (
<view className="tabs-preview tabs-preview--dynamic-height">
<TabsRoot defaultValue="1" style={{ width: "100%" }}>
<TabsList>
<TabsTrigger value="1">라벨1</TabsTrigger>
<TabsTrigger value="2">라벨2</TabsTrigger>
<TabsTrigger value="3">라벨3</TabsTrigger>
</TabsList>
<TabsContent
className="tabs-preview__dynamic-content"
value="1"
style={{ height: "100px" }}
>
<text className="tabs-preview__content-text">Content 1</text>
</TabsContent>
<TabsContent
className="tabs-preview__dynamic-content"
value="2"
style={{ height: "200px" }}
>
<text className="tabs-preview__content-text">Content 2</text>
</TabsContent>
<TabsContent
className="tabs-preview__dynamic-content"
value="3"
style={{ height: "300px" }}
>
<text className="tabs-preview__content-text">Content 3</text>
</TabsContent>
</TabsRoot>
<view className="tabs-preview__dynamic-footer">
<text className="tabs-preview__content-text">아래 콘텐츠</text>
</view>
</view>
);
}Scroll to Top
TabsTrigger의 bindtap은 이미 선택된 Trigger를 다시 탭할 때도 호출됩니다. 아래 예시는 네 번째 항목부터 보이도록 시작하며, 선택된 탭을 다시 탭하면 현재 콘텐츠의 scroll-view가 맨 위로 이동합니다.
import "./styles";
import { useCallback, useRef, useState } from "@lynx-js/react";
import type { NodesRef } from "@lynx-js/types";
import { TabsCarousel, TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs";
const SCROLL_ITEMS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function scrollToTop(ref: NodesRef | null) {
"background only";
ref?.invoke({ method: "scrollTo", params: { offset: 0, smooth: true } }).exec();
}
export default function Example() {
const [currentTab, setCurrentTab] = useState("1");
const firstContentRef = useRef<NodesRef | null>(null);
const secondContentRef = useRef<NodesRef | null>(null);
const setFirstContentRef = useCallback((node: NodesRef | null) => {
"background only";
firstContentRef.current = node;
}, []);
const setSecondContentRef = useCallback((node: NodesRef | null) => {
"background only";
secondContentRef.current = node;
}, []);
function handleFirstTriggerTap() {
"background only";
if (currentTab === "1") scrollToTop(firstContentRef.current);
}
function handleSecondTriggerTap() {
"background only";
if (currentTab === "2") scrollToTop(secondContentRef.current);
}
return (
<TabsRoot
triggerLayout="fill"
value={currentTab}
onValueChange={setCurrentTab}
style={{ width: "100%" }}
>
<TabsList>
<TabsTrigger value="1" bindtap={handleFirstTriggerTap}>
라벨1
</TabsTrigger>
<TabsTrigger value="2" bindtap={handleSecondTriggerTap}>
라벨2
</TabsTrigger>
</TabsList>
<TabsCarousel swipeable className="tabs-preview__scroll-top-carousel">
<TabsContent value="1">
<scroll-view
ref={setFirstContentRef}
scroll-orientation="vertical"
initial-scroll-offset={300}
className="tabs-preview__scroll-top"
>
<view className="tabs-preview__scroll-top-content">
{SCROLL_ITEMS.map((item) => (
<view key={item} className="tabs-preview__scroll-top-item">
<text className="tabs-preview__content-text">Content 1 · {item}</text>
</view>
))}
</view>
</scroll-view>
</TabsContent>
<TabsContent value="2">
<scroll-view
ref={setSecondContentRef}
scroll-orientation="vertical"
initial-scroll-offset={300}
className="tabs-preview__scroll-top"
>
<view className="tabs-preview__scroll-top-content">
{SCROLL_ITEMS.map((item) => (
<view key={item} className="tabs-preview__scroll-top-item">
<text className="tabs-preview__content-text">Content 2 · {item}</text>
</view>
))}
</view>
</scroll-view>
</TabsContent>
</TabsCarousel>
</TabsRoot>
);
}Web Version Differences
- 탭 이벤트는
onClick대신bindtap으로 받습니다. - Carousel은 네이티브
<viewpager>, 가로 탭 목록은<scroll-view>를 사용합니다. - 접근성은 ARIA 대신 Lynx의
accessibility-*속성을 사용합니다.
스와이프와 Indicator 이동은 QR 코드 패널에서 Lynx Explorer로 확인하세요. 브라우저 미리보기와 네이티브 기기의 제스처 결과가 다를 수 있습니다.
Unsupported Lynx Features
- 세로
orientation - 키보드 방향키, Home/End, roving focus
- RTL 순서와 스와이프 방향 반전
asChildlazyMount,unmountOnExit- Carousel의
loop,autoHeight,dragThreshold,carouselPreventDrag - 콘텐츠 전환 애니메이션
Last updated on