TabButtons
A radio-group based tab control for switching between compact views.
Status: work in progress. The v1 API and implementation are still being polished, especially route/link behavior, option compatibility, and browser-tab details. Treat this component as preview-quality until this note is removed.
Playground
Variants
<script setup>
import { ref } from 'vue'
import { TabButtons } from 'frappe-ui'
const buttons = [
{ label: 'Home', value: 'home', iconLeft: 'lucide-home' },
{ label: 'Task', value: 'task', iconLeft: 'lucide-list-checks' },
{ label: 'Contact', value: 'contact', iconLeft: 'lucide-contact' },
{ label: 'Others', value: 'others', iconLeft: 'lucide-globe' },
]
const subtle = ref('task')
const ghost = ref('task')
const underline = ref('task')
const browserTab = ref('task')
</script>
<template>
<div class="flex flex-col gap-6 p-2">
<div class="flex items-center gap-6">
<div class="w-24 text-sm text-ink-gray-6">Subtle</div>
<TabButtons v-model="subtle" :options="buttons" type="subtle" />
</div>
<div class="flex items-center gap-6">
<div class="w-24 text-sm text-ink-gray-6">Ghost</div>
<TabButtons v-model="ghost" :options="buttons" type="ghost" />
</div>
<div class="flex items-center gap-6">
<div class="w-24 text-sm text-ink-gray-6">Underline</div>
<TabButtons v-model="underline" :options="buttons" type="underline" />
</div>
<div class="flex items-center gap-6">
<div class="w-24 text-sm text-ink-gray-6">Browser tab</div>
<TabButtons v-model="browserTab" :options="buttons" type="browser-tab" />
</div>
</div>
</template>Sizes
<script setup>
import { ref } from 'vue'
import { TabButtons } from 'frappe-ui'
const current = ref('list')
const currentMd = ref('list')
const buttons = [
{ label: 'List', value: 'list', iconLeft: 'lucide-list' },
{ label: 'Board', value: 'board', iconLeft: 'lucide-columns-3' },
{ label: 'Calendar', value: 'calendar', iconLeft: 'lucide-calendar' },
]
</script>
<template>
<div class="flex flex-col gap-5 p-2">
<div class="flex items-center gap-6">
<div class="w-16 text-sm text-ink-gray-6">Small</div>
<TabButtons v-model="current" :options="buttons" size="sm" />
</div>
<div class="flex items-center gap-6">
<div class="w-16 text-sm text-ink-gray-6">Medium</div>
<TabButtons v-model="currentMd" :options="buttons" size="md" />
</div>
</div>
</template>Vertical
<script setup>
import { ref } from 'vue'
import { TabButtons } from 'frappe-ui'
const subtle = ref('home')
const underline = ref('home')
const browserLeft = ref('home')
const sidebar = ref('home')
const navButtons = [
{ label: 'Home', value: 'home', iconLeft: 'lucide-home' },
{ label: 'Task', value: 'task', iconLeft: 'lucide-list-checks' },
{ label: 'Contact', value: 'contact', iconLeft: 'lucide-contact' },
{ label: 'Others', value: 'others', iconLeft: 'lucide-globe' },
]
const iconButtons = [
{ label: 'Search', value: 'search', icon: 'lucide-search' },
{ label: 'Inbox', value: 'inbox', icon: 'lucide-inbox' },
{ label: 'Home', value: 'home', icon: 'lucide-home' },
{ label: 'Team', value: 'team', icon: 'lucide-users' },
{ label: 'Settings', value: 'settings', icon: 'lucide-settings' },
]
</script>
<template>
<div class="flex items-start gap-10 p-2">
<div class="flex flex-col gap-3">
<div class="text-sm text-ink-gray-6">Subtle</div>
<TabButtons v-model="subtle" :options="navButtons" vertical />
</div>
<div class="flex flex-col gap-3">
<div class="text-sm text-ink-gray-6">Underline</div>
<TabButtons
v-model="underline"
:options="navButtons"
type="underline"
vertical
/>
</div>
<div class="flex flex-col gap-3">
<div class="text-sm text-ink-gray-6">Browser tab</div>
<TabButtons
v-model="browserLeft"
:options="navButtons"
type="browser-tab"
vertical
direction="left"
/>
</div>
<div class="flex flex-col gap-3">
<div class="text-sm text-ink-gray-6">Icon sidebar</div>
<TabButtons
v-model="sidebar"
:options="iconButtons"
type="ghost"
vertical
/>
</div>
</div>
</template>Prefix and suffix
<script setup>
import { ref } from 'vue'
import { TabButtons } from 'frappe-ui'
const inboxTab = ref('comments')
const viewTab = ref('list')
const inboxButtons = [
{ label: 'Inbox', value: 'inbox' },
{ label: 'Comments', value: 'comments' },
]
const inboxIcons = {
inbox: 'lucide-inbox',
comments: 'lucide-message-square',
}
const inboxCounts = {
inbox: 8,
comments: 14,
}
const viewButtons = [
{ label: 'List', value: 'list', icon: 'lucide-list' },
{ label: 'Board', value: 'board', icon: 'lucide-columns-3' },
{ label: 'Calendar', value: 'calendar', icon: 'lucide-calendar' },
]
</script>
<template>
<div class="flex flex-col gap-6 p-2">
<TabButtons v-model="inboxTab" :options="inboxButtons" size="md">
<template #prefix="{ button }">
<span :class="inboxIcons[button.modelValue]" class="size-4 shrink-0" />
</template>
<template #suffix="{ button }">
<span
class="rounded-full bg-surface-gray-2 px-1.5 text-xs text-ink-gray-7"
>
{{ inboxCounts[button.modelValue] }}
</span>
</template>
</TabButtons>
<TabButtons
v-model="inboxTab"
:options="inboxButtons"
type="underline"
size="md"
>
<template #prefix="{ button }">
<span :class="inboxIcons[button.modelValue]" class="size-4 shrink-0" />
</template>
<template #suffix="{ button }">
<span
class="rounded-full bg-surface-gray-2 px-1.5 text-xs text-ink-gray-7"
>
{{ inboxCounts[button.modelValue] }}
</span>
</template>
</TabButtons>
<TabButtons v-model="viewTab" :options="viewButtons" />
</div>
</template>API Reference
Show types
import type { Component } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type { PillSize } from '../Pill'
export type TabButtonValue = string | number | boolean
export type TabButtonIcon = string | Component
export type NativeButtonClass = string | string[] | Record<string, boolean>
export type TabButtonsType = 'subtle' | 'ghost' | 'underline' | 'browser-tab'
export type TabButtonsDirection = 'left' | 'right'
export interface TabButton {
label?: string | number
value?: TabButtonValue
/** Icon-only tab; `label` becomes accessibility text. */
icon?: TabButtonIcon
/** Leading accent icon, rendered next to the visible label. */
iconLeft?: TabButtonIcon
/** Trailing accent icon, rendered next to the visible label. */
iconRight?: TabButtonIcon
active?: boolean
disabled?: boolean
tooltip?: string
class?: NativeButtonClass
/** Renders the tab as a `<RouterLink>` to the given target. */
route?: RouteLocationRaw
/** Renders the tab as an `<a href>`, opens in a new tab. */
href?: string
onClick?: (event: MouseEvent) => void
}
export interface TabButtonsProps {
/** List of options to render. */
options?: TabButton[]
/** @deprecated Use `options` instead. */
buttons?: TabButton[]
modelValue?: TabButtonValue
type?: TabButtonsType
size?: PillSize
vertical?: boolean
/** Edge the active browser tab attaches to. Only used when `type='browser-tab'` and `vertical`. */
direction?: TabButtonsDirection
}
export interface TabButtonsEmits {
'update:modelValue': [value: TabButtonValue | undefined]
}List of options to render.
Deprecated — Use `options` instead.
Edge the active browser tab attaches to. Only used when `type='browser-tab'` and `vertical`.
| Slot | Payload |
|---|---|
prefix | { button: { key: string; label: string | number | undefined; icon: TabButtonIcon | undefined; iconLe |
suffix | { button: { key: string; label: string | number | undefined; icon: TabButtonIcon | undefined; iconLe |
| Event | Payload |
|---|---|
update:modelValue | [value: TabButtonValue | undefined] Fired when the model value changes. |
Fired when the model value changes.
Migration from v0
TabButtons no longer wraps <Button> internally — each tab is now a native <button>, <a href>, or <RouterLink> rendering a <Pill> for its visual treatment. This is a breaking change for consumers that were passing Button props through option entries.
theme,variant,size,loading,prefixon individual options are no longer honored. UseButtonorPilldirectly if you need per-tab theming or a loading spinner.hideLabelon options is gone. Useiconfor an icon-only tab — itslabel, if provided, is automatically exposed as accessibility text. UseiconLeft/iconRightwhen you want an accent icon next to a visible label.routeandhrefon options are honored: a tab renders as a<RouterLink>whenrouteis set, or an<a href target=_blank>whenhrefis set.- The per-tab
tooltipvalue now surfaces as the nativetitleattribute rather than the floating<Tooltip>popover. Wrap theTabButtonsinstance in a custom tooltip if you need styled behavior.