@olundot/ui · Surfaces
Tabs
같은 페이지 맥락에 속하는 동등한 패널 간 전환을 위한 탭 서피스 패턴입니다.
- API
- tabs(TabItem[]) · activeKey · onChange(key) · baseId(a11y id prefix)
- variant
- line(기본) · contained
- orientation
- horizontal(기본) · vertical — Radix keyboard semantics 유지
- TabItem
- key · label · badge?(number) · disabled?(boolean)
- TabsContent
- tabKey + baseId — aria-controls/aria-labelledby 자동 연결
- 배지
- badge > 0일 때 자동 표시, 99 초과 시 '99+' 처리
- 접근성
- Radix가 tablist/tab/tabpanel + arrow-key 네비게이션 자동 관리
기본
activeKey와 onChange로 탭 상태 관리시험 기간, 접근 규칙, 준비 상태를 확인합니다.
코드 보기tsx
import { useState } from "react";
import { Tabs, TabsContent } from "@olundot/ui";
export function TabsBasic() {
const [active, setActive] = useState("overview");
const tabs = [
{ key: "overview", label: "개요" },
{ key: "questions", label: "문항" },
{ key: "students", label: "수강생" },
];
return (
<Tabs tabs={tabs} activeKey={active} onChange={setActive} baseId="basic-tabs">
<TabsContent tabKey="overview" baseId="basic-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
시험 기간, 접근 규칙, 준비 상태를 확인합니다.
</p>
</TabsContent>
<TabsContent tabKey="questions" baseId="basic-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
32개 문항 목록입니다.
</p>
</TabsContent>
<TabsContent tabKey="students" baseId="basic-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
128명의 수강생이 등록되어 있습니다.
</p>
</TabsContent>
</Tabs>
);
}배지 포함
탭 label 옆에 카운트 배지 표시응시 대기 12명
코드 보기tsx
import { useState } from "react";
import { Tabs, TabsContent } from "@olundot/ui";
export function TabsWithBadges() {
const [active, setActive] = useState("pending");
const tabs = [
{ key: "all", label: "전체", badge: 48 },
{ key: "pending", label: "대기", badge: 12 },
{ key: "submitted", label: "제출", badge: 36 },
{ key: "graded", label: "채점 완료", badge: 0 },
];
return (
<Tabs tabs={tabs} activeKey={active} onChange={setActive} baseId="badge-tabs">
<TabsContent tabKey="all" baseId="badge-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>전체 48명</p>
</TabsContent>
<TabsContent tabKey="pending" baseId="badge-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>응시 대기 12명</p>
</TabsContent>
<TabsContent tabKey="submitted" baseId="badge-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>제출 완료 36명</p>
</TabsContent>
<TabsContent tabKey="graded" baseId="badge-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>채점 완료 0명</p>
</TabsContent>
</Tabs>
);
}Contained
bounded surface 안에서 쓰는 segmented tab전체 항목을 표시합니다.
코드 보기tsx
import { useState } from "react";
import { Tabs, TabsContent } from "@olundot/ui";
export function TabsContained() {
const [active, setActive] = useState("all");
const tabs = [
{ key: "all", label: "전체" },
{ key: "ready", label: "준비됨" },
{ key: "blocked", label: "보류" },
];
return (
<Tabs variant="contained" tabs={tabs} activeKey={active} onChange={setActive} baseId="contained-tabs">
<TabsContent tabKey="all" baseId="contained-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>전체 항목을 표시합니다.</p>
</TabsContent>
<TabsContent tabKey="ready" baseId="contained-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>준비된 항목입니다.</p>
</TabsContent>
<TabsContent tabKey="blocked" baseId="contained-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>보류 중인 항목입니다.</p>
</TabsContent>
</Tabs>
);
}Vertical
긴 레이블과 넓은 레이아웃을 위한 세로 탭프로필 정보를 확인합니다.
코드 보기tsx
import { useState } from "react";
import { Tabs, TabsContent } from "@olundot/ui";
export function TabsVertical() {
const [active, setActive] = useState("profile");
const tabs = [
{ key: "profile", label: "프로필" },
{ key: "security", label: "보안 설정" },
{ key: "billing", label: "결제 정보" },
];
return (
<Tabs orientation="vertical" tabs={tabs} activeKey={active} onChange={setActive} baseId="vertical-tabs">
<TabsContent tabKey="profile" baseId="vertical-tabs">
<p style={{ color: "var(--text-secondary)", fontSize: "0.875rem" }}>프로필 정보를 확인합니다.</p>
</TabsContent>
<TabsContent tabKey="security" baseId="vertical-tabs">
<p style={{ color: "var(--text-secondary)", fontSize: "0.875rem" }}>보안 설정을 관리합니다.</p>
</TabsContent>
<TabsContent tabKey="billing" baseId="vertical-tabs">
<p style={{ color: "var(--text-secondary)", fontSize: "0.875rem" }}>결제 정보를 확인합니다.</p>
</TabsContent>
</Tabs>
);
}비활성 탭
선택할 수 없는 탭 상태발행된 강의 목록입니다.
코드 보기tsx
import { useState } from "react";
import { Tabs, TabsContent } from "@olundot/ui";
export function TabsDisabled() {
const [active, setActive] = useState("published");
const tabs = [
{ key: "published", label: "발행됨" },
{ key: "draft", label: "초안" },
{ key: "archived", label: "보관됨", disabled: true },
{ key: "deleted", label: "삭제됨", disabled: true },
];
return (
<Tabs tabs={tabs} activeKey={active} onChange={setActive} baseId="disabled-tabs">
<TabsContent tabKey="published" baseId="disabled-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
발행된 강의 목록입니다.
</p>
</TabsContent>
<TabsContent tabKey="draft" baseId="disabled-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
초안 목록입니다.
</p>
</TabsContent>
<TabsContent tabKey="archived" baseId="disabled-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
보관된 항목 (비활성)
</p>
</TabsContent>
<TabsContent tabKey="deleted" baseId="disabled-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
삭제된 항목 (비활성)
</p>
</TabsContent>
</Tabs>
);
}긴 탭 목록
좁은 영역에서 가로 스크롤로 처리긴 레이블과 많은 탭은 탭 행 안에서 가로 스크롤됩니다.
코드 보기tsx
import { useState } from "react";
import { Tabs, TabsContent } from "@olundot/ui";
export function TabsOverflow() {
const [active, setActive] = useState("overview");
const tabs = [
{ key: "overview", label: "시험 개요" },
{ key: "questions", label: "문항 구성" },
{ key: "students", label: "수강생 현황" },
{ key: "scores", label: "점수 분포" },
{ key: "appeals", label: "이의 신청" },
{ key: "history", label: "변경 이력" },
];
return (
<div style={{ maxWidth: "320px" }}>
<Tabs tabs={tabs} activeKey={active} onChange={setActive} baseId="overflow-tabs">
<TabsContent tabKey="overview" baseId="overflow-tabs">
<p style={{ padding: "16px 0", color: "var(--text-secondary)", fontSize: "0.875rem" }}>
긴 레이블과 많은 탭은 탭 행 안에서 가로 스크롤됩니다.
</p>
</TabsContent>
</Tabs>
</div>
);
}