Getting Started
App Providerglobal.d.tsThemesTypes
cn(...args)Text ParserUnits Converters
AnchorAvatarBreadcrumbBurgerButtonCardCarouselCheckerCodeColor PickerCommandConfettiCopyButtonDouble Helix WordsFloating IndicatorGroupHighlight TextIndicatorInputKbdLabelLoaderPaginationPassword RequirementPolymorphic SlotProgressProseRatingRunning AreaScroll AreaSheetsSkeletonSliderStackSvgTableTabsTextareaTimelineTimesToasterTooltipTyping WordsTypography
useClickOutsideuseClipboarduseDeviceInfouseDialoguseDidUpdateuseDirectionuseDisclosureuseDocumentTitleuseDocumentVisibilityuseElementInfouseEyeDropperuseFetchuseFullscreenuseGeoLocationuseHotkeysuseHoveruseIduseImagePopupuseInputStateuseIntersectionuseIntervaluseIsomorphicEffectuseListStateuseLocalStorageuseMeasureScrollbaruseMediaQueryuseMergedRefuseMouseuseMoveuseMutationObserveruseNetworkuseOpenStateuseOrientationuseOSusePaginationusePWAInstalleruseRandomColorsuseReducedMotionuseReloaduseResizeObserveruseScrollIntoViewuseStateHistoryuseTimeoutuseTouchuseTriggeruseUncontrolleduseValidatedStateuseViewportSizeuseWindowEventuseWindowScroll
Docs
Web
Components
Tabs
Table

A structured component for displaying tabular data with customizable formatting.

Textarea

A form input for capturing multi-line text with customizable styling and behavior (support JsonInput).


Edit this page on GitHub
  • Started
  • Utilities
  • Configuration
  • Components
  • Hooks
  • Examples
  • Github
  • Contributing
⌘+J

© 2025 oeri rights MIT


Designed in Earth-616

Built by oeri

Tabs

A component for managing multiple views or content sections in a single space using tabbed navigation.


The Tabs component is a powerful navigation tool that organizes content into multiple sections, allowing users to switch between different views seamlessly. It is highly flexible and customizable, supporting various layouts, dynamic positioning, and advanced styling options to fit a wide range of use cases, from simple content toggling to complex interfaces.


Usage

Basic usage example to quickly see how the Tabs component works.

Branch tab content
Commit tab content
Pull Request tab content
// Using Compound Components is only supported on client side rendering
"use client";
import { Tabs } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";

export function TabsDemo() {
  return (
    <Tabs defaultValue="branch" color="#f08c00" classNames={{ panel: "min-w-56 p-4" }}>
      <Tabs.List>
        <Tabs.Tab value="branch" leftSection={<GitBranchIcon size={14} />}>Branch</Tabs.Tab>
        <Tabs.Tab value="commit" leftSection={<GitCommitIcon size={14} />}>Commit</Tabs.Tab>
        <Tabs.Tab value="pullrequest" leftSection={<GitPullRequestIcon size={14} />}>Pull Request</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="branch">Branch tab content</Tabs.Panel>
      <Tabs.Panel value="commit">Commit tab content</Tabs.Panel>
      <Tabs.Panel value="pullrequest">Pull Request tab content</Tabs.Panel>
    </Tabs>
  );
}

Properties

Interactive configurator to explore customization options for the Tabs component.

Round
Color
// Using Compound Components is only supported on client side rendering
"use client";
import { Tabs } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";

export function TabsDemo() {
  return (
    <Tabs defaultValue="commit" classNames={{ root: "data-[orientation=horizontal]:flex data-[orientation=horizontal]:flex-col data-[inverted]:data-[orientation=horizontal]:flex-col-reverse size-full", panel: "min-w-56 p-4" }}>
      <Tabs.List>
        <Tabs.Tab value="branch" leftSection={<GitBranchIcon size={14} />}>Branch</Tabs.Tab>
        <Tabs.Tab value="commit" leftSection={<GitCommitIcon size={14} />}>Commit
        </Tabs.Tab>
        <Tabs.Tab value="pullrequest" leftSection={<GitPullRequestIcon size={14} />}>Pull Request</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="branch">Branch tab content</Tabs.Panel>
      <Tabs.Panel value="commit">Commit tab content</Tabs.Panel>
      <Tabs.Panel value="pullrequest">Pull Request tab content</Tabs.Panel>
    </Tabs>
  );
}

Tabs Position

Showcase how to change the position of tabs (top, bottom, left, right) to better match the layout needs of your application.

// Using Compound Components is only supported on client side rendering
"use client";
import { Tabs } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";

export function TabsDemo() {
  return (
    <Tabs defaultValue="commit" classNames={{ panel: "min-w-56 p-4" }}>
      <Tabs.List>
        <Tabs.Tab value="branch" leftSection={<GitBranchIcon size={14} />}>Branch</Tabs.Tab>
        <Tabs.Tab value="commit" leftSection={<GitCommitIcon size={14} />}>Commit
        </Tabs.Tab>
        <Tabs.Tab value="pullrequest" leftSection={<GitPullRequestIcon size={14} />}>Pull Request</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="branch">Branch tab content</Tabs.Panel>
      <Tabs.Panel value="commit">Commit tab content</Tabs.Panel>
      <Tabs.Panel value="pullrequest">Pull Request tab content</Tabs.Panel>
    </Tabs>
  );
}

Tabs Customs

Demonstrates how to customize the Tabs component, such as adding a floating indicator to tab triggers for a more dynamic and modern UI experience.

"use client";
import React from "react";
import { Tabs, type TabsProps, type TabsListProps } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";
import { FloatingIndicator } from "@/ui/floating-indicator";

const data = [
  {
    label: "Branch",
    description: "Branches are a core feature of version control systems, allowing you to isolate and manage changes in your codebase. A branch represents a separate line of development where you can experiment with new features, fix bugs, or make changes without impacting the main codebase. Once your work is complete, you can merge the branch back into the main development stream.",
    icon: <GitBranchIcon size={14} />
  },
  {
    label: "Commit",
    description: "Commits are snapshots of your project at a specific point in time. They serve as a way to document changes in your code, providing a history of development that can be referenced or reverted to if needed. Each commit includes a message describing the changes, making it easier to track the evolution of your codebase over time.",
    icon: <GitCommitIcon size={14} />
  },
  {
    label: "Pull Request",
    description: "Pull requests facilitate collaboration in version control workflows, enabling developers to propose changes, review code, and discuss potential improvements before merging into the main branch. They promote code quality, encourage team collaboration, and provide a platform to ensure that every change meets the required standards before integration.",
    icon: <GitPullRequestIcon size={14} />
  }
];

const validatedValue = (value: string) => value.trim().toLowerCase();
const DEFAULT_VALUE: string = validatedValue(data[0]?.label);

export function TabsDemo() {
  const [parentRef, setParentRef] = React.useState<HTMLDivElement | null>(null);
  const [controlsRefs, setControlsRefs] = React.useState<Record<string, HTMLButtonElement | null>>({});
  const [active, setActive] = React.useState<string>(DEFAULT_VALUE);
  const [hover, setHover] = React.useState<string | null>(null);

  const setControlRef = (key: string) => (node: HTMLButtonElement) => {
    controlsRefs[key] = node;
    setControlsRefs(controlsRefs);
  };

  const propsTab = (key: string) => ({
    ref: setControlRef(key),
    onClick: () => setActive(key),
    onMouseEnter: () => setHover(key),
    onMouseLeave: () => setHover(null),
  });

  return (
    <Tabs defaultValue={DEFAULT_VALUE} classNames={{ root: "size-full", panel: "min-w-56 p-4" }}>
      <Tabs.List ref={setParentRef}>
        {data.map((i, _i) => (
          <Tabs.Tab key={_i} value={validatedValue(i.label)} leftSection={i.icon} {...propsTab(validatedValue(i.label))}>
            {i.label}
          </Tabs.Tab>
        ))}

        <FloatingIndicator
          target={controlsRefs[hover ?? active]}
          parent={parentRef}
          transitionDuration={350}
          className="bg-muted/60 border-muted-foreground/60 rounded-md rounded-b-none border-b-transparent -z-1 shadow-md border-2"
        />
      </Tabs.List>

      {data.map((i, _i) => (
        <Tabs.Panel key={_i} value={validatedValue(i.label)} className="overflow-auto max-h-full max-w-full">
          {i.description}
        </Tabs.Panel>
      ))}
    </Tabs>
  );
}

Tip:

You can combine floating indicators with animated transitions to create a smoother and more engaging tab switching experience.

Note:

When using custom tab indicators or advanced tab positioning, ensure that accessibility attributes like aria-selected and aria-controls are properly handled to maintain a user-friendly and accessible interface.

Reminder:

Keep tab labels short and meaningful to help users quickly understand the available sections. Long labels may cause layout issues, especially on smaller screens.


API References

Styles API

type T = "root" | "list" | "tab" | "tabLabel" | "tabSection" | "panel";
Styles APITypeDefaultAnnotation
unstyled?Partial<Record<T, boolean>>falseif true, default styles will be removed
className?stringundefinedpass to root component <div>
classNames?Partial<Record<T, string>>undefined
style?CSSPropertiesundefinedpass to root component <div>
styles?Partial<Record<T, CSSProperties>>undefined

Props API

Props APITypeDefaultAnnotation
defaultValue?string | nullDefault value for uncontrolled component
value?string | nullValue for controlled component
onChange?(value: string | null) => voidCalled when value changes
orientation?'vertical' | 'horizontal'horizontalTabs orientation
placement?'left' | 'right''left'Tabs.List placement relative to Tabs.Panel, applicable only when orientation="vertical"
id?stringgenerated randomlyBase id, used to generate ids to connect labels with controls
loop?booleantrueDetermines whether arrow key presses should loop though items (first to last and last to first)
activateTabWithKeyboard?booleantrueDetermines whether tab should be activated with arrow key press
allowTabDeactivation?booleanfalseDetermines whether tab can be deactivated
children?React.ReactNodeundefinedTabs content
color?Colors``Changes colors of Tabs.Tab components when variant is pills or default, does nothing for other variants
radius?(string & {}) | number``Key of valid CSS value to set border-radius
inverted?booleanfalseDetermines whether tabs should have inverted styles
keepMounted?booleantrueIf set to false, Tabs.Panel content will be unmounted when the associated tab is not active

Source Codes

Full working code example, including necessary markup and styles. You can copy and paste this code directly to start using the component immediately.

tabs.tsx
globals.css
/* tabs */ @layer base { .stylelayer-tabs { @apply [--tab-border-color:hsl(var(--border))] [display:--tabs-display] [flex-direction:--tabs-flex-direction] [--tab-justify:flex-start] [--tabs-panel-grow:unset] [--tabs-display:block] [--tabs-flex-direction:row] [--tabs-list-border-width:0] [--tabs-list-border-size:0_0_var(--tabs-list-border-width)_0] [--tabs-list-line-bottom:0] [--tabs-list-line-top:unset] [--tabs-list-line-start:0] [--tabs-list-line-end:0] [--tab-round:var(--tabs-round)_var(--tabs-round)_0_0] [--tab-border-width:0_0_var(--tabs-list-border-width)_0]; &[data-inverted] { @apply [--tabs-list-line-bottom:unset] [--tabs-list-line-top:0] [--tab-round:0_0_var(--tabs-round)_var(--tabs-round)] [--tab-border-width:var(--tabs-list-border-width)_0_0_0]; & .tabs-list[data-variant="default"]::before, & .tabs-list[data-variant="outline"]::before { @apply top-0 bottom-[unset]; } } &[data-orientation="vertical"] { @apply [--tabs-list-line-start:unset] [--tabs-list-line-end:0] [--tabs-list-line-top:0] [--tabs-list-line-bottom:0] [--tabs-list-border-size:0_var(--tabs-list-border-width)_0_0] [--tab-border-width:0_var(--tabs-list-border-width)_0_0] [--tab-round:var(--tabs-round)_0_0_var(--tabs-round)] [--tabs-panel-grow:1] [--tabs-display:flex]; &:where([dir="rtl"]) { @apply [--tabs-list-border-size:0_0_0_var(--tabs-list-border-width)] [--tab-border-width:0_0_0_var(--tabs-list-border-width)] [--tab-round:0_var(--tabs-round)_var(--tabs-round)_0]; } &[data-placement="right"] { @apply [--tabs-flex-direction:row-reverse] [--tabs-list-line-start:0] [--tabs-list-line-end:unset] [--tabs-list-border-size:0_0_0_var(--tabs-list-border-width)] [--tab-border-width:0_0_0_var(--tabs-list-border-width)] [--tab-round:_0_var(--tabs-round)_var(--tabs-round)_0]; &:where([dir="rtl"]) { @apply [--tabs-list-border-size:0_var(--tabs-list-border-width)_0_0] [--tab-border-width:0_var(--tabs-list-border-width)_0_0] [--tab-round:var(--tabs-round)_0_0_var(--tabs-round)]; } } } &[data-orientation="horizontal"] { @apply [--tab-justify:center]; } &[data-variant="default"] { @apply [--tabs-list-border-width:2px] [--tab-hover-color:hsl(var(--muted)/0.5)]; } &[data-variant="outline"] { @apply [--tabs-list-border-width:0.0625rem]; } &[data-variant="pills"] { @apply [--tab-hover-color:hsl(var(--muted)/0.5)]; } & .tabs-list { &[data-grow] { @apply [--tab-grow:1]; } &[data-variant="default"], &[data-variant="outline"] { @apply relative; &::before { @apply [content:--content,''] absolute border-solid border-[--tab-border-color] [border-width:--tabs-list-border-size] bottom-[--tabs-list-line-bottom] left-[--tabs-list-line-start] right-[--tabs-list-line-end] top-[--tabs-list-line-top]; } } } & .tabs-panel { @apply [flex-grow:--tabs-panel-grow]; } & .tabs-tab { @apply relative py-2 px-4 text-sm whitespace-nowrap z-[0] flex items-center leading-none select-none [flex-grow:--tab-grow] [justify-content:--tab-justify] focus:z-[1] focus-visible:z-1; &:disabled, &[data-disabled], &[aria-disabled="true"] { @apply opacity-50 cursor-not-allowed; } &[data-variant="default"] { @apply [--tab-bg:transparent] rounded-[--tab-round] border-solid border-transparent [border-width:--tab-border-width] bg-[--tab-bg] active:[&:where(:not([data-active]))]:border-[--tab-border-color] active:[&:where(:not([data-active]))]:[--tab-bg:hsl(var(--muted))] [&:where([data-active])]:border-[--tabs-color] [&:where([data-active])]:active:[--tab-bg:--tab-hover-color]; &:disabled, &[data-disabled] { @apply hover:[--tab-bg:transparent]; } } &[data-variant="outline"] { @apply relative border border-solid border-transparent rounded-[--tab-round] border-t-[--tab-border-top-color] border-b-[--tab-border-bottom-color] [--tab-border-bottom-color:transparent] [--tab-border-top-color:transparent] [--tab-border-inline-end-color:transparent] [--tab-border-inline-start-color:transparent]; &:where([data-active]) { @apply border-t-[--tab-border-top-color] border-b-[--tab-border-bottom-color] border-l-[--tab-border-inline-start-color] border-r-[--tab-border-inline-end-color] [--tab-border-top-color:--tab-border-color] [--tab-border-inline-start-color:--tab-border-color] [--tab-border-inline-end-color:--tab-border-color] [--tab-border-bottom-color:hsl(var(--background))]; &::before { @apply content-[''] absolute bg-[--tab-border-color] bottom-[--tab-before-bottom,-0.0625rem] left-[--tab-before-left,-0.0625rem] right-[--tab-before-right,auto] top-[--tab-before-top,auto] size-px; } &::after { @apply content-[''] absolute bg-[--tab-border-color] bottom-[--tab-after-bottom,-0.0625rem] right-[--tab-after-right,-0.0625rem] left-[--tab-after-left,auto] top-[--tab-after-top,auto] size-px; } &[data-inverted] { @apply [--tab-border-bottom-color:--tab-border-color] [--tab-border-top-color:--tab-border-color] [--tab-before-bottom:auto] [--tab-before-top:-0.0625rem] [--tab-after-bottom:auto] [--tab-after-top:-0.0625rem]; &[data-orientation="horizontal"] { @apply [--tab-border-top-color:hsl(var(--background))]; } } &[data-orientation="vertical"] { &[data-placement="left"] { @apply [--tab-border-inline-end-color:hsl(var(--background))] [--tab-border-inline-start-color:--tab-border-color] [--tab-border-bottom-color:--tab-border-color] [--tab-before-right:-0.0625rem] [--tab-before-left:auto] [--tab-before-bottom:auto] [--tab-before-top:-0.0625rem] [--tab-after-left:auto] [--tab-after-right:-0.0625rem]; &:where([dir="rtl"]) { @apply [--tab-before-right:auto] [--tab-before-left:-0.0625rem] [--tab-after-left:-0.0625rem] [--tab-after-right:auto]; } } &[data-placement="right"] { @apply [--tab-border-inline-start-color:hsl(var(--background))] [--tab-border-inline-end-color:--tab-border-color] [--tab-border-bottom-color:--tab-border-color] [--tab-before-left:-0.0625rem] [--tab-before-right:auto] [--tab-before-bottom:auto] [--tab-before-top:-0.0625rem] [--tab-after-right:auto] [--tab-after-left:-0.0625rem]; &:where([dir="rtl"]) { @apply [--tab-before-left:auto] [--tab-before-right:-0.0625rem] [--tab-after-right:-0.0625rem] [--tab-after-left:auto]; } } } } } &[data-variant="pills"] { @apply rounded-[--tabs-round] bg-[--tab-bg] text-[--tab-color] [--tab-bg:transparent] [--tab-color:inherit]; &:not([data-disabled]) { @apply hover:[--tab-bg:--tab-hover-color]; } &[data-active][data-active] { @apply [--tab-bg:--tabs-color] [--tab-color:--tabs-text-color] hover:[--tab-bg:--tabs-color]; } } } & .tabs-tab-section { @apply flex items-center justify-center ml-[--tab-section-margin-left,0] mr-[--tab-section-margin-right,0]; &[data-position="left"] { &:not(:only-child) { @apply [--tab-section-margin-right:0.625rem]; &:where([dir="rtl"]) { @apply [--tab-section-margin-right:0rem] [--tab-section-margin-left:0.625rem]; } } } &[data-position="right"] { &:not(:only-child) { @apply [--tab-section-margin-left:0.625rem]; &:where([dir="rtl"]) { @apply [--tab-section-margin-left:0rem] [--tab-section-margin-right:0.625rem]; } } } } } }