UIPackage

Vertical Tabs

Vue navigation
Edit on GitHub

Settings-page navigation pattern — labels stack on the left, content panel on the right. Same API as Tabs but with a vertical orientation. Use for dense, multi-section settings UIs.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/vertical-tabs.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/vertical-tabs

Examples

Props

Name Type / Values Default Required
class HTMLAttributes['class'] optional

Dependencies

Files (6)

  • app/components/ui/vertical-tabs/VerticalTabs.vue 0.7 kB
    <script setup lang="ts">
    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']
      }
    >()
    const emits = defineEmits<TabsRootEmits>()
    
    const delegated = reactiveOmit(props, 'class', 'orientation')
    const forwarded = useForwardPropsEmits(delegated, emits)
    </script>
    
    <template>
      <TabsRoot
        v-bind="{ 'data-slot': 'vertical-tabs', orientation: 'vertical', ...forwarded }"
        :class="cn('flex w-full gap-6', props.class)"
      >
        <slot />
      </TabsRoot>
    </template>
  • app/components/ui/vertical-tabs/VerticalTabsList.vue 0.7 kB
    <script setup lang="ts">
    import type { TabsListProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { TabsList, useForwardProps } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<
      TabsListProps & {
        class?: HTMLAttributes['class']
      }
    >()
    
    const delegated = reactiveOmit(props, 'class')
    const forwarded = useForwardProps(delegated)
    </script>
    
    <template>
      <TabsList
        data-uipkge
        data-slot="vertical-tabs-list"
        v-bind="forwarded"
        :class="cn('border-border flex w-56 shrink-0 flex-col gap-0.5 border-r pr-3', props.class)"
      >
        <slot />
      </TabsList>
    </template>
  • app/components/ui/vertical-tabs/VerticalTabsSection.vue 0.5 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    defineProps<{
      label: string
      class?: HTMLAttributes['class']
    }>()
    </script>
    
    <template>
      <div
        data-uipkge
        data-slot="vertical-tabs-section"
        :class="
          cn(
            'text-muted-foreground mt-3 mb-1 px-2 text-xs font-medium tracking-wider uppercase first:mt-0',
            $attrs.class as string,
          )
        "
      >
        {{ label }}
      </div>
    </template>
  • app/components/ui/vertical-tabs/VerticalTabsTrigger.vue 1.4 kB
    <script setup lang="ts">
    import { computed } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { TabsTrigger } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    interface Props {
      class?: HTMLAttributes['class']
      value: string
      disabled?: boolean
    }
    
    const props = withDefaults(defineProps<Props>(), {
      disabled: false,
    })
    
    const delegated = computed(() => {
      const { class: _, ...rest } = props
      return rest
    })
    </script>
    
    <template>
      <TabsTrigger
        v-slot="slotProps"
        data-uipkge
        data-slot="vertical-tabs-trigger"
        v-bind="delegated"
        :class="
          cn(
            'group/trigger text-muted-foreground relative flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm font-medium transition-[color,background-color] duration-150',
            'hover:bg-muted/60 hover:text-foreground',
            'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',
            'disabled:pointer-events-none disabled:opacity-50',
            'data-[state=active]:bg-muted data-[state=active]:text-foreground',
            '[&>svg]:size-4 [&>svg]:shrink-0',
            props.class,
          )
        "
        :disabled="props.disabled"
      >
        <span
          aria-hidden
          class="bg-primary absolute inset-y-1 left-0 w-0.5 rounded-full opacity-0 transition-opacity duration-150 group-data-[state=active]/trigger:opacity-100"
        />
        <slot v-bind="slotProps" />
      </TabsTrigger>
    </template>
  • app/components/ui/vertical-tabs/VerticalTabsContent.vue 0.8 kB
    <script setup lang="ts">
    import { computed } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { TabsContent } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    interface Props {
      class?: HTMLAttributes['class']
      value: string
      forceMount?: boolean
    }
    
    const props = defineProps<Props>()
    
    const delegated = computed(() => {
      const { class: _, ...rest } = props
      return rest
    })
    </script>
    
    <template>
      <TabsContent
        v-slot="slotProps"
        data-uipkge
        data-slot="vertical-tabs-content"
        v-bind="delegated"
        :class="
          cn(
            'ring-offset-background focus-visible:ring-ring/50 flex-1 focus-visible:ring-2 focus-visible:outline-none',
            props.class,
          )
        "
        :force-mount="props.forceMount"
      >
        <slot v-bind="slotProps" />
      </TabsContent>
    </template>
  • app/components/ui/vertical-tabs/index.ts 0.3 kB
    export { default as VerticalTabs } from './VerticalTabs.vue'
    export { default as VerticalTabsList } from './VerticalTabsList.vue'
    export { default as VerticalTabsSection } from './VerticalTabsSection.vue'
    export { default as VerticalTabsTrigger } from './VerticalTabsTrigger.vue'
    export { default as VerticalTabsContent } from './VerticalTabsContent.vue'

Raw manifest: https://uipkge.dev/r/vue/vertical-tabs.json