Free pack

Tabs — <mv-tabs>

Accessible tabs with a sliding indicator (underline, pill or segmented), horizontal or vertical, automatic or manual activation, and crossfading panels.

CategoryNavigation
TypeWeb Component (<mv-tabs>)
Statusstable
Keywordstabs, onglets, segmented-control, underline, pill, vertical, indicator

When to use

Avoid when

Install

node scripts/add.mjs tabs --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["tabs"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, components/tabs/tabs.js, components/tabs/tabs.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,22rem),1fr));gap:2.5rem 3rem;width:100%;font-size:.875rem;color:var(--mv-fg-muted);line-height:1.55">

  <!-- Underline, with icons and a count badge -->
  <mv-tabs data-variant="underline" value="apercu" style="grid-column:1/-1">
    <div role="tablist" aria-label="Project Atlas">
      <button role="tab" data-value="apercu">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
        Overview
      </button>
      <button role="tab" data-value="activite">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
        Activity <span class="mv-tabs-count">12</span>
      </button>
      <button role="tab" data-value="membres">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>
        Members
      </button>
      <button role="tab" data-value="parametres">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 7h-9M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
        Settings
      </button>
      <button role="tab" data-value="archives" disabled>Archive</button>
    </div>
    <div role="tabpanel" data-value="apercu">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">Customer portal redesign</p>
      <p style="margin:0">Launch is set for Oct 14. 18 of 24 tasks are done; two design reviews still need to be scheduled with the product team.</p>
    </div>
    <div role="tabpanel" data-value="activite">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">12 new updates</p>
      <p style="margin:0">Emma approved the dashboard mockup, Kenji opened 3 tickets on PDF export and Amara merged the “checkout-flow” branch.</p>
    </div>
    <div role="tabpanel" data-value="membres">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">6 members</p>
      <p style="margin:0">Emma Clarke (owner), Kenji Tanaka, Amara Okafor, Lucas Silva, Sofia Rossi and Omar Haddad have access to the project.</p>
    </div>
    <div role="tabpanel" data-value="parametres">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">Visibility: private</p>
      <p style="margin:0">Only invited members can view and edit this project. Notifications go out every morning at 9 a.m.</p>
    </div>
    <div role="tabpanel" data-value="archives"><p style="margin:0">Nothing archived yet.</p></div>
  </mv-tabs>

  <!-- Segmented (default) -->
  <mv-tabs value="annuel" data-stretch style="max-width:22rem">
    <div role="tablist" aria-label="Billing period">
      <button role="tab" data-value="mensuel">Monthly</button>
      <button role="tab" data-value="annuel">Yearly <span class="mv-tabs-count">−20%</span></button>
    </div>
    <div role="tabpanel" data-value="mensuel">
      <p style="margin:0;font-size:1.875rem;font-weight:700;letter-spacing:-.02em;color:var(--mv-fg)">$24 <span style="font-size:.875rem;font-weight:500;color:var(--mv-fg-muted)">/ month</span></p>
      <p style="margin:.25rem 0 0">No commitment, cancel anytime.</p>
    </div>
    <div role="tabpanel" data-value="annuel">
      <p style="margin:0;font-size:1.875rem;font-weight:700;letter-spacing:-.02em;color:var(--mv-fg)">$19 <span style="font-size:.875rem;font-weight:500;color:var(--mv-fg-muted)">/ month</span></p>
      <p style="margin:.25rem 0 0">Billed $228 a year — two months free.</p>
    </div>
  </mv-tabs>

  <!-- Pill -->
  <mv-tabs data-variant="pill" data-size="sm">
    <div role="tablist" aria-label="Filter orders">
      <button role="tab">All</button>
      <button role="tab">In progress</button>
      <button role="tab">Delivered</button>
      <button role="tab">Returned</button>
    </div>
    <div role="tabpanel"><p style="margin:0">148 orders in the last 30 days, totaling $12,480.</p></div>
    <div role="tabpanel"><p style="margin:0">9 orders being prepared, 3 of them shipped today.</p></div>
    <div role="tabpanel"><p style="margin:0">132 orders delivered, with a 2.4-day average delivery time.</p></div>
    <div role="tabpanel"><p style="margin:0">7 returns awaiting a refund.</p></div>
  </mv-tabs>

  <!-- Vertical, manual activation -->
  <mv-tabs data-variant="underline" orientation="vertical" activation="manual" style="grid-column:1/-1">
    <div role="tablist" aria-label="Account settings">
      <button role="tab">Profile</button>
      <button role="tab">Notifications</button>
      <button role="tab">Security</button>
      <button role="tab">Billing</button>
    </div>
    <div role="tabpanel">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">Public profile</p>
      <p style="margin:0">Your name, photo and bio are visible to members of your workspaces.</p>
    </div>
    <div role="tabpanel">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">Notifications</p>
      <p style="margin:0">Get a daily digest or an alert for every mention.</p>
    </div>
    <div role="tabpanel">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">Two-factor authentication enabled</p>
      <p style="margin:0">Last sign-in on Sep 21 from London, UK (Firefox on macOS).</p>
    </div>
    <div role="tabpanel">
      <p style="margin:0 0 .25rem;color:var(--mv-fg);font-weight:600">Team plan</p>
      <p style="margin:0">Next invoice of $228 on Nov 1, charged to the Visa ending in 4242.</p>
    </div>
  </mv-tabs>
</div>

API

Attributes

NameTypeDefaultDescription
valuestringValue of the active tab (the tab’s data-value, otherwise its index). Reflected on every change.
activationautomatic | manualautomaticautomatic: arrows activate the tab; manual: arrows move focus, Enter/Space activates.
orientationhorizontal | verticalhorizontalList above or to the left of the panels; also changes the arrow keys.
data-variantsegmented | pill | underlinesegmentedsegmented: gray track with a raised pill; pill: accent pill with no track; underline: sliding accent bar.
data-sizesm | lg(md)Tab height.
data-stretchbooleanTabs share the full width.

Properties

NameTypeDescription
selectedTabHTMLElement | nullCurrently selected tab (read-only).

Methods

NameDescription
select(value)Activates the tab with that value and emits mv-change.

Events

NameDescription
mv-changeUser-initiated change. detail: { value, previous, tab, panel }.

Content structure

NameDescription
[role=tablist]Direct child; contains the <button role="tab"> elements (direct children, optional data-value, can be disabled).
[role=tabpanel]Direct children of <mv-tabs>, matched by data-value or by order.

CSS classes

NameDescription
mv-tabs-indicatorSliding indicator, created automatically inside the list.
mv-tabs-countCount badge inside a tab (gets accented when the tab is active).

CSS variables

NameDefaultDescription
--mv-tabs-indicatorIndicator color (pill background or bar).
--mv-tabs-radiusvar(--mv-radius-md)Radius of the segmented track.
--mv-tabs-x / -y / -w / -hIndicator position and size, written by the JS (read-only).

Accessibility

ARIA Tabs pattern: tablist/tab/tabpanel roles, aria-selected, aria-controls and aria-labelledby wired automatically, aria-orientation. Roving tabindex: a single Tab stop to enter the list, arrows (left/right or up/down depending on orientation), Home/End; disabled tabs are skipped. Panels get tabindex=0 and inactive panels are hidden. Reduced motion: instant indicator and fade.