Tabs

A set of layered sections of content—known as tab panels—that are displayed one at a time.

Examples

Basic

PropDefaultTypeDescription
content-stringSet the tabs content.
disabled-booleanSet to disable the tabs.
Preview
Code
We'll never share your email with anyone else.

Variant and Color

PropDefaultTypeDescription
tabs-activesoft-blackstringControls the appearance of active tabs by setting variant and color.
tabs-inactive-stringDefines the variant and color styling for inactive tabs.
_tabsTrigger.tabs-activesoft-blackstringOverrides the active tab styling at the individual trigger level.
_tabsTrigger.tabs-inactive-stringOverrides the inactive tab styling at the individual trigger level.
Preview
Code
We'll never share your email with anyone else.

Disabled

PropDefaultTypeDescription
disabledfalsebooleanSet the tabs disabled.
_tabsTrigger.disabled-booleanSet the tabs disabled via _tabsTrigger.
Preview
Code

Size

PropDefaultTypeDescription
sizesmstringSet the tabs size.
_tabsTrigger.sizesmstringSet the trigger size.
Preview
Code
Tab 1 content
Tab 1 content
Tab 1 content
Tab 2 content

Slots

NamePropsDescription
default-Allows advanced customization using sub-components, replacing the default tabs structure.
listitemsThe container for tab triggers/buttons that users can click to switch between tabs.
triggervalueThe clickable tab button that activates its corresponding content panel.
contentvalueThe content panel that displays when its corresponding trigger is selected.

Custom Rendering

Use the default slot for full control over the tabs's structure. This allows you to compose the tabs using its individual sub-components (like TabsList, TabsTrigger, etc., listed in the Components section), similar to libraries like shadcn/ui.

Preview
Code

Account

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

Presets

shortcuts/tabs.ts
type TabsPrefix = 'tabs'

export const staticTabs: Record<`${TabsPrefix}-${string}` | TabsPrefix, string> = {
  // configurations
  'tabs': 'flex flex-col gap-2',
  'tabs-disabled': 'n-disabled',

  // components
  'tabs-trigger': 'py-0.25em h-[calc(100%-1px)]',
  'tabs-list': 'bg-muted text-muted inline-flex h-2.5714285714285716em w-fit items-center justify-center rounded-lg p-[3px]',
  'tabs-content': 'flex-1 outline-none',
}

export const dynamicTabs = [
  [/^tabs-active-([^-]+)-([^-]+)$/, ([, v = 'solid', c = 'primary']) => `data-[state=active]:btn-${v}-${c}`],
  [/^tabs-inactive-([^-]+)-([^-]+)$/, ([, v = 'solid', c = 'primary']) => `data-[state=inactive]:btn-${v}-${c}`],
]

export const tabs = [
  ...dynamicTabs,
  staticTabs,
]

Props

types/tabs.ts
import type { TabsContentProps, TabsListProps, TabsRootProps, TabsTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'

interface BaseExtensions {
  class?: HTMLAttributes['class']
  size?: HTMLAttributes['class']
}

export interface NTabsProps extends TabsRootProps, BaseExtensions, Pick<NTabsTriggerProps, 'tabsActive' | 'tabsInactive' | 'disabled'> {
  /**
   * The array of items that is passed to tabs.
   *
   * @default []
   */
  items?: any[]

  // sub-components
  _tabsContent?: Partial<NTabsContentProps>
  _tabsTrigger?: Partial<NTabsTriggerProps>
  _tabsList?: Partial<NTabsListProps>

  una?: NTabsUnaProps
}

export interface NTabsListProps extends TabsListProps, BaseExtensions {
  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/tabs.ts
   */
  una?: Pick<NTabsUnaProps, 'tabsList'>
}

export interface NTabsTriggerProps extends TabsTriggerProps, Omit<NButtonProps, 'una'> {
  una?: Pick<NTabsUnaProps, 'tabsTrigger'> & Pick<NButtonProps, 'una'>
}

export interface NTabsContentProps extends TabsContentProps, BaseExtensions {
  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/tabs.ts
   */
  una?: Pick<NTabsUnaProps, 'tabsContent'>
}

export interface NTabsUnaProps {
  tabs?: HTMLAttributes['class']
  tabsList?: HTMLAttributes['class']
  tabsTrigger?: HTMLAttributes['class']
  tabsContent?: HTMLAttributes['class']
}

Components

Tabs.vue
TabsList.vue
TabsTrigger.vue
TabsContent.vue
<script setup lang="ts">
import type { TabsRootEmits } from 'reka-ui'
import type { NTabsProps } from '../../../types/tabs'
import { reactiveOmit } from '@vueuse/core'
import { TabsRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '../../../utils'
import TabsContent from './TabsContent.vue'
import TabsList from './TabsList.vue'
import TabsTrigger from './TabsTrigger.vue'

const props = withDefaults(defineProps<NTabsProps>(), {
  size: 'sm',
})
const emits = defineEmits<TabsRootEmits>()

const delegatedProps = reactiveOmit(props, ['class', 'items', 'tabsActive', 'tabsInactive', 'disabled', 'size', 'una'])

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
  <TabsRoot
    v-bind="forwarded"
    :class="cn(
      'tabs',
      props.una?.tabs,
      props.class,
    )"
  >
    <slot>
      <TabsList
        v-bind="forwarded._tabsList"
        :una
        :size
      >
        <slot name="list" :items="items">
          <template
            v-for="item in items"
            :key="item.value"
          >
            <TabsTrigger
              :tabs-active="item?._tabsTrigger?.tabsActive || item.tabsActive || props.tabsActive"
              :tabs-inactive="item?._tabsTrigger?.tabsInactive || item.tabsInactive || props.tabsInactive"
              :disabled="item?._tabsTrigger?.disabled || item.disabled || props.disabled"
              :value="item.value"
              :size
              v-bind="{ ...forwarded._tabsTrigger, ...item?._tabsTrigger }"
              :una
            >
              <slot name="trigger" :item="item" :disabled="item?._tabsTrigger?.disabled || item.disabled || props.disabled">
                {{ item.name }}
              </slot>
            </TabsTrigger>
          </template>
        </slot>
      </TabsList>
      <template
        v-for="item in items"
        :key="item.value"
      >
        <TabsContent
          v-bind="forwarded._tabsContent"
          :value="item.value"
          :una
        >
          <slot name="content" :item="item">
            <component :is="typeof item.content === 'string' ? 'span' : item.content">
              {{ typeof item.content === 'string' ? item.content : '' }}
            </component>
          </slot>
        </TabsContent>
      </template>
    </slot>
  </TabsRoot>
</template>