Tabs
Vue navigationHorizontal tab navigation with content panels — pick one panel at a time. Underline or pills variants. Built on reka-ui with full keyboard navigation.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/tabs.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/tabs.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/tabs.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/tabs.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/tabs
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | | segmented | optional |
class | HTMLAttributes['class'] | — | optional |
orientation | 'horizontal''vertical' | — | optional |
Dependencies
Files (6)
-
app/components/ui/tabs/Tabs.vue 1.2 kB
<script setup lang="ts"> import { computed, provide } from 'vue' import type { TabsRootEmits, TabsRootProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { TabsRoot, useForwardPropsEmits } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps< TabsRootProps & { class?: HTMLAttributes['class'] orientation?: 'horizontal' | 'vertical' } >() const emits = defineEmits<TabsRootEmits>() const delegatedProps = reactiveOmit(props, 'class', 'orientation') const forwarded = useForwardPropsEmits(delegatedProps, emits) // Make orientation reachable from descendants without each consumer having to // pass it manually. TabsList / TabsTrigger inject this to apply variant CSS. provide( Symbol.for('tabsOrientation'), computed(() => props.orientation ?? 'horizontal'), ) </script> <template> <TabsRoot data-uipkge data-slot="tabs" :data-orientation="orientation ?? 'horizontal'" :orientation="orientation ?? 'horizontal'" v-bind="forwarded" :class="cn('flex w-full', orientation === 'vertical' ? 'flex-row gap-4' : 'flex-col gap-2', props.class)" > <slot /> </TabsRoot> </template> -
app/components/ui/tabs/TabsContent.vue 0.7 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { TabsContent, type TabsContentProps } from 'reka-ui' import { cn } from '@/lib/utils' interface Props { class?: HTMLAttributes['class'] value: string forceMount?: boolean } const props = defineProps<Props>() </script> <template> <TabsContent v-slot="slotProps" :value="props.value" :force-mount="props.forceMount" :class=" cn( 'ring-offset-background focus-visible:border-ring focus-visible:ring-ring/50 flex-1 focus-visible:ring-2 focus-visible:ring-[3px] focus-visible:outline-none', props.class, ) " data-uipkge data-slot="tabs-content" > <slot v-bind="slotProps" /> </TabsContent> </template> -
app/components/ui/tabs/TabsList.vue 1.4 kB
<script setup lang="ts"> import { computed, inject } from 'vue' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { TabsList, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' import { tabsListVariants } from './tabs.variants' // Inlined unions: SFC compiler can't extract runtime props from // indexed-access types or reka-ui's TabsListProps. const props = defineProps<{ class?: HTMLAttributes['class'] variant?: 'segmented' | 'pill' | 'underline' orientation?: 'horizontal' | 'vertical' asChild?: boolean as?: string | object loop?: boolean }>() const delegatedProps = reactiveOmit(props, 'class', 'variant', 'orientation') const forwarded = useForwardProps(delegatedProps) // Inherit orientation from <Tabs> when not set explicitly. Provided as a // computed ref by Tabs.vue so updates propagate. const tabsOrientation = inject<{ value: 'horizontal' | 'vertical' } | 'horizontal' | 'vertical'>( Symbol.for('tabsOrientation'), 'horizontal', ) const effectiveOrientation = computed(() => { if (props.orientation) return props.orientation return typeof tabsOrientation === 'string' ? tabsOrientation : tabsOrientation.value }) </script> <template> <TabsList data-uipkge data-slot="tabs-list" v-bind="forwarded" :class="cn(tabsListVariants({ variant, orientation: effectiveOrientation }), props.class)" > <slot /> </TabsList> </template> -
app/components/ui/tabs/TabsTrigger.vue 1.4 kB
<script setup lang="ts"> import { computed, inject } from 'vue' import type { HTMLAttributes } from 'vue' import { TabsTrigger } from 'reka-ui' import { cn } from '@/lib/utils' import { tabsTriggerVariants } from './tabs.variants' // Inlined unions: SFC compiler can't extract runtime props from // indexed-access types like TabsTriggerVariants['size']. const props = withDefaults( defineProps<{ class?: HTMLAttributes['class'] size?: 'default' | 'sm' | 'lg' variant?: 'segmented' | 'pill' | 'underline' orientation?: 'horizontal' | 'vertical' value: string disabled?: boolean }>(), { disabled: false, }, ) const tabsOrientation = inject<{ value: 'horizontal' | 'vertical' } | 'horizontal' | 'vertical'>( Symbol.for('tabsOrientation'), 'horizontal', ) const effectiveOrientation = computed(() => { if (props.orientation) return props.orientation return typeof tabsOrientation === 'string' ? tabsOrientation : tabsOrientation.value }) </script> <template> <TabsTrigger v-slot="slotProps" :value="props.value" :disabled="props.disabled" data-uipkge data-slot="tabs-trigger" :class=" cn( tabsTriggerVariants({ size: props.size, variant: props.variant, orientation: effectiveOrientation, }), props.class, ) " > <slot v-bind="slotProps" /> </TabsTrigger> </template> -
app/components/ui/tabs/tabs.variants.ts 2.9 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so consuming Vue SFCs can import without creating a circular * dependency through the index. See card.variants.ts for the canonical * example + the SSR symptom that motivated the split. */ export const tabsListVariants = cva('inline-flex items-stretch text-muted-foreground', { variants: { variant: { // Solid muted track with rounded inset triggers — the default look. segmented: 'gap-1 rounded-md bg-muted p-1', // Transparent track with rounded-full pill triggers. pill: 'gap-2 bg-transparent p-0', // Bottom-border bar (horizontal) or right-border bar (vertical), with // an underline on the active trigger. underline: 'gap-0 bg-transparent p-0', }, orientation: { horizontal: 'flex-row', vertical: 'h-auto flex-col items-stretch', }, }, compoundVariants: [ { variant: 'underline', orientation: 'horizontal', class: 'w-full justify-start border-b border-border', }, { variant: 'underline', orientation: 'vertical', class: 'border-r border-border', }, ], defaultVariants: { variant: 'segmented', orientation: 'horizontal', }, }) export const tabsTriggerVariants = cva( 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap font-medium ring-offset-background transition-[color,background-color,box-shadow,border-color] duration-150 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { segmented: 'rounded-sm data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs', pill: 'rounded-full border border-border bg-transparent data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:border-primary', underline: 'rounded-none border-b-2 border-transparent -mb-px data-[state=active]:border-foreground data-[state=active]:text-foreground', }, size: { default: 'h-9 px-3 text-sm', sm: 'h-8 px-2.5 text-xs', lg: 'h-10 px-4 text-sm', }, orientation: { horizontal: '', vertical: 'w-full justify-start', }, }, compoundVariants: [ { variant: 'underline', orientation: 'vertical', class: 'border-b-0 border-r-2 -mr-px', }, ], defaultVariants: { variant: 'segmented', size: 'default', orientation: 'horizontal', }, }, ) export type TabsListVariants = VariantProps<typeof tabsListVariants> export type TabsTriggerVariants = VariantProps<typeof tabsTriggerVariants> -
app/components/ui/tabs/index.ts 0.5 kB
export { default as Tabs } from './Tabs.vue' export { default as TabsContent } from './TabsContent.vue' export { default as TabsList } from './TabsList.vue' export { default as TabsTrigger } from './TabsTrigger.vue' // Re-export variant API from the sibling file (kept separate to avoid the // Component.vue <-> index.ts circular import that broke dev SSR for Card). export { tabsListVariants, tabsTriggerVariants, type TabsListVariants, type TabsTriggerVariants } from './tabs.variants'
Raw manifest: https://uipkge.dev/r/vue/tabs.json