Tabs
A set of layered sections of content—known as tab panels—that are displayed one at a time.
Examples
Basic
Prop | Default | Type | Description |
---|---|---|---|
content | - | string | Set the tabs content. |
disabled | - | boolean | Set to disable the tabs. |
Preview
Code
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"
:_tabs-content="{
class: 'py-4',
}"
>
<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'">
<ExampleVueRadioGroupIcon />
</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>
Read more in Reka Tabs Root API.
Variant and Color
Prop | Default | Type | Description |
---|---|---|---|
tabs-active | soft-black | string | Controls the appearance of active tabs by setting variant and color. |
tabs-inactive | - | string | Defines the variant and color styling for inactive tabs. |
_tabsTrigger.tabs-active | soft-black | string | Overrides the active tab styling at the individual trigger level. |
_tabsTrigger.tabs-inactive | - | string | Overrides the inactive tab styling at the individual trigger level. |
Preview
Code
We'll never share your email with anyone else.
<script setup lang="ts">
const items = ref([
{
value: 'account',
name: 'Account',
tabsActive: 'solid-primary',
_tabsTrigger: {
leading: 'i-lucide-user',
},
},
{
value: 'service',
name: 'Service Provider',
_tabsTrigger: {
tabsActive: 'soft-pink',
leading: 'i-lucide-atom',
},
},
{
value: 'reviews',
name: 'Reviews',
_tabsTrigger: {
tabsActive: 'outline-orange',
leading: 'i-lucide-star',
},
},
])
</script>
<template>
<NTabs
:items="items"
default-value="account"
:_tabs-list="{
class: 'grid grid-cols-3 w-full',
}"
:_tabs-content="{
class: 'py-6 w-full',
}"
>
<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'">
<ExampleVueRadioGroupSlot />
</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>
Read more in Button Colors
Disabled
Prop | Default | Type | Description |
---|---|---|---|
disabled | false | boolean | Set the tabs disabled. |
_tabsTrigger.disabled | - | boolean | Set the tabs disabled via _tabsTrigger . |
Preview
Code
or
<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
Prop | Default | Type | Description |
---|---|---|---|
size | sm | string | Set the tabs size. |
_tabsTrigger.size | sm | string | Set the trigger size. |
Preview
Code
Tab 1 content
Tab 1 content
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>
<div class="space-y-8">
<NTabs :items="items" default-value="tab1" size="xs" class="mb-4" />
<NTabs :items="items" default-value="tab1" size="sm" class="mb-4" />
<NTabs :items="items" default-value="tab1" size="md" class="mb-4" />
<NTabs
:items="items" default-value="tab2" size="xl" :_tabs-trigger="{
size: 'xl',
}"
/>
</div>
</template>
Read more in Button Size
Slots
Name | Props | Description |
---|---|---|
default | - | Allows advanced customization using sub-components, replacing the default tabs structure. |
list | items | The container for tab triggers/buttons that users can click to switch between tabs. |
trigger | value | The clickable tab button that activates its corresponding content panel. |
content | value | The 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.
<template>
<div class="flex flex-col gap-6">
<NTabs default-value="account" class="max-w-[400px]">
<NTabsList class="grid grid-cols-2 w-full">
<NTabsTrigger value="account" leading="i-lucide-user">
Account
</NTabsTrigger>
<NTabsTrigger value="password" leading="i-lucide-lock">
Password
</NTabsTrigger>
</NTabsList>
<NTabsContent value="account">
<NCard>
<NCardHeader>
<NCardTitle>Account</NCardTitle>
<NCardDescription>
Make changes to your account here. Click save when you're
done.
</NCardDescription>
</NCardHeader>
<NCardContent class="grid gap-6">
<div class="grid gap-3">
<NInput id="tabs-demo-name" default-value="Pedro Duarte" />
<NInput id="tabs-demo-name" default-value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<NLabel for="tabs-demo-username">
Username
</NLabel>
<NInput id="tabs-demo-username" default-value="@peduarte" />
</div>
</NCardContent>
<NCardFooter>
<NButton>Save changes</NButton>
</NCardFooter>
</NCard>
</NTabsContent>
<NTabsContent value="password">
<NCard>
<NCardHeader>
<NCardTitle>Password</NCardTitle>
<NCardDescription>
Change your password here. After saving, you'll be logged
out.
</NCardDescription>
</NCardHeader>
<NCardContent class="grid gap-6">
<div class="grid gap-3">
<NLabel for="tabs-demo-current">
Current password
</NLabel>
<NInput id="tabs-demo-current" type="password" />
</div>
<div class="grid gap-3">
<NLabel for="tabs-demo-new">
New password
</NLabel>
<NInput id="tabs-demo-new" type="password" />
</div>
</NCardContent>
<NCardFooter>
<NButton>Save password</NButton>
</NCardFooter>
</NCard>
</NTabsContent>
</NTabs>
<NTabs default-value="home">
<NTabsList>
<NTabsTrigger value="home">
Home
</NTabsTrigger>
<NTabsTrigger value="settings">
Settings
</NTabsTrigger>
</NTabsList>
</NTabs>
<NTabs default-value="home">
<NTabsList>
<NTabsTrigger value="home">
Home
</NTabsTrigger>
<NTabsTrigger value="settings" disabled>
Disabled
</NTabsTrigger>
</NTabsList>
</NTabs>
<NTabs default-value="preview">
<NTabsList>
<NTabsTrigger value="preview">
<NIcon name="i-lucide-app-window" />
Preview
</NTabsTrigger>
<NTabsTrigger value="code">
<NIcon name="i-lucide-code" />
Code
</NTabsTrigger>
</NTabsList>
</NTabs>
</div>
</template>
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>
<script setup lang="ts">
import type { NTabsListProps } from '../../../types/tabs'
import { reactiveOmit } from '@vueuse/core'
import { TabsList } from 'reka-ui'
import { cn } from '../../../utils'
const props = withDefaults(defineProps<NTabsListProps>(), {
size: 'sm',
})
const delegatedProps = reactiveOmit(props, ['class', 'size'])
</script>
<template>
<TabsList
data-slot="tabs-list"
:size
v-bind="delegatedProps"
:class="cn(
'tabs-list',
props.una?.tabsList,
props.class,
)"
>
<slot />
</TabsList>
</template>
<script setup lang="ts">
import type { NTabsTriggerProps } from '../../../types/tabs'
import { reactiveOmit } from '@vueuse/core'
import { TabsTrigger } from 'reka-ui'
import { cn } from '../../../utils'
import Button from '../Button.vue'
const props = withDefaults(defineProps<NTabsTriggerProps>(), {
tabsActive: 'soft-black',
size: 'sm',
})
const delegatedProps = reactiveOmit(props, ['class', 'size'])
</script>
<template>
<TabsTrigger
v-bind="delegatedProps"
:size
data-slot="tabs-trigger"
:class="cn(
'tabs-trigger',
props.una?.tabsTrigger,
props.class,
)"
:as="Button"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</TabsTrigger>
</template>
<script setup lang="ts">
import type { NTabsContentProps } from '../../../types/tabs'
import { reactiveOmit } from '@vueuse/core'
import { TabsContent } from 'reka-ui'
import { cn } from '../../../utils'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<NTabsContentProps>()
const delegatedProps = reactiveOmit(props, ['class'])
</script>
<template>
<TabsContent
data-slot="tabs-content"
v-bind="{ ...delegatedProps, ...$attrs }"
:class="cn(
'tabs-content',
props.class,
props.una?.tabsContent,
)"
>
<slot />
</TabsContent>
</template>