๐ŸŸข Tabs


Basic

NTabs are used to navigate between different content and also they have robust focus management and keyboard navigation support.

We'll never share your email with anyone else.
<script setup lang="ts">
const items = ref([
  {
    value: 'account',
    name: 'Account',
  },
  {
    value: 'service',
    name: 'Service Provider',
  },
  {
    value: 'reviews',
    name: 'Reviews',
  },
])
</script>

<template>
  <NTabs
    :items="items"
    default-value="account"
  >
    <template #content="{ item }">
      <div v-if="item.value === 'account'" class="flex flex-col items-start">
        <NFormGroup
          label="Email"
          required
          description="We'll never share your email with anyone else."
        >
          <NInput
            placeholder="phojrengel@gmail.com"
            leading="i-heroicons-envelope-20-solid"
          />
        </NFormGroup>
        <NButton class="mt-3 self-start px-4">
          Confirm
        </NButton>
      </div>
      <div v-if="item.value === 'service'">
        <ExampleVueRadioFormGroup />
      </div>
      <div v-if="item.value === 'reviews'">
        <NFormGroup label="Write your review">
          <NInput
            type="textarea"
            placeholder="Write your review here..."
            resize="x"
          />
        </NFormGroup>
        <NButton class="mt-2 self-start px-4">
          Save changes
        </NButton>
      </div>
    </template>
  </NTabs>
</template>

Variant and Color

tabs="{variant}-{color}" is used to set the variant of the tabs. The default variant is soft-black.

PropDescription
tabsSet the tabs variant and color.
_tabsTrigger.tabsSet the tabs variant and color via _tabsTrigger.
NTabs is wrapped around the NButton component. This means that all the props and slots of NButton are available through the _tabsTrigger prop.
We'll never share your email with anyone else.
<script setup lang="ts">
const items = ref([
  {
    value: 'account',
    name: 'Account',
    tabs: 'outline-pink',
    _tabsTrigger: {
      leading: 'i-heroicons-user-20-solid',
    },
  },
  {
    value: 'service',
    name: 'Service Provider',
    _tabsTrigger: {
      tabs: 'solid-primary',
      trailing: 'i-heroicons-chevron-right-20-solid',
    },
  },
  {
    value: 'reviews',
    name: 'Reviews',
    _tabsTrigger: {
      tabs: 'outline-success',
      loading: true,
    },
  },
])
</script>

<template>
  <NTabs
    :items="items"
    default-value="account"
  >
    <template #content="{ item }">
      <div v-if="item.value === 'account'" class="flex flex-col items-start">
        <NFormGroup
          label="Email"
          required
          description="We'll never share your email with anyone else."
        >
          <NInput
            placeholder="phojrengel@gmail.com"
            leading="i-heroicons-envelope-20-solid"
          />
        </NFormGroup>
        <NButton class="mt-3 self-start px-4">
          Confirm
        </NButton>
      </div>
      <div v-if="item.value === 'service'">
        <ExampleVueRadioFormGroup />
      </div>
      <div v-if="item.value === 'reviews'">
        <NFormGroup label="Write your review">
          <NInput
            type="textarea"
            placeholder="Write your review here..."
            resize="x"
          />
        </NFormGroup>
        <NButton class="mt-2 self-start px-4">
          Save changes
        </NButton>
      </div>
    </template>
  </NTabs>
</template>

Disabled

disabled="{boolean}" is used to disable the tabs. The default value is false.

PropDescription
disabledSet the tabs disabled.
_tabsTrigger.disabledSet the tabs disabled via _tabsTrigger.
<script setup lang="ts">
const items = ref([
  {
    value: 'tab1',
    name: 'Tab 1',
    disabled: false,
  },
  {
    value: 'tab2',
    name: 'Tab 2',
    _tabsTrigger: {
      disabled: true,
    },
  },
  {
    value: 'tab3',
    name: 'Tab 3',
  },
  {
    value: 'tab4',
    name: 'Tab 4',
    disabled: true,
  },
])
</script>

<template>
  <div>
    <NTabs
      :items="items"
      disabled
      default-value="tab1"
    >
      <template #trigger="{ disabled }">
        Disabled: {{ disabled }}
      </template>
    </NTabs>

    <NSeparator label="or" />

    <NTabs
      :items="items"
      default-value="tab3"
    >
      <template #trigger="{ disabled }">
        Disabled: {{ disabled }}
      </template>
    </NTabs>
  </div>
</template>

Size

PropDescription
sizeSet the tabs size.
_tabsTrigger.sizeSet the trigger size.
_tabsContent.sizeSet the content size.

๐Ÿš€ You can freely adjust the size of the tabs using any size imaginable. No limits exist, and you aan use breakpoints such as sm:sm, xs:lg to change size based on screen size or states such as hover:lg, focus:3xl to change size based on input state and more.

The height and width of the tabs scale depends on the tabs-size. If you want to change the height and width simultaneously, you can always customize it using utility classes.
Tab 1 content
Tab 2 content
<script setup lang="ts">
const items = ref([
  {
    value: 'tab1',
    name: 'Una Tab 1',
    content: 'Tab 1 content',
    disabled: false,
  },
  {
    value: 'tab2',
    name: 'Una Tab 2',
    content: 'Tab 2 content',
    disabled: false,
  },
  {
    value: 'tab3',
    name: 'Una Tab 3',
    content: 'Tab 3 content',
    disabled: false,
  },
])
</script>

<template>
  <NTabs :items="items" default-value="tab1" size="md" class="mb-4" />
  <NTabs
    :items="items" default-value="tab2" size="xl" :_tabs-trigger="{
      size: 'xl',
    }"
  />
</template>

Props

import type { TabsContentProps, TabsListProps, TabsRootProps, TabsTriggerProps } from 'radix-vue'
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, 'tabs' | 'disabled'> {
  /**
   * The array of items that is passed to tabs.
   *
   * @default []
   */
  items: any[]

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

export interface NTabsRootProps extends TabsRootProps, BaseExtensions {
  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/tabs.ts
   */
  una?: {
    // components
    tabsRoot?: HTMLAttributes['class']
  }
}

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?: {
    // components
    tabsList?: HTMLAttributes['class']
  }
}

export interface NTabsTriggerProps extends TabsTriggerProps, Omit<NButtonProps, 'una' | 'size'>, BaseExtensions {
  /**
   * Allows you to add `UnaUI` button preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/tabs.ts
   * @example
   * tabs="solid-green"
   */
  tabs?: string
  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/tabs.ts
   */
  una?: {
    // components
    tabsTrigger?: HTMLAttributes['class']
    tabsDefaultVariant?: HTMLAttributes['class']
  } & 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?: {
    // components
    tabsContent?: HTMLAttributes['class']
  }
}

Presets

type TabsPrefix = 'tabs'

export const staticTabs: Record<`${TabsPrefix}-${string}` | TabsPrefix, string> = {
  // configurations
  'tabs': 'transition-colors duration-200 ease-out',
  'tabs-default-variant': 'tabs-soft-black',
  'tabs-disabled': 'n-disabled',

  // components
  'tabs-root': 'flex flex-col w-full',
  'tabs-trigger': 'w-full focus-visible:z-10',
  'tabs-list': 'flex bg-muted items-center justify-center rounded-md p-1 w-full',
  'tabs-content': 'mt-4 text-base',
}

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

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

Component

<script setup lang="ts">
import type { TabsRootEmits } from 'radix-vue'
import type { NTabsProps } from '../../../types/tabs'
import { useForwardPropsEmits } from 'radix-vue'
import { computed } from 'vue'
import { omitProps } from '../../../utils'
import TabsContent from './TabsContent.vue'
import TabsList from './TabsList.vue'
import TabsRoot from './TabsRoot.vue'
import TabsTrigger from './TabsTrigger.vue'

const props = defineProps<NTabsProps>()
const emits = defineEmits<TabsRootEmits>()

const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props

  return delegated
})

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

<template>
  <TabsRoot
    v-bind="omitProps(forwarded, ['items', 'tabs', 'disabled'])"
    :default-value="defaultValue"
  >
    <TabsList v-bind="forwarded._tabsList">
      <slot name="list" :items="items">
        <template
          v-for="item in items"
          :key="item.value"
        >
          <TabsTrigger
            :tabs="item?._tabsTrigger?.tabs || item.tabs || props.tabs"
            :disabled="item?._tabsTrigger?.disabled ?? item.disabled ?? props.disabled"
            :value="item.value"
            v-bind="{ ...forwarded._tabsTrigger, ...item?._tabsTrigger }"
          >
            <slot name="trigger" :item="item" :disabled="item?._tabsTrigger?.disabled ?? item.disabled ?? props.disabled ?? false">
              {{ item.name }}
            </slot>
          </TabsTrigger>
        </template>
      </slot>
    </TabsList>
    <template
      v-for="item in items"
      :key="item.value"
    >
      <TabsContent v-bind="forwarded._tabsContent" :value="item.value">
        <slot name="content" :item="item">
          <component :is="typeof item.content === 'string' ? 'span' : item.content">
            {{ typeof item.content === 'string' ? item.content : '' }}
          </component>
        </slot>
      </TabsContent>
    </template>
  </TabsRoot>
</template>
<script setup lang="ts">
import type { TabsRootEmits } from 'radix-vue'
import type { NTabsRootProps } from '../../../types/tabs'
import { TabsRoot, useForwardPropsEmits } from 'radix-vue'
import { cn } from '../../../utils'

const props = defineProps<NTabsRootProps>()
const emits = defineEmits<TabsRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>

<template>
  <TabsRoot
    v-bind="forwarded"
    :class="cn(
      'tabs-root',
      props.class,
      props.una?.tabsRoot,
    )"
  >
    <slot />
  </TabsRoot>
</template>
<script setup lang="ts">
import type { NTabsListProps } from '../../../types/tabs'
import { TabsList } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'

const props = defineProps<NTabsListProps>()
const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props
  return delegated
})
</script>

<template>
  <TabsList
    v-bind="delegatedProps"
    :class="cn(
      'tabs-list',
      props.class,
      props.una?.tabsList,
    )"
  >
    <slot />
  </TabsList>
</template>
<script setup lang="ts">
import type { NTabsTriggerProps } from '../../../types/tabs'
import { TabsTrigger } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'
import Button from '../Button.vue'

const props = defineProps<NTabsTriggerProps>()

const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props

  return delegated
})
</script>

<template>
  <TabsTrigger
    v-bind="delegatedProps"
    :class="cn(
      'tabs-trigger',
      props.class,
    )"
    :una="{
      ...props.una,
      btnDefaultVariant: props.tabs ? `tabs-${props.tabs}` : 'tabs-default-variant',
    }"
    :as="Button"
  >
    <slot />
  </TabsTrigger>
</template>
<script setup lang="ts">
import type { NTabsContentProps } from '../../../types/tabs'
import { TabsContent } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '../../../utils'

defineOptions({
  inheritAttrs: false,
})

const props = defineProps<NTabsContentProps>()

const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props
  return delegated
})
</script>

<template>
  <TabsContent
    v-bind="{ ...delegatedProps, ...$attrs }"
    :class="cn(
      'tabs-content',
      props.class,
      props.una?.tabsContent,
    )"
  >
    <slot />
  </TabsContent>
</template>