UI components
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
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>
Dropdown Menu
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:
- Open the component in
app/components/ui/[component-name].vue - Modify the styles by changing Tailwind classes
- Add new variants by extending the component's props
- Change behavior by modifying the component logic
Example - customizing the Button component:
<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
- shadcn-vue documentation
- Reka UI documentation
- vee-validate documentation
- Zod documentation
- @nuxt/icon documentation