콘텐츠로 이동

Tab — spec

Synthesized from MUI Tab. A single tab inside a Tabs set. Used for top-level navigation within a section ("개요 / 활동 / 멤버") and for bottom navigation patterns.

When to use

  • Mutually exclusive views of the same context (account → profile / security / billing).
  • Switching between datasets in a dashboard.
  • For top-level app navigation, prefer dedicated nav (drawer, top bar) — Tabs are within-page.

When NOT to use

  • 2 options → use ToggleButtonGroup or Radio.
  • 7+ options → switch to a dropdown or a sidebar.

Anatomy

[Tab1]  [Tab2]  [Tab3]
─────  ─────  ─────
        ▔▔▔        ← indicator under selected

API

<Tabs value={tab} onChange={(_, v) => setTab(v)} aria-label="account sections">
  <Tab label="프로필" value="profile" />
  <Tab label="보안" value="security" />
  <Tab label="결제" value="billing" disabled />
</Tabs>
Prop Type Default Description
label ReactNode Tab label
value any Identifies tab; matched against parent Tabs.value
icon ReactNode Leading icon (above label by default)
iconPosition 'top' \| 'bottom' \| 'start' \| 'end' 'top' Icon placement
disabled boolean false Non-interactive
wrapped boolean false Allow text to wrap to 2 lines
href string Render as link (use for tabs that map to routes)

States

State Visual
Default fg-muted, no underline
Hover fg-default
Focus-visible fg-default + ring
Selected fg-primary, underline indicator
Disabled reduced opacity

Tokens consumed

--tab-fg-default          /* fg-muted */
--tab-fg-selected         /* fg-primary OR fg-default — design-system choice */
--tab-indicator-color     /* brand */
--tab-indicator-height    /* 2px */
--tab-min-width-72        /* default min */
--tab-min-height-48       /* touch */

Accessibility

  • Renders role="tab" inside role="tablist". Selected = aria-selected="true".
  • Keyboard: ArrowLeft/Right cycle between tabs. Home/End jump.
  • For tabs with associated panels: aria-controls="panel-id" + matching role="tabpanel" + aria-labelledby="tab-id".
  • Disabled tabs are skipped in keyboard nav by default.

Edge cases

  • Long labels in Korean — Hangul is wider; expect tabs to overflow. Use <Tabs variant="scrollable"> for horizontal scroll on overflow.
  • Mobile narrow viewportvariant="fullWidth" distributes tabs evenly; OK for ≤ 4 tabs. For 5+, switch to scrollable.
  • Icon + label — label first then icon-only on narrow → use useMediaQuery to switch iconPosition.
  • Nested tabs — discouraged but possible. Aria gets confusing; consider a sub-nav pattern instead.

Code example

function AccountTabs() {
  const [tab, setTab] = useState('profile');
  const navigate = useNavigate();

  return (
    <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
      <Tabs
        value={tab}
        onChange={(_, v) => { setTab(v); navigate(`/account/${v}`); }}
        aria-label="계정 설정"
        variant="scrollable"
        scrollButtons="auto"
      >
        <Tab label="프로필" value="profile" />
        <Tab label="보안" value="security" />
        <Tab label="결제" value="billing" />
        <Tab label="알림" value="notifications" />
        <Tab label="개인정보" value="privacy" />
      </Tabs>
    </Box>
  );
}

Don't

  • Don't use Tabs for binary on/off — use Switch or ToggleButton.
  • Don't put 2 separate Tabs sets on the same screen — confuses keyboard navigation.
  • Don't use Tab text as the page heading — they have separate semantics.

References

Cross-reference