Skip to main content
Nexus Design System
GitHub

Tabs

Tabs switch between panels of related content in one place. Use them to split a view into sections that users visit one at a time.

Make changes to your account here. Click save when you're done.

'use client';

import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from '@/components/nxds/components/tabs/tabs';

export default function TabsDefault() {
  return (
    <Tabs defaultValue="account" className="nx:w-[400px]">
      <TabsList>
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
      </TabsList>
      <TabsContent value="account">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Make changes to your account here. Click save when you&apos;re done.
        </p>
      </TabsContent>
      <TabsContent value="password">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Change your password here. After saving, you&apos;ll be logged out.
        </p>
      </TabsContent>
    </Tabs>
  );
}

Installation

npx nxds-cli@latest add tabs

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from '@/components/nxds/components/tabs/tabs';

Composition

Tabs
├── TabsList
│   └── TabsTrigger
└── TabsContent

Examples

Underline variant

Set variant="underline" on TabsList for a line under the active tab.

Account settings with underline variant tabs.

'use client';

import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from '@/components/nxds/components/tabs/tabs';

export default function TabsUnderlineVariant() {
  return (
    <Tabs defaultValue="account" className="nx:w-[400px]">
      <TabsList variant="underline">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
        <TabsTrigger value="settings">Settings</TabsTrigger>
      </TabsList>
      <TabsContent value="account">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Account settings with underline variant tabs.
        </p>
      </TabsContent>
      <TabsContent value="password">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Password settings with underline variant tabs.
        </p>
      </TabsContent>
      <TabsContent value="settings">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          General settings with underline variant tabs.
        </p>
      </TabsContent>
    </Tabs>
  );
}

Small size

Set size="sm" on TabsList for compact tabs.

Small size tabs content.

'use client';

import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from '@/components/nxds/components/tabs/tabs';

export default function TabsSmallSize() {
  return (
    <Tabs defaultValue="tab1" className="nx:w-[400px]">
      <TabsList size="sm">
        <TabsTrigger value="tab1">Small Tab</TabsTrigger>
        <TabsTrigger value="tab2">Another</TabsTrigger>
        <TabsTrigger value="tab3">Third</TabsTrigger>
      </TabsList>
      <TabsContent value="tab1">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Small size tabs content.
        </p>
      </TabsContent>
    </Tabs>
  );
}

Large size

Set size="lg" on TabsList for roomier tabs.

Large size tabs content.

'use client';

import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from '@/components/nxds/components/tabs/tabs';

export default function TabsLargeSize() {
  return (
    <Tabs defaultValue="tab1" className="nx:w-[500px]">
      <TabsList size="lg">
        <TabsTrigger value="tab1">Large Tab</TabsTrigger>
        <TabsTrigger value="tab2">Another</TabsTrigger>
        <TabsTrigger value="tab3">Third</TabsTrigger>
      </TabsList>
      <TabsContent value="tab1">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Large size tabs content.
        </p>
      </TabsContent>
    </Tabs>
  );
}

With disabled tab

Disable a TabsTrigger so users can see the tab but not open it.

Account content

'use client';

import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from '@/components/nxds/components/tabs/tabs';

export default function TabsWithDisabledTab() {
  return (
    <Tabs defaultValue="account" className="nx:w-[400px]">
      <TabsList>
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
        <TabsTrigger value="settings" disabled>
          Settings
        </TabsTrigger>
      </TabsList>
      <TabsContent value="account">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Account content
        </p>
      </TabsContent>
      <TabsContent value="password">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Password content
        </p>
      </TabsContent>
      <TabsContent value="settings">
        <p className="nx:typography-body-default nx:text-muted-foreground">
          Settings content
        </p>
      </TabsContent>
    </Tabs>
  );
}

Props

Tabs

Tabs adds no props of its own; it accepts the props of the element or primitive it wraps.

TabsContent

TabsContent adds no props of its own; it accepts the props of the element or primitive it wraps.

TabsList

TabsList props
PropTypeDefaultDescription
size"default" | "sm" | "lg""default"

Size of every trigger in the list.

variant"default" | "underline""default"

Visual style of every trigger in the list.

TabsTrigger

TabsTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.

Tabs switch between panels of related content in one place. Use them to split a view into sections that users visit one at a time.

<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs';
</script>

<template>
  <Tabs default-value="account" class="nx:w-[400px]">
    <TabsList>
      <TabsTrigger value="account">Account</TabsTrigger>
      <TabsTrigger value="password">Password</TabsTrigger>
    </TabsList>
    <TabsContent value="account">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Make changes to your account here. Click save when you're done.
      </p>
    </TabsContent>
    <TabsContent value="password">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Change your password here. After saving, you'll be logged out.
      </p>
    </TabsContent>
  </Tabs>
</template>

Installation

  1. Install the following dependencies:

    npm install @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0
    
  2. Copy and paste the following code into your project.

    components/tabs/TabsContent.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      TabsContent,
      type TabsContentProps as RekaTabsContentProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface TabsContentProps extends RekaTabsContentProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<TabsContentProps>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <TabsContent
        v-bind="forwarded"
        data-slot="tabs-content"
        :class="
          cn(
            'nx:mt-2 nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
            props.class
          )
        "
      >
        <slot />
      </TabsContent>
    </template>
    
    components/tabs/TabsList.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      injectTabsRootContext,
      TabsIndicator,
      TabsList,
      type TabsListProps as RekaTabsListProps,
      useForwardProps,
    } from 'reka-ui';
    import { computed, type HTMLAttributes, provide } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { TabsListKey } from '.';
    
    export interface TabsListProps extends RekaTabsListProps {
      /** Visual style of every trigger in the list. */
      variant?: 'default' | 'underline';
      /** Size of every trigger in the list. */
      size?: 'sm' | 'default' | 'lg';
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<TabsListProps>(), {
      variant: 'default',
      size: 'default',
    });
    
    provide(TabsListKey, props);
    
    const delegated = reactiveOmit(props, 'class', 'variant', 'size');
    const forwarded = useForwardProps(delegated);
    
    const { orientation } = injectTabsRootContext();
    const isVertical = computed(() => orientation.value === 'vertical');
    </script>
    
    <template>
      <TabsList
        v-bind="forwarded"
        data-slot="tabs-list"
        :data-variant="variant"
        :data-size="size"
        :class="
          cn(
            'nx:relative nx:isolate nx:inline-flex nx:items-center nx:justify-center',
            'nx:data-[orientation=vertical]:flex-col nx:data-[orientation=vertical]:items-stretch',
            variant === 'default' &&
              'nx:rounded-md nx:bg-control-background nx:p-1',
            props.class
          )
        "
      >
        <TabsIndicator
          aria-hidden="true"
          data-slot="tabs-indicator"
          :data-variant="variant"
          :class="
            cn(
              'nx:pointer-events-none nx:absolute nx:z-0',
              'nx:transition-[translate,width,height] nx:duration-fast nx:ease-move',
              isVertical
                ? 'nx:top-0 nx:h-(--reka-tabs-indicator-size) nx:translate-y-(--reka-tabs-indicator-position)'
                : 'nx:left-0 nx:w-(--reka-tabs-indicator-size) nx:translate-x-(--reka-tabs-indicator-position)',
              variant === 'default' && [
                'nx:rounded-sm nx:border-default nx:border-border-default nx:bg-background',
                isVertical ? 'nx:inset-x-1' : 'nx:inset-y-1',
              ],
              variant === 'underline' && [
                'nx:bg-primary-background',
                isVertical
                  ? 'nx:right-0 nx:w-(--nx-borderwidth-thick)'
                  : 'nx:bottom-0 nx:h-(--nx-borderwidth-thick)',
              ]
            )
          "
        />
        <slot />
      </TabsList>
    </template>
    
    components/tabs/TabsTrigger.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      TabsTrigger,
      type TabsTriggerProps as RekaTabsTriggerProps,
      useForwardProps,
    } from 'reka-ui';
    import { type HTMLAttributes, inject } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { TabsListKey, tabsTriggerVariants } from '.';
    
    export interface TabsTriggerProps extends RekaTabsTriggerProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<TabsTriggerProps>();
    
    const list = inject(TabsListKey, {
      variant: 'default',
      size: 'default',
    } as const);
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <TabsTrigger
        v-bind="forwarded"
        data-slot="tabs-trigger"
        :data-variant="list.variant"
        :data-size="list.size"
        :class="
          cn(
            tabsTriggerVariants({ variant: list.variant, size: list.size }),
            props.class
          )
        "
      >
        <slot />
      </TabsTrigger>
    </template>
    
    components/tabs/index.ts
    import { cva } from 'class-variance-authority';
    import type { InjectionKey } from 'vue';
    
    import type { TabsListProps } from './TabsList.vue';
    
    export {
      default as TabsContent,
      type TabsContentProps,
    } from './TabsContent.vue';
    export { default as TabsList, type TabsListProps } from './TabsList.vue';
    export {
      default as TabsTrigger,
      type TabsTriggerProps,
    } from './TabsTrigger.vue';
    export { TabsRoot as Tabs, type TabsRootProps as TabsProps } from 'reka-ui';
    
    export const TabsListKey: InjectionKey<
      Required<Pick<TabsListProps, 'variant' | 'size'>>
    > = Symbol('tabs-list');
    
    export const tabsTriggerVariants = cva(
      // Base classes
      [
        'nx:relative nx:z-1 nx:inline-flex nx:items-center nx:justify-center',
        'nx:whitespace-nowrap nx:bg-transparent',
        'nx:text-muted-foreground',
        'nx:transition-control',
        'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
        'nx:disabled:pointer-events-none nx:disabled:text-disabled-foreground',
      ],
      {
        variants: {
          /** Visual style. */
          variant: {
            default: [
              'nx:rounded-sm nx:border-default nx:border-transparent',
              'nx:data-[state=inactive]:hover:bg-control-background-hover',
              'nx:data-[state=active]:text-foreground',
            ],
            underline: [
              'nx:rounded-none nx:border-0 nx:border-b-thick nx:border-transparent',
              'nx:data-[orientation=vertical]:border-b-0 nx:data-[orientation=vertical]:border-r-thick',
              'nx:bg-transparent',
              'nx:data-[state=active]:text-foreground',
            ],
          },
          /** Size. */
          size: {
            sm: 'nx:px-2 nx:py-1 nx:typography-label-small',
            default: 'nx:px-3 nx:py-1.5 nx:typography-label-default',
            lg: 'nx:px-4 nx:py-2 nx:typography-label-default',
          },
        },
        defaultVariants: {
          variant: 'default',
          size: 'default',
        },
      }
    );
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs';

Examples

Underline variant

Set variant="underline" on TabsList for a line under the active tab.

<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs';
</script>

<template>
  <Tabs default-value="account" class="nx:w-[400px]">
    <TabsList variant="underline">
      <TabsTrigger value="account">Account</TabsTrigger>
      <TabsTrigger value="password">Password</TabsTrigger>
      <TabsTrigger value="settings">Settings</TabsTrigger>
    </TabsList>
    <TabsContent value="account">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Account settings with underline variant tabs.
      </p>
    </TabsContent>
    <TabsContent value="password">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Password settings with underline variant tabs.
      </p>
    </TabsContent>
    <TabsContent value="settings">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        General settings with underline variant tabs.
      </p>
    </TabsContent>
  </Tabs>
</template>

Small size

Set size="sm" on TabsList for compact tabs.

<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs';
</script>

<template>
  <Tabs default-value="tab1" class="nx:w-[400px]">
    <TabsList size="sm">
      <TabsTrigger value="tab1">Small Tab</TabsTrigger>
      <TabsTrigger value="tab2">Another</TabsTrigger>
      <TabsTrigger value="tab3">Third</TabsTrigger>
    </TabsList>
    <TabsContent value="tab1">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Small size tabs content.
      </p>
    </TabsContent>
  </Tabs>
</template>

Large size

Set size="lg" on TabsList for roomier tabs.

<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs';
</script>

<template>
  <Tabs default-value="tab1" class="nx:w-[500px]">
    <TabsList size="lg">
      <TabsTrigger value="tab1">Large Tab</TabsTrigger>
      <TabsTrigger value="tab2">Another</TabsTrigger>
      <TabsTrigger value="tab3">Third</TabsTrigger>
    </TabsList>
    <TabsContent value="tab1">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Large size tabs content.
      </p>
    </TabsContent>
  </Tabs>
</template>

With disabled tab

Disable a TabsTrigger so users can see the tab but not open it.

<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs';
</script>

<template>
  <Tabs default-value="account" class="nx:w-[400px]">
    <TabsList>
      <TabsTrigger value="account">Account</TabsTrigger>
      <TabsTrigger value="password">Password</TabsTrigger>
      <TabsTrigger value="settings" disabled>Settings</TabsTrigger>
    </TabsList>
    <TabsContent value="account">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Account content
      </p>
    </TabsContent>
    <TabsContent value="password">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Password content
      </p>
    </TabsContent>
    <TabsContent value="settings">
      <p class="nx:typography-body-default nx:text-muted-foreground">
        Settings content
      </p>
    </TabsContent>
  </Tabs>
</template>

Props

TabsContent

TabsContent adds no props of its own; it accepts the props of the element or primitive it wraps.

TabsList

TabsList props
PropTypeDefaultDescription
size"default" | "sm" | "lg""default"

Size of every trigger in the list.

variant"default" | "underline""default"

Visual style of every trigger in the list.

TabsTrigger

TabsTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.