UI components

Learn about the 170+ shadcn-vue components included in this boilerplate

This boilerplate includes shadcn-vue, a collection of beautifully designed, accessible, and customizable UI components built on top of Reka UI (formerly Radix Vue).

Overview

The component library provides:

  • 170+ components - Buttons, forms, modals, data tables, and more
  • Fully accessible - WAI-ARIA compliant with keyboard navigation
  • Customizable - Components are in your codebase, not node_modules
  • Type-safe - Full TypeScript support
  • Themeable - Adapts to your design system
  • Auto-imported - No import statements needed

Installation location

All UI components are located in app/components/ui/. They're part of your project, so you can modify them freely.

Adding new components

To add a component that's not yet installed:

pnpm dlx shadcn-vue@latest add [component-name]

For example, to add a calendar component:

pnpm dlx shadcn-vue@latest add calendar
The component will be added to app/components/ui/ and automatically configured.

Commonly used components

Button

Buttons with multiple variants and sizes:

<template>
  <!-- Variants -->
  <Button>Default</Button>
  <Button variant="secondary">Secondary</Button>
  <Button variant="outline">Outline</Button>
  <Button variant="ghost">Ghost</Button>
  <Button variant="destructive">Destructive</Button>

  <!-- Sizes -->
  <Button size="sm">Small</Button>
  <Button size="default">Default</Button>
  <Button size="lg">Large</Button>
  <Button size="icon">
    <Icon name="lucide:plus" />
  </Button>

  <!-- With icon -->
  <Button>
    <Icon name="lucide:download" class="mr-2" />
    Download
  </Button>

  <!-- As a link -->
  <Button as-child>
    <NuxtLink to="/about">About</NuxtLink>
  </Button>
</template>

Card

Container component for content:

<template>
  <Card>
    <CardHeader>
      <CardTitle>Card Title</CardTitle>
      <CardDescription> Card description goes here </CardDescription>
    </CardHeader>
    <CardContent>
      <p>Card content...</p>
    </CardContent>
    <CardFooter>
      <Button>Action</Button>
    </CardFooter>
  </Card>
</template>

Dialog

Modal dialogs:

<script setup>
const isOpen = ref(false)
</script>

<template>
  <Dialog v-model:open="isOpen">
    <DialogTrigger as-child>
      <Button>Open Dialog</Button>
    </DialogTrigger>
    <DialogContent>
      <DialogHeader>
        <DialogTitle>Dialog Title</DialogTitle>
        <DialogDescription> Dialog description </DialogDescription>
      </DialogHeader>
      <div class="py-4">Dialog content goes here</div>
      <DialogFooter>
        <Button @click="isOpen = false">Close</Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>

Form components

Input

<template>
  <div class="space-y-2">
    <Label for="email">Email</Label>
    <Input id="email" type="email" placeholder="Enter your email" v-model="email" />
  </div>
</template>

Textarea

<template>
  <div class="space-y-2">
    <Label for="message">Message</Label>
    <Textarea id="message" placeholder="Type your message here" v-model="message" />
  </div>
</template>

Select

<template>
  <Select v-model="value">
    <SelectTrigger>
      <SelectValue placeholder="Select an option" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="option1">Option 1</SelectItem>
      <SelectItem value="option2">Option 2</SelectItem>
      <SelectItem value="option3">Option 3</SelectItem>
    </SelectContent>
  </Select>
</template>

Checkbox

<template>
  <div class="flex items-center space-x-2">
    <Checkbox id="terms" v-model:checked="accepted" />
    <Label for="terms">Accept terms and conditions</Label>
  </div>
</template>

Switch

<template>
  <div class="flex items-center space-x-2">
    <Switch id="airplane-mode" v-model:checked="enabled" />
    <Label for="airplane-mode">Airplane mode</Label>
  </div>
</template>

Context menus and dropdowns:

<template>
  <DropdownMenu>
    <DropdownMenuTrigger as-child>
      <Button variant="outline">Open Menu</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent>
      <DropdownMenuLabel>My Account</DropdownMenuLabel>
      <DropdownMenuSeparator />
      <DropdownMenuItem @click="handleProfile"> Profile </DropdownMenuItem>
      <DropdownMenuItem @click="handleSettings"> Settings </DropdownMenuItem>
      <DropdownMenuSeparator />
      <DropdownMenuItem @click="handleLogout"> Log out </DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</template>

Sheet

Side panels and drawers:

<script setup>
const isOpen = ref(false)
</script>

<template>
  <Sheet v-model:open="isOpen">
    <SheetTrigger as-child>
      <Button>Open Sheet</Button>
    </SheetTrigger>
    <SheetContent side="right">
      <SheetHeader>
        <SheetTitle>Sheet Title</SheetTitle>
        <SheetDescription> Sheet description </SheetDescription>
      </SheetHeader>
      <div class="py-4">Sheet content goes here</div>
    </SheetContent>
  </Sheet>
</template>

Table

Data tables:

<template>
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead>Name</TableHead>
        <TableHead>Email</TableHead>
        <TableHead>Role</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow v-for="user in users" :key="user.id">
        <TableCell>{{ user.name }}</TableCell>
        <TableCell>{{ user.email }}</TableCell>
        <TableCell>{{ user.role }}</TableCell>
      </TableRow>
    </TableBody>
  </Table>
</template>

Tabs

Tabbed interfaces:

<template>
  <Tabs default-value="account">
    <TabsList>
      <TabsTrigger value="account">Account</TabsTrigger>
      <TabsTrigger value="password">Password</TabsTrigger>
      <TabsTrigger value="notifications">Notifications</TabsTrigger>
    </TabsList>
    <TabsContent value="account"> Account settings content </TabsContent>
    <TabsContent value="password"> Password settings content </TabsContent>
    <TabsContent value="notifications"> Notification settings content </TabsContent>
  </Tabs>
</template>

Toast / Sonner

Toast notifications using vue-sonner:

<script setup>
import { toast } from 'vue-sonner'

const showToast = () => {
  toast.success('Success!', {
    description: 'Your changes have been saved.',
  })
}

const showError = () => {
  toast.error('Error', {
    description: 'Something went wrong.',
  })
}
</script>

<template>
  <Button @click="showToast">Show Success</Button>
  <Button @click="showError">Show Error</Button>
</template>

Toast variants:

// Success
toast.success('Success message')

// Error
toast.error('Error message')

// Info
toast.info('Info message')

// Warning
toast.warning('Warning message')

// Loading
toast.loading('Loading...')

// Custom
toast('Custom message', {
  description: 'Additional info',
  action: {
    label: 'Undo',
    onClick: () => console.log('Undo'),
  },
})

Badge

Small status indicators:

<template>
  <Badge>Default</Badge>
  <Badge variant="secondary">Secondary</Badge>
  <Badge variant="outline">Outline</Badge>
  <Badge variant="destructive">Destructive</Badge>
</template>

Avatar

User avatars with fallback:

<template>
  <Avatar>
    <AvatarImage :src="user.avatar" :alt="user.name" />
    <AvatarFallback>{{ getInitials(user.name) }}</AvatarFallback>
  </Avatar>
</template>

Tooltip

Hover tooltips:

<template>
  <TooltipProvider>
    <Tooltip>
      <TooltipTrigger as-child>
        <Button variant="outline">Hover me</Button>
      </TooltipTrigger>
      <TooltipContent>
        <p>Tooltip content</p>
      </TooltipContent>
    </Tooltip>
  </TooltipProvider>
</template>

Separator

Divider lines:

<template>
  <div>
    <div>Content above</div>
    <Separator class="my-4" />
    <div>Content below</div>
  </div>
</template>

Form validation

The boilerplate uses vee-validate with Zod for form validation:

<script setup lang="ts">
import { z } from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'

const schema = toTypedSchema(
  z.object({
    email: z.string().email('Invalid email address'),
    password: z.string().min(8, 'Password must be at least 8 characters'),
  })
)

const { handleSubmit, errors, defineField } = useForm({
  validationSchema: schema,
})

const [email] = defineField('email')
const [password] = defineField('password')

const onSubmit = handleSubmit(values => {
  console.log('Form submitted:', values)
})
</script>

<template>
  <form @submit="onSubmit" class="space-y-4">
    <div class="space-y-2">
      <Label for="email">Email</Label>
      <Input id="email" v-model="email" type="email" placeholder="Email" />
      <p v-if="errors.email" class="text-sm text-destructive">
        {{ errors.email }}
      </p>
    </div>

    <div class="space-y-2">
      <Label for="password">Password</Label>
      <Input id="password" v-model="password" type="password" placeholder="Password" />
      <p v-if="errors.password" class="text-sm text-destructive">
        {{ errors.password }}
      </p>
    </div>

    <Button type="submit">Submit</Button>
  </form>
</template>

Customizing components

Since components are in your project, you can customize them:

  1. Open the component in app/components/ui/[component-name].vue
  2. Modify the styles by changing Tailwind classes
  3. Add new variants by extending the component's props
  4. Change behavior by modifying the component logic

Example - customizing the Button component:

app/components/ui/Button.vue
<script setup lang="ts">
import { type ButtonHTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'

const buttonVariants = cva('inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors', {
  variants: {
    variant: {
      default: 'bg-primary text-primary-foreground hover:bg-primary/90',
      // Add your custom variant here
      gradient: 'bg-gradient-to-r from-primary to-secondary text-white',
    },
  },
})
</script>

Icons

The boilerplate uses @nuxt/icon with Iconify for icons:

<template>
  <!-- Lucide icons (recommended) -->
  <Icon name="lucide:home" />
  <Icon name="lucide:user" />
  <Icon name="lucide:settings" />

  <!-- Simple icons -->
  <Icon name="simple-icons:github" />
  <Icon name="simple-icons:twitter" />

  <!-- Size and color -->
  <Icon name="lucide:star" class="size-6 text-yellow-500" />
</template>

Browse available icons:

Complete component list

For the full list of available shadcn-vue components, visit:

Best practices

Use semantic components

<!-- Good: Semantic structure -->
<Card>
  <CardHeader>
    <CardTitle>Title</CardTitle>
  </CardHeader>
  <CardContent>Content</CardContent>
</Card>

<!-- Avoid: Generic divs -->
<div class="card">
  <div class="card-header">
    <h2>Title</h2>
  </div>
  <div class="card-content">Content</div>
</div>

Leverage as-child for flexibility

<template>
  <!-- Button as a link -->
  <Button as-child>
    <NuxtLink to="/about">About</NuxtLink>
  </Button>

  <!-- Trigger as custom element -->
  <DialogTrigger as-child>
    <div class="custom-trigger">Click me</div>
  </DialogTrigger>
</template>

Use v-model for form components

<script setup>
const email = ref('')
const checked = ref(false)
const selected = ref('')
</script>

<template>
  <Input v-model="email" />
  <Checkbox v-model:checked="checked" />
  <Select v-model="selected" />
</template>

Reference

For detailed component APIs and examples, always refer to the official shadcn-vue documentation.