
Shadcn Vue
- 766 installs
- 10 repo stars
- Updated June 13, 2026
- noartem/laravel-vue-skills
shadcn-vue is a frontend skill that adds production-grade accessible UI components to Nuxt or Vue apps using shadcn-vue, Tailwind CSS, and one-command component installation for developers building design-system-backed i
About
shadcn-vue is a Claude Code skill from noartem/laravel-vue-skills for installing shadcn-vue components into Nuxt or Vue applications with Tailwind CSS and color-mode modules. It configures the shadcn-vue schema with new-york style, slate base color, CSS variables, and aliases for components, utils, ui, lib, and composables paths. The Nuxt setup integrates @nuxtjs/tailwindcss and @nuxtjs/color-mode with explicit tailwind cssPath and configPath entries. Frontend developers use it when they need accessible, copy-in UI primitives instead of hand-rolling buttons, dialogs, and forms in Vue or Nuxt codebases.
- Official shadcn-vue component registry for Nuxt and Vue 3
- Copies fully customizable components directly into your codebase
- Built on Tailwind CSS, Radix Vue, and TypeScript
- New York style preset with CSS variables and dark mode support
- Seamless integration with Nuxt Tailwind and color-mode modules
Shadcn Vue by the numbers
- 766 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #453 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/noartem/laravel-vue-skills --skill shadcn-vueAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 766 |
|---|---|
| repo stars | ★ 10 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 13, 2026 |
| Repository | noartem/laravel-vue-skills ↗ |
How do you add shadcn-vue components to Nuxt apps?
Add production-grade, accessible UI components to Nuxt or Vue apps with one command.
Who is it for?
Vue and Nuxt developers who want accessible, Tailwind-based UI components with shadcn-vue aliases and copy-in primitives.
Skip if: React or Next.js projects that should use shadcn/ui instead of the Vue port, or backends with no frontend surface.
When should I use this skill?
The user asks to add shadcn-vue, set up Nuxt Tailwind UI components, or scaffold accessible Vue component libraries.
What you get
Configured shadcn-vue schema, Tailwind and color-mode setup, component aliases, and installed accessible UI primitives under @/components/ui.
- shadcn-vue components.json
- Nuxt Tailwind config
- Installed UI primitives
Files
shadcn-vue
---
Quick Start (3 Minutes)
For Vue Projects (Vite)
1. Initialize shadcn-vue
npx shadcn-vue@latest initDuring initialization:
- Style:
New YorkorDefault(cannot change later!) - Base color:
Slate(recommended) - CSS variables:
Yes(required for dark mode)
2. Configure TypeScript Path Aliases
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}3. Configure Vite
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite"; // Tailwind v4
import path from "path";
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});4. Add Your First Component
npx shadcn-vue@latest add button---
Quick Reference
| Need | Command or file |
|---|---|
| Initialize project | npx shadcn-vue@latest init |
| Add component | npx shadcn-vue@latest add button |
| Add multiple components | npx shadcn-vue@latest add button card dialog |
| Build registry JSON | npx shadcn-vue@latest build |
| Generate component docs | npx tsx scripts/generate-shadcn-components.ts |
| Enable CSS variables | components.json → tailwind.cssVariables: true |
| Add registry namespace | components.json → registries map |
| Opencode MCP init | npx shadcn-vue@latest mcp init --client opencode |
| Codex MCP config | ~/.codex/config.toml with mcp_servers.shadcn |
---
Bundled Resources
Templates (templates/):
quick-setup.ts- Complete setup guide for Vue/Nuxt with examples (190 lines)
References (references/):
cli.md- CLI commands and optionsmcp.md- MCP setup, client configs, promptstheming.md- Theming andcssVariableserror-catalog.md- All 7 documented issues with solutions (267 lines)component-examples.md- All 50+ component examples with codedark-mode-setup.md- Complete dark mode implementation guidedata-tables.md- Data tables with TanStack Table
Component Documentation (components/):
references/components.md- Index of all shadcn-vue componentscomponents/<component>.md- Individual component documentation with installation, usage, and examples
Official Documentation:
- shadcn-vue Docs: https://shadcn-vue.com
- Reka UI Docs: https://reka-ui.com
- GitHub: https://github.com/radix-vue/shadcn-vue
---
When to Load References
Load these references based on the task:
1. Load `references/error-catalog.md` when:
- User encounters "component not found" or import errors
- Setup commands fail or configuration issues arise
- Tailwind CSS variables or TypeScript paths broken
- Trigger phrases: "not working", "error", "fails to", "broken"
2. Load `references/components.md` when:
- User asks what components are available (names, categories, status)
- User needs to add/use a component and wants the correct install/import paths
- You need to confirm a component exists before recommending a custom build
3. Load `references/component-examples.md` when:
- User asks "how do I implement [component]?"
- Need copy-paste examples for specific components
- Building forms, tables, navigation, or data display
- Trigger phrases: "example", "how to use", "implement", "code sample"
4. Load `references/cli.md` when:
- User asks how to run the CLI (
init,add,update) or what prompts mean - Need the exact command/flags for installing one or more components
- Troubleshooting CLI-related issues (registry, paths, overwrites)
5. Load `references/dark-mode-setup.md` when:
- Implementing dark mode / theme switching
- User mentions Vue 3 + Vite, Nuxt, or Astro setup
- Need composable patterns for theme management
- Trigger phrases: "dark mode", "theme", "light/dark", "color scheme"
6. Load `references/theming.md` when:
- User wants to customize theme tokens via CSS variables (
cssVariables,:root,.dark) - Need to wire Tailwind to CSS-variable-based colors and radii
- Setting up/adjusting design tokens (colors, radius, typography) for shadcn-vue
7. Load `references/mcp.md` when:
- Setting up MCP server for opencode, Codex, Cursor, VS Code
- Configuring registries in
components.json - Troubleshooting missing components or registry namespaces
- Trigger phrases: "MCP", "opencode", "codex", "cursor", "registry"
8. Load `references/data-tables.md` when:
- Building sortable/filterable/paginated tables
- User mentions TanStack Table or
DataTable - Trigger phrases: "data table", "table", "tanstack", "sorting", "pagination"
---
Critical Rules
Always Do
✅ Run `init` before adding components
- Creates required configuration and utilities
- Sets up path aliases
✅ Use CSS variables for theming (cssVariables: true)
- Enables dark mode support
- Flexible theme customization
✅ Configure TypeScript path aliases
- Required for component imports
- Must match
components.jsonaliases
✅ Keep components.json in version control
- Team members need same configuration
- Documents project setup
Never Do
❌ Don't change `style` after initialization
- Requires complete reinstall
- Reinitialize in new directory instead
❌ Don't mix Radix Vue and Reka UI v2
- Incompatible component APIs
- Use one or the other
❌ Don't skip TypeScript configuration
- Component imports will fail
- IDE autocomplete won't work
❌ Don't use without Tailwind CSS
- Components are styled with Tailwind
- Won't render correctly
---
Common Mistakes
- Running
addbeforeinitand missingcomponents.json. - Forgetting to enable the MCP server in the client UI/config.
- Mis-typed registry namespaces (
@namespace/component). - Using CSS variable classes without
tailwind.cssVariables: true.
---
CLI Commands Reference
init Command
# Initialize in current directory
npx shadcn-vue@latest init
# Initialize in specific directory (monorepo)
npx shadcn-vue@latest init -c ./apps/webadd Command
# Add single component
npx shadcn-vue@latest add button
# Add multiple components
npx shadcn-vue@latest add button card dialog
# Add all components
npx shadcn-vue@latest add --alldiff Command
# Check for component updates
npx shadcn-vue@latest diff buttonmcp Command
# Initialize MCP for specific client
npx shadcn-vue@latest mcp init --client opencode
npx shadcn-vue@latest mcp init --client codex
npx shadcn-vue@latest mcp init --client cursor
npx shadcn-vue@latest mcp init --client vscode---
Configuration
shadcn-vue uses components.json to configure:
- Component paths (
@/components/ui) - Utils location (
@/lib/utils) - Tailwind config paths
- TypeScript paths
Full example: See templates/components.json or generate via npx shadcn-vue@latest init
---
Utils Library
The @/lib/utils.ts file provides the cn() helper for merging Tailwind classes:
- Combines multiple className strings
- Uses
clsx+tailwind-mergefor conflict resolution
Auto-generated by shadcn-vue init - no manual setup needed.
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "",
"css": "src/assets/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"rsc": false,
"tsx": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
}
}
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode'
],
tailwindcss: {
cssPath: '~/assets/css/tailwind.css',
configPath: 'tailwind.config.js'
},
colorMode: {
classSuffix: '',
preference: 'system',
fallback: 'light',
storageKey: 'nuxt-color-mode'
},
devtools: { enabled: true },
compatibilityDate: '2025-11-10'
})
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path Aliases - CRITICAL for shadcn-vue */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
tailwindcss()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
Accordion
Description: A vertically stacked set of interactive headings that each reveal a section of content.
---
::component-preview --- name: AccordionDemo class: '[&_.preview>div]:sm:max-w-[80%] **:[.preview]:min-h-[400px]' description: An accordion with three items align: start --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add accordion::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion' </script>
<template> <Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger>Is it accessible?</AccordionTrigger> <AccordionContent> Yes. It adheres to the WAI-ARIA design pattern. </AccordionContent> </AccordionItem> </Accordion> </template>
Alert Dialog
Description: A modal dialog that interrupts the user with important content and expects a response.
---
::component-preview --- name: AlertDialogDemo class: '[&_.preview]:min-h-[400px]' description: An alert dialog that asks for user confirmation --- ::
::vue-school-link{class="mt-6" lesson="dialogs-and-alerts-with-shadcn-vue" placement="top"} Watch a Vue School video about dialogs and alerts in shadcn-vue. ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add alert-dialog::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' </script>
<template> <AlertDialog> <AlertDialogTrigger>Open</AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Continue</AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> </template>
Alert
Description: Displays a callout for user attention.
---
::component-preview --- name: AlertDemo title: An alert with an icon, title and description. description: An alert with an icon, title and description. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add alert::
::tabs-content{value="manual"} ::steps ::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' </script>
<template> <Alert> <AlertTitle>Heads up!</AlertTitle> <AlertDescription> You can add components and dependencies to your app using the cli. </AlertDescription> </Alert> </template>
Aspect Ratio
Description: Displays content within a desired ratio.
---
::component-preview --- name: AspectRatioDemo description: A component that displays an image with a 16:9 aspect ratio. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add aspect-ratio::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script lang="ts"> import { AspectRatio } from '@/components/ui/aspect-ratio' </script>
<template> <AspectRatio :ratio="16 / 9"> <img src="..." alt="Image" class="rounded-md object-cover"> </AspectRatio> </template>
Avatar
Description: An image element with a fallback for representing the user.
---
::component-preview --- name: AvatarDemo class: '[&_.preview]:min-h-[200px]' description: An avatar with a fallback. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add avatar::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' </script>
<template> <Avatar> <AvatarImage src="https://github.com/shadcn.png" /> <AvatarFallback>CN</AvatarFallback> </Avatar> </template>
Badge
Description: Displays a badge or a component that looks like a badge.
---
::component-preview --- name: BadgeDemo description: A default badge --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add badge::
::tabs-content{value="manual"} ::steps ::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Badge } from '@/components/ui/badge' </script>
<template> <Badge variant="default | outline | secondary | destructive"> Badge </Badge> </template>
### Link
You can use the `as-child` prop to make another component look like a badge. Here's an example of a link that looks like a badge.
<script setup lang="ts"> import { Badge } from '@/components/ui/badge' </script>
<template> <Badge as-child> <a href="#">Badge</a> </Badge> </template>
Breadcrumb
Description: Displays the path to the current resource using a hierarchy of links.
---
::component-preview --- name: BreadcrumbDemo class: '[&_.preview]:p-2' description: A breadcrumb with a collapsible dropdown. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add breadcrumb::
::tabs-content{value="manual"} ::steps ::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb' </script>
<template> <Breadcrumb> <BreadcrumbList> <BreadcrumbItem> <BreadcrumbLink href="/"> Home </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> <BreadcrumbItem> <BreadcrumbLink href="/components"> Components </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator /> <BreadcrumbItem> <BreadcrumbPage>Breadcrumb</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </template>
## Examples
### Custom separator
Use a custom component as `children` for `<BreadcrumbSeparator />` to create a custom separator.
::component-preview
---
name: BreadcrumbCustomSeparatorDemo
---
::
<script setup lang="ts"> import { SlashIcon } from '@lucide/vue' // ... </script>
<template> <Breadcrumb> <BreadcrumbList> <BreadcrumbItem> <BreadcrumbLink href="/"> Home </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator> <SlashIcon /> </BreadcrumbSeparator> <BreadcrumbItem> <BreadcrumbLink href="/components"> Components </BreadcrumbLink> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </template>
----
### Dropdown
You can compose `<BreadcrumbItem />` with a `<DropdownMenu />` to create a dropdown in the breadcrumb.
::component-preview
---
name: BreadcrumbDropdownDemo
---
::
<script setup lang="ts"> import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" //... </script>
<template> <BreadcrumbItem> <DropdownMenu> <DropdownMenuTrigger> Components </DropdownMenuTrigger> <DropdownMenuContent align="start"> <DropdownMenuItem>Documentation</DropdownMenuItem> <DropdownMenuItem>Themes</DropdownMenuItem> <DropdownMenuItem>GitHub</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </BreadcrumbItem> </template>
----
### Collapsed
We provide a `<BreadcrumbEllipsis />` component to show a collapsed state when the breadcrumb is too long.
::component-preview
---
name: BreadcrumbCollapsedDemo
---
::
<script setup lang="ts"> import { BreadcrumbEllipsis } from "@/components/ui/breadcrumb" //... </script>
<template> <Breadcrumb> <BreadcrumbList> <!-- ... --> <BreadcrumbItem> <BreadcrumbEllipsis /> </BreadcrumbItem> <!-- ... --> </BreadcrumbList> </Breadcrumb> </template>
----
### Link component
To use a custom link component from your routing library, you can use the `as-child` prop on `<BreadcrumbLink />`.
::component-preview
---
name: BreadcrumbLinkDemo
---
::
<script setup lang="ts"> import { NuxtLink } from '#components' // ... </script>
<template> <Breadcrumb> <BreadcrumbList> <BreadcrumbItem> <BreadcrumbLink as-child> <NuxtLink to="/"> Home </NuxtLink> </BreadcrumbLink> </BreadcrumbItem> {/ ... /} </BreadcrumbList> </Breadcrumb> </template>
----
### Responsive
Here's an example of a responsive breadcrumb that composes `<BreadcrumbItem />` with `<BreadcrumbEllipsis />`, ` <DropdownMenu />`, and `<Drawer />`.
It displays a dropdown on desktop and a drawer on mobile.
::component-preview
---
name: BreadcrumbResponsiveDemo
---
::
Button Group
Description: A container that groups related buttons together with consistent styling.
---
::component-preview --- name: ButtonGroupDemo class: '[&_.preview]:min-h-[400px]' --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add button-group::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
<script setup lang="ts">
import {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
} from '@/components/ui/button-group'
</script>
<template>
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
</template>Accessibility
- The
ButtonGroupcomponent has theroleattribute set togroup. - Use
Tabto navigate between the buttons in the group. - Use
aria-labeloraria-labelledbyto label the button group.
<template>
<ButtonGroup aria-label="Button group">
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
</template>ButtonGroup vs ToggleGroup
- Use the
ButtonGroupcomponent when you want to group buttons that perform an action. - Use the
ToggleGroupcomponent when you want to group buttons that toggle a state.
Examples
Orientation
Set the orientation prop to change the button group layout.
::component-preview --- name: ButtonGroupOrientationDemo --- ::
Size
Control the size of buttons using the size prop on individual buttons.
::component-preview --- name: ButtonGroupSizeDemo --- ::
Nested
<ButtonGroup> components to create button groups with spacing.
::component-preview --- name: ButtonGroupNestedDemo --- ::
Separator
The ButtonGroupSeparator component visually divides buttons within a group.
Buttons with variant outline do not need a separator since they have a border. For other variants, a separator is recommended to improve the visual hierarchy.
::component-preview --- name: ButtonGroupSeparatorDemo --- ::
Split
Create a split button group by adding two buttons separated by a ButtonGroupSeparator.
::component-preview --- name: ButtonGroupSplitDemo --- ::
Input
Wrap an Input component with buttons.
::component-preview --- name: ButtonGroupWithInputDemo --- ::
Input Group
Wrap an InputGroup component to create complex input layouts.
::component-preview --- name: ButtonGroupInputGroupDemo --- ::
Dropdown Menu
Create a split button group with a DropdownMenu component.
::component-preview --- name: ButtonGroupWithDropdownMenuDemo --- ::
Select
Pair with a Select component.
::component-preview --- name: ButtonGroupWithSelectDemo --- ::
Popover
Use with a Popover component.
::component-preview --- name: ButtonGroupWithPopoverDemo --- ::
API Reference
ButtonGroup
The ButtonGroup component is a container that groups related buttons together with consistent styling.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" \ | "vertical" |
<template>
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
</template>Nest multiple button groups to create complex layouts with spacing. See the nested example for more details.
<template>
<ButtonGroup>
<ButtonGroup />
<ButtonGroup />
</ButtonGroup>
</template>ButtonGroupSeparator
The ButtonGroupSeparator component visually divides buttons within a group.
| Prop | Type | Default |
|---|---|---|
orientation | `"horizontal" \ | "vertical"` |
<template>
<ButtonGroup>
<Button>Button 1</Button>
<ButtonGroupSeparator />
<Button>Button 2</Button>
</ButtonGroup>
</template>ButtonGroupText
Use this component to display text within a button group.
| Prop | Type | Default |
|---|---|---|
as-child | boolean | false |
<template>
<ButtonGroup>
<ButtonGroupText>Text</ButtonGroupText>
<Button>Button</Button>
</ButtonGroup>
</template>Use the as-child prop to render a custom component as the text, for example a label.
<script setup lang="ts">
import { ButtonGroupText } from '@/components/ui/button-group'
import { Label } from '@/components/ui/label'
</script>
<template>
<ButtonGroup>
<ButtonGroupText as-child>
<Label for="name">Text</Label>
</ButtonGroupText>
<Input id="name" placeholder="Type something here..." />
</ButtonGroup>
</template>Button
Description: Displays a button or a component that looks like a button.
---
::component-preview --- name: ButtonDemo description: A button --- ::
::vue-school-link{class="mt-6" lesson="buttons-variants-and-the-as-child-prop-with-shadcn-vue" placement="top"} Watch a Vue School video about buttons, variants, and the as-child prop in shadcn-vue. ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add button::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Button } from '@/components/ui/button' </script>
<template> <Button variant="outline"> Button </Button> </template>
## Cursor
Tailwind v4 [switched](https://tailwindcss.com/docs/upgrade-guide#buttons-use-the-default-cursor) from `cursor: pointer` to `cursor: default` for the button component.
If you want to keep the `cursor: pointer` behavior, add the following code to your CSS file:
You can also enable this during project setup with `npx shadcn-vue@latest init --pointer`.
@layer base { button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; } }
## Examples
### Size
::component-preview
---
name: ButtonSize
class: mb-4
---
::
### Default
::component-preview
---
name: ButtonDefault
class: mb-4
---
::
### Outline
::component-preview
---
name: ButtonOutline
class: mb-4
---
::
### Secondary
::component-preview
---
name: ButtonSecondary
class: mb-4
---
::
### Ghost
::component-preview
---
name: ButtonGhost
class: mb-4
---
::
### Destructive
::component-preview
---
name: ButtonDestructive
class: mb-4
---
::
### Link
::component-preview
---
name: ButtonLink
class: mb-4
---
::
### Icon
::component-preview
---
name: ButtonIcon
class: mb-4
---
::
<template> <Button variant="outline" size="icon" aria-label="Submit"> <CircleFadingArrowUpIcon /> </Button> </template>
### With Icon
The spacing between the icon and the text is automatically adjusted based on the size of the button. You do not need any margin on the icon.
::component-preview
---
name: ButtonWithIcon
class: mb-4
---
::
### Rounded
Use the `rounded-full` class to make the button rounded.
::component-preview
---
name: ButtonRounded
class: mb-4
---
::
### Spinner
::component-preview
---
name: ButtonLoading
class: mb-4
---
::
### Button Group
::component-preview
---
name: ButtonGroupDemo
class: mb-4
---
::
To create a button group, use the `ButtonGroup` component. See the [Button Group](/docs/components/button-group) documentation for more details.
### Link (asChild)
You can use the `as-child` prop to make another component look like a button. Here's an example of a link that looks like a button.
<script setup lang="ts"> import { Button } from '@/components/ui/button' </script>
<template> <Button as-child> <a href="/login">Login</a> </Button> </template>
## API Reference
### Button
The `Button` component is a wrapper around the `button` element that adds a variety of styles and functionality.
| Prop | Type | Default |
| --------- | ----------------------------------------------------------------------------- | ----------- |
| `variant` | `"default" \| "outline" \| "ghost" \| "destructive" \| "secondary" \| "link"` | `"default"` |
| `size` | `"default" \| "sm" \| "lg" \| "icon" \| "icon-sm" \| "icon-lg"` | `"default"` |
| `asChild` | `boolean` | `false` |
Calendar
Description: A date field component that allows users to enter and edit date.
---
::component-preview --- name: CalendarDemo description: A calendar component. --- ::
About
The <Calendar /> component is built on top of the Reka UI Calendar component, which uses the @internationalized/date package to handle dates.
If you're looking for a range calendar, check out the Range Calendar component.
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add calendar::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Calendar } from '@/components/ui/calendar' </script>
<template> <Calendar /> </template>
## Calendar Systems (Persian / Hijri / Jalali for example)
[@internationalized/date](https://react-spectrum.adobe.com/internationalized/date/index.html) Supports 13 calendar systems
Here, we'll use the Persian calendar as an example to show how to use calendar systems with the `<Calendar />` component or any other Calendar components.
The default calendar system is `gregory`.<br/>
To use a different calendar system, you need to provide a value with the desired system through the **`defaultPlaceholder`** or **`placeholder`** props.
It's recommended to add either the `placeholder` or `defaultPlaceholder` to the component even if you don't use any other calendar system
<script setup lang="ts"> import type { DateValue } from '@internationalized/date' import { getLocalTimeZone, PersianCalendar, toCalendar, today } from '@internationalized/date' import { Calendar } from '@/registry/new-york-v4/ui/calendar'
const date = ref(today(getLocalTimeZone())) as Ref<DateValue> // no need to add calendar identifier to modelValue when using placeholder
const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar())) as Ref<DateValue> // or const defaultPlaceholder = toCalendar(today(getLocalTimeZone())) </script>
<template> <Calendar v-model="date" v-model:placeholder="placeholder" locale="fa-IR" /> <!-- or --> <Calendar v-model="date" :default-placeholder="placeholder" locale="fa-IR" /> </template>
If none of these props are provided, the emitted dates will use the `gregorian` calendar by default, since it is the most widely used system.
The emitted value from the Calendar component will vary depending on the specified calendar system identifier. <br />
You can also change the locale using the `locale` prop to match the calendar system interface.
::code-collapsible-wrapper
<script setup lang="ts"> import { CalendarDate, fromDate, getLocalTimeZone, parseDate, PersianCalendar, toCalendar, today } from '@internationalized/date' import { ref } from 'vue'
const date = ref(toCalendar(new CalendarDate(2025, 1, 1), new PersianCalendar())) // or const date = ref(toCalendar(parseDate('2022-02-03'), new PersianCalendar())) // or const date = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar())) // or const date = ref(new CalendarDate(new PersianCalendar(), 1404, 1, 1)) // or const date = ref(toCalendar(fromDate(new Date(), getLocalTimeZone()), new PersianCalendar()))
const placeholder = ref(toCalendar(today(getLocalTimeZone()), new PersianCalendar())) </script>
<template> <Calendar v-model="date" v-model:placeholder="placeholder" locale="fa-IR" dir="rtl" /> </template>
::
::component-preview
---
name: CalendarPersianDemo
description: A Persian calendar.
---
::
## Examples
### Calendar Systems
importing `createCalendar` into your project will result in all available calendars being included in your bundle. If you wish to limit the supported calendars to reduce bundle sizes, you can create your own implementation that only imports the desired classes. This way, your bundler can tree-shake the unused calendar implementations.
Check [`@internationalized/date`](https://react-spectrum.adobe.com/internationalized/date/), especially the section on [**Calendar Identifiers**](https://react-spectrum.adobe.com/internationalized/date/Calendar.html#calendar-identifiers).
import { GregorianCalendar, JapaneseCalendar } from '@internationalized/date'
function createCalendar(identifier) { switch (identifier) { case 'gregory': return new GregorianCalendar() case 'japanese': return new JapaneseCalendar() default: throw new Error(Unsupported calendar ${identifier}) } }
::component-preview
---
name: CalendarSystems
title: Calendar systems
description: A calendar with multiple calendar systems
class: '**:[.preview]:h-[560px] mt-8'
---
::
### Month and Year Selector
Make sure to pass either the `placeholder` or `defaultPlaceholder` prop when using this feature.
::component-preview
---
name: CalendarYearAndMonthSelector
title: Month and Year Selector
description: A calendar with month and year dropdowns.
---
::
### Date of Birth Picker
::component-preview
---
name: CalendarDateBirth
title: Date of Birth Picker
description: A calendar with date of birth picker.
---
::
### Date and Time Picker
::component-preview
---
name: CalendarDateAndTimePicker
title: Date and Time Picker
description: A calendar with date and time picker.
---
::
### Natural Language Picker
This component uses the `chrono-node` library to parse natural language dates.
::component-preview
---
name: CalendarNaturalLanguagePicker
title: Natural Language Picker
description: A calendar with natural language picker.
---
::
### Custom Heading and Cell Size
::component-preview
---
name: CalendarCustomCellSize
title: Custom Heading and Cell Size
description: A calendar with custom cell size that's responsive.
class: '**:[.preview]:h-[560px]'
---
::
Card
Description: Displays a card with header, content, and footer.
---
::component-preview --- name: CardDemo description: A card with a form --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add card::
::tabs-content{value="manual"} ::steps ::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card' </script>
<template> <Card> <CardHeader> <CardTitle>Card Title</CardTitle> <CardDescription>Card Description</CardDescription> </CardHeader> <CardContent> <p>Card Content</p> </CardContent> <CardFooter> <p>Card Footer</p> </CardFooter> </Card> </template>
Carousel
Description: A carousel with motion and swipe built using Embla.
---
::component-preview --- name: CarouselDemo title: Carousel description: A carousel component. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add carousel::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install embla-carousel-vue::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, } from '@/components/ui/carousel' </script>
<template> <Carousel> <CarouselContent> <CarouselItem>...</CarouselItem> <CarouselItem>...</CarouselItem> <CarouselItem>...</CarouselItem> </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel> </template>
## Examples
### Sizes
To set the size of the items, you can use the `basis` utility class on the `<CarouselItem />`.
::component-preview
---
name: CarouselSize
title: Carousel
description: A carousel with 3 active items of equal size.
---
::
// 33% of the carousel width. <template> <Carousel> <CarouselContent> <CarouselItem class="basis-1/3"> ... </CarouselItem> <CarouselItem class="basis-1/3"> ... </CarouselItem> <CarouselItem class="basis-1/3"> ... </CarouselItem> </CarouselContent> </Carousel> </template>
// 50% on small screens and 33% on larger screens. <template> <Carousel> <CarouselContent> <CarouselItem class="md:basis-1/2 lg:basis-1/3"> ... </CarouselItem> <CarouselItem class="md:basis-1/2 lg:basis-1/3"> ... </CarouselItem> <CarouselItem class="md:basis-1/2 lg:basis-1/3"> ... </CarouselItem> </CarouselContent> </Carousel> </template>
### Spacing
To set the spacing between the items, we use a `pl-[VALUE]` utility on the `<CarouselItem />` and a negative `-ml-[VALUE]` on the `<CarouselContent />`.
::callout{class="mt-6"}
**Why:** I tried to use the `gap` property or a `grid` layout on the `
<CarouselContent />` but it required a lot of math and mental effort to get the
spacing right. I found `pl-[VALUE]` and `-ml-[VALUE]` utilities much easier to
use.
You can always adjust this in your own project if you need to.
::
::component-preview
---
name: CarouselSpacing
title: Carousel
description: A carousel with 3 items with a spacing of 1rem.
---
::
<template> <Carousel> <CarouselContent class="-ml-4"> <CarouselItem class="pl-4"> ... </CarouselItem> <CarouselItem class="pl-4"> ... </CarouselItem> <CarouselItem class="pl-4"> ... </CarouselItem> </CarouselContent> </Carousel> </template>
<template> <Carousel> <CarouselContent class="-ml-2 md:-ml-4"> <CarouselItem class="pl-2 md:pl-4"> ... </CarouselItem> <CarouselItem class="pl-2 md:pl-4"> ... </CarouselItem> <CarouselItem class="pl-2 md:pl-4"> ... </CarouselItem> </CarouselContent> </Carousel> </template>
### Orientation
Use the `orientation` prop to set the orientation of the carousel.
::component-preview
---
name: CarouselOrientation
title: Carousel
description: A vertical carousel.
---
::
<Carousel orientation="vertical | horizontal"> ... </Carousel>
## Options
You can pass options to the carousel using the `opts` prop. See the [Embla Carousel docs](https://www.embla-carousel.com/api/options/) for more information.
<template> <Carousel :opts="{ align: 'start', loop: true, }" > <CarouselContent> <CarouselItem>...</CarouselItem> <CarouselItem>...</CarouselItem> <CarouselItem>...</CarouselItem> </CarouselContent> </Carousel> </template>
## API
### Method 1
Use the `@init-api` emit method on `<Carousel />` component to set the instance of the API.
::component-preview
---
name: CarouselApi
title: Carousel
description: A carousel with a slide counter.
---
::
### Method 2
You can access it through setting a template ref on the `<Carousel />` component.
<script setup lang="ts"> const carouselContainerRef = ref<InstanceType<typeof Carousel> | null>(null)
function accessApi() { carouselContainerRef.value?.carouselApi.on('select', () => {}) } </script>
<template> <Carousel ref="carouselContainerRef"> ... </Carousel> </template>
## Events
You can listen to events using the API. To get the API instance use the `@init-api` emit method on the `<Carousel />` component
<script setup lang="ts"> import { nextTick, ref, watch } from 'vue' import { useCarousel } from '@/components/ui/carousel'
const api = ref<CarouselApi>()
function setApi(val: CarouselApi) { api.value = val }
const stop = watch(api, (api) => { if (!api) return
// Watch only once or use watchOnce() in @vueuse/core nextTick(() => stop())
api.on('select', () => { // Do something on select. }) }) </script>
<template> <Carousel @init-api="setApi"> ... </Carousel> </template>
See the [Embla Carousel docs](https://www.embla-carousel.com/api/events/) for more information on using events.
## Slot Props
You can get the reactive slot props like `carouselRef, canScrollNext..Prev, scrollNext..Prev` using the `v-slot` directive in the `<Carousel v-slot="slotProps" />` component to extend the functionality.
<template> <Carousel v-slot="{ canScrollNext, canScrollPrev }"> ... <CarouselPrevious v-if="canScrollPrev" /> <CarouselNext v-if="canScrollNext" /> </Carousel> </template>
## Plugins
You can use the `plugins` prop to add plugins to the carousel.
npm install embla-carousel-autoplay
<script setup lang="ts"> import Autoplay from 'embla-carousel-autoplay' </script>
<template> <Carousel class="w-full max-w-xs" :plugins="[Autoplay({ delay: 2000, })]" > ... </Carousel> </template>
::component-preview
---
name: CarouselPlugin
title: Carousel
description: A carousel with the autoplay plugin.
---
::
See the [Embla Carousel docs](https://www.embla-carousel.com/api/plugins/) for more information on using plugins.
Chart
Description: Beautiful charts. Built using Unovis. Copy and paste into your apps.
---
::component-preview --- name: "ChartBarInteractive" class: "theme-blue [&_.preview]:h-auto [&_.preview]:p-0 [&_.preview]:lg:min-h-[404px] [&_.preview>div]:w-full [&_.preview>div]:border-none [&_.preview>div]:shadow-none" hideCode: true --- ::
Introducing Charts. A collection of chart components that you can copy and paste into your apps.
Charts are designed to look great out of the box. They work well with the other components and are fully customizable to fit your project.
Component
We use Unovis under the hood.
We designed the chart component with composition in mind. You build your charts using Unovis components and only bring in custom components, such as `ChartTooltip`, when and where you need it.
```vue showLineNumbers <script setup lang="ts"> import { VisGroupedBar, VisXYContainer } from '@unovis/vue' import { ChartContainer, ChartTooltipContent } from '@/components/ui/chart' </script>
<template> <ChartContainer :config="chartConfig"> <VisXYContainer :data="data"> <VisGroupedBar :x="(d) => d.month" :y="(d) => d.value" /> <ChartTooltip :template="componentToString(chartConfig, ChartTooltipContent)" /> </VisXYContainer> </ChartContainer> </template>
We do not wrap Unovis. This means you're not locked into an abstraction. When a new Unovis version is released, you can follow the official upgrade path to upgrade your charts.
**The components are yours**.
## Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"}
CLI
::
::tabs-trigger{value="manual"}
Manual
::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add chart
::
::tabs-content{value="manual"}
::steps
::step
Install the following dependencies
::
npm install @unovis/ts @unovis/vue
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/chart) into your project
::
::step
Update the import paths to match your project setup
::
::step
Add the following colors to your CSS file
::
@layer base { :root { --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); }
.dark { --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); } }
::
::
::
## Usage
<script setup lang="ts"> import type { ChartConfig } from '@/components/ui/chart' import { VisGroupedBar, VisXYContainer } from '@unovis/vue' import { ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString, } from '@/components/ui/chart'
const chartData = [ { date: new Date("2024-01-01"), desktop: 186, mobile: 80 }, { date: new Date("2024-02-01"), desktop: 305, mobile: 200 }, { date: new Date("2024-03-01"), desktop: 237, mobile: 120 }, ]; type Data = (typeof chartData)[number]
const chartConfig = { desktop: { label: "Desktop", color: "var(--chart-1)", }, mobile: { label: "Mobile", color: "var(--chart-2)", }, } satisfies ChartConfig </script> <template> <ChartContainer :config="chartConfig" class="min-h-[400px] w-full"> <VisXYContainer :data="chartData"> <VisGroupedBar :x="(d: Data) => d.date" :y="[(d: Data) => d.desktop, (d: Data) => d.mobile]" :color="[chartConfig.desktop.color, chartConfig.mobile.color]" /> <ChartTooltip /> <ChartCrosshair :template=" componentToString(chartConfig, ChartTooltipContent, { labelFormatter(d) { return new Date(d).toLocaleDateString('en-US', { month: 'long', }); }, }) " :color="[chartConfig.desktop.color, chartConfig.mobile.color]" /> </VisXYContainer> </ChartContainer> </template>
## Your First Chart
Let's build your first chart. We'll build a bar chart, add a grid, axis, tooltip and legend.
::steps
::step
Start by defining your data
::
The following data represents the number of desktop and mobile users for each month.
const chartData = [ { month: 'January', desktop: 186, mobile: 80 }, { month: 'February', desktop: 305, mobile: 200 }, { month: 'March', desktop: 237, mobile: 120 }, { month: 'April', desktop: 73, mobile: 190 }, { month: 'May', desktop: 209, mobile: 130 }, { month: 'June', desktop: 214, mobile: 140 }, ]
::step
Define your chart config
::
The chart config holds configuration for the chart. This is where you place human-readable strings, such as labels, icons and color tokens for theming.
import type { ChartConfig } from '@/components/ui/chart'
const chartConfig = { desktop: { label: 'Desktop', color: 'var(--chart-1)', }, mobile: { label: 'Mobile', color: 'var(--chart-2)', }, } satisfies ChartConfig
::step
Build your chart
::
You can now build your chart using Unovis components.
::component-source{name="ChartBarDemo" title="components/ExampleChart.vue"}
::
::component-preview
---
name: ChartBarDemo
class: '[&_.preview]:min-h-[250px] [&_.preview]:p-4'
---
::
::
### Add an Axis
To add axes to the chart, we use the `VisAxis` component.
::steps
::step
Import the `VisAxis` component
::
import { VisAxis, VisGroupedBar, VisXYContainer } from '@unovis/vue'
::step
Add the `VisAxis` components to your chart
::
<template> <VisAxis type="x" :x="(d: Data) => d.date" :tick-line="false" :domain-line="false" :grid-line="false" :tick-format="(d: number) => { const date = new Date(d) return date.toLocaleDateString('en-US', { month: 'short', }) }" :tick-values="chartData.map(d => d.date)" /> <VisAxis type="y" :tick-format="(d: number) => ''" :tick-line="false" :domain-line="false" :grid-line="true" /> </template>
::component-preview
---
name: ChartBarDemoAxis
class: '[&_.preview]:min-h-[250px] [&_.preview]:p-4'
---
::
::
### Add Tooltip
To add a tooltip, we'll use the custom `ChartTooltip` and `ChartTooltipContent` components from `chart`.
::steps
::step
Import the `ChartTooltip` and `ChartTooltipContent` components
::
import { ChartTooltip, ChartTooltipContent, componentToString } from '@/components/ui/chart'
::step
Add the components to your chart
::
<ChartTooltip />
<ChartCrosshair :template="componentToString(chartConfig, ChartTooltipContent)" />
::component-preview
---
name: ChartBarDemoTooltip
class: '[&_.preview]:min-h-[250px] [&_.preview]:p-4'
---
::
Hover to see the tooltips. Easy, right? Two components, and we've got a beautiful tooltip.
::
### Add Legend
We'll do the same for the legend. We'll use the `ChartLegend` and `ChartLegendContent` components from `chart`.
::steps
::step
Import the `ChartLegendContent` components.
::
import { ChartLegendContent } from '@/components/ui/chart'
::step
Add the components to your chart.
::
<template> <ChartContainer :config="chartConfig" class="min-h-[200px] w-full"> <VisXYContainer :data="chartData" /> <ChartLegendContent /> </ChartContainer> </template>
::component-preview
---
name: ChartBarDemoLegend
class: '[&_.preview]:min-h-[250px] [&_.preview]:p-4'
---
::
::
Done. You've built your first chart! What's next?
- [Themes and Colors](/docs/components/chart#theming)
- [Tooltip](/docs/components/chart#tooltip)
- [Legend](/docs/components/chart#legend)
## Chart Config
The chart config is where you define the labels, icons and colors for a chart.
It is intentionally decoupled from chart data.
This allows you to share config and color tokens between charts. It can also works independently for cases where your data or color tokens live remotely or in a different format.
<script setup lang="ts"> import type { ChartConfig } from '@/components/ui/chart' import { Monitor } from '@lucide/vue'
const chartConfig = { desktop: { label: 'Desktop', icon: Monitor, // A color like 'hsl(220, 98%, 61%)' or 'var(--color-name)' color: 'var(--chart-1)', // OR a theme object with 'light' and 'dark' keys theme: { light: 'var(--chart-1)', dark: 'var(--chart-2)', }, }, } satisfies ChartConfig </script>
## Theming
Charts has built-in support for theming. You can use css variables (recommended) or color values in any color format, such as hex, hsl or oklch.
### CSS Variables
::steps
::step
Define your colors in your css file
::
@layer base { :root { --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); }
.dark { --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); } }
::step
Add the color to your `chartConfig`
::
const chartConfig = { desktop: { label: 'Desktop', color: 'var(--chart-1)', }, mobile: { label: 'Mobile', color: 'var(--chart-2)', }, } satisfies ChartConfig
::
### hex, hsl or oklch
You can also define your colors directly in the chart config. Use the color format you prefer.
const chartConfig = { desktop: { label: 'Desktop', color: '#2563eb', }, } satisfies ChartConfig
### Using Colors
To use the theme colors in your chart, reference the colors using the format `var(--color-KEY)`.
#### Components
<VisGroupedBar :x="(d) => d.month" :y="(d) => d.desktop" color="var(--color-desktop)" />
#### Chart Data
const chartData = [ { browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' }, { browser: 'safari', visitors: 200, fill: 'var(--color-safari)' }, ]
## Tooltip
A chart tooltip contains a label, name, indicator and value. You can use a combination of these to customize your tooltip.
You can turn on/off any of these using the `hideLabel`, `hideIndicator` props and customize the indicator style using the `indicator` prop.
Use `labelKey` and `nameKey` to use a custom key for the tooltip label and name.
Chart comes with the `ChartTooltip` and `ChartTooltipContent` components. You can use these two components to add custom tooltips to your chart.
import { ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
<template> <ChartTooltip /> <ChartCrosshair :template="componentToString(chartConfig, ChartTooltipContent)" /> </template>
### Props
Use the following props to customize the tooltip.
| Prop | Type | Description |
| :-------------- | :----------------------- | :------------------------------------------- |
| `labelKey` | string | The config or data key to use for the label. |
| `nameKey` | string | The config or data key to use for the name. |
| `indicator` | `dot` `line` or `dashed` | The indicator style for the tooltip. |
| `hideLabel` | boolean | Whether to hide the label. |
| `hideIndicator` | boolean | Whether to hide the indicator. |
### Colors
Colors are automatically referenced from the chart config.
### Custom
To use a custom key for tooltip label and names, use the `labelKey` and `nameKey` props.
const chartData = [ { browser: 'chrome', visitors: 187, fill: 'var(--color-chrome)' }, { browser: 'safari', visitors: 200, fill: 'var(--color-safari)' }, ]
const chartConfig = { visitors: { label: 'Total Visitors', }, chrome: { label: 'Chrome', color: 'var(--chart-1)', }, safari: { label: 'Safari', color: 'var(--chart-2)', }, } satisfies ChartConfig
<template> <ChartCrosshair :template="componentToString(chartConfig, ChartTooltipContent, { labelKey: 'visitors', nameKey: 'browser', })" /> </template>
This will use `Total Visitors` for label and `Chrome` and `Safari` for the tooltip names.
## Legend
You can use the custom `<ChartLegendContent>` components to add a legend to your chart.
import { ChartLegendContent } from '@/components/ui/chart'
<template> <ChartLegendContent /> </template>
### Colors
Colors are automatically referenced from the chart config.
### Custom
To use a custom key for legend names, use the `nameKey` prop.
const chartData = [ { browser: 'chrome', visitors: 187, fill: 'var(--color-chrome)' }, { browser: 'safari', visitors: 200, fill: 'var(--color-safari)' }, ]
const chartConfig = { chrome: { label: 'Chrome', color: 'hsl(var(--chart-1))', }, safari: { label: 'Safari', color: 'hsl(var(--chart-2))', }, } satisfies ChartConfig
<template> <ChartLegendContent name-key="browser" /> </template>
This will use `Chrome` and `Safari` for the legend names.
Checkbox
Description: A control that allows the user to toggle between checked and not checked.
---
::component-preview --- name: CheckboxDemo description: A checkbox --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add checkbox::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Checkbox } from '@/components/ui/checkbox' </script>
<template> <Checkbox /> </template>
Collapsible
Description: An interactive component which expands/collapses a panel.
---
::component-preview --- name: CollapsibleDemo description: A collapsible component. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add collapsible::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' </script>
<template> <Collapsible> <CollapsibleTrigger>Can I use this in my project?</CollapsibleTrigger> <CollapsibleContent> Yes. Free to use for personal and commercial projects. No attribution required. </CollapsibleContent> </Collapsible> </template>
Combobox
Description: Autocomplete input and command palette with a list of suggestions.
---
::component-preview --- name: ComboboxDemo description: A combobox with a list of frameworks. --- ::
Installation
The Combobox is built using a composition of the Popover and the Command components.
See installation instructions for the Popover and the Command components.
Usage
```vue showLineNumbers <script setup lang="ts"> import { CheckIcon, ChevronsUpDownIcon } from '@lucide/vue' import { ref } from 'vue' import { Button } from '@/components/ui/button' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'
const frameworks = [ { value: 'next.js', label: 'Next.js' }, { value: 'sveltekit', label: 'SvelteKit' }, { value: 'nuxt.js', label: 'Nuxt.js' }, { value: 'remix', label: 'Remix' }, { value: 'astro', label: 'Astro' }, ]
const open = ref(false) const value = ref('') </script>
<template> <Popover v-model:open="open"> <PopoverTrigger as-child> <Button variant="outline" role="combobox" :aria-expanded="open" class="w-[200px] justify-between" > {{ value ? frameworks.find(framework => framework.value === value)?.label : 'Select framework...' }} <ChevronsUpDownIcon class="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent class="w-[200px] p-0"> <Command> <CommandInput placeholder="Search framework..." /> <CommandList> <CommandEmpty>No framework found.</CommandEmpty> <CommandGroup> <CommandItem v-for="framework in frameworks" :key="framework.value" :value="framework.value" @select="() => { value = value === framework.value ? '' : framework.value open = false }" > <CheckIcon :class="cn( 'mr-2 h-4 w-4', value === framework.value ? 'opacity-100' : 'opacity-0', )" /> {{ framework.label }} </CommandItem> </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> </template>
Command
Description: Fast, composable, unstyled command menu for Vue.
---
::component-preview --- name: CommandDemo description: A command component. class: "[&_.preview>div]:max-w-[450px]" --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add command::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from '@/components/ui/command' </script>
<template> <Command> <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup heading="Suggestions"> <CommandItem>Calendar</CommandItem> <CommandItem>Search Emoji</CommandItem> <CommandItem>Calculator</CommandItem> </CommandGroup> <CommandSeparator /> <CommandGroup heading="Settings"> <CommandItem>Profile</CommandItem> <CommandItem>Billing</CommandItem> <CommandItem>Settings</CommandItem> </CommandGroup> </CommandList> </Command> </template>
Context Menu
Description: Displays a menu to the user — such as a set of actions or functions — triggered by a right click.
---
::component-preview --- name: ContextMenuDemo description: A context menu component. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add context-menu::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu' </script>
<template> <ContextMenu> <ContextMenuTrigger>Right click</ContextMenuTrigger> <ContextMenuContent> <ContextMenuItem>Profile</ContextMenuItem> <ContextMenuItem>Billing</ContextMenuItem> <ContextMenuItem>Team</ContextMenuItem> <ContextMenuSeparator /> <ContextMenuItem>Subscription</ContextMenuItem> </ContextMenuContent> </ContextMenu> </template>
Data Table
Description: Powerful table and datagrids built using TanStack Table.
---
::component-preview --- name: DataTableDemo description: A data table with sorting, filtering, and pagination. align: start --- ::
::vue-school-link{class="mt-6" lesson="data-tables-and-sonner-in-shadcn-vue" placement="top"} Watch a Vue School video about data tables in shadcn-vue. ::
Introduction
Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that headless UI provides.
So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.
We'll start with the basic <Table /> component and build a complex data table from scratch.
::callout{class="mt-4"}
Tip: If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.
::
Table of Contents
This guide will show you how to use TanStack Table and the <Table /> component to build your own custom data table. We'll cover the following topics:
Installation
1. Add the <Table /> component to your project:
npx shadcn-vue@latest add table2. Add tanstack/vue-table dependency:
npm install @tanstack/vue-tablePrerequisites
We are going to build a table to show recent payments. Here's what our data looks like:
```ts showLineNumbers interface Payment { id: string amount: number status: 'pending' | 'processing' | 'success' | 'failed' email: string }
export const payments: Payment[] = [ { id: '728ed52f', amount: 100, status: 'pending', email: 'm@example.com', }, { id: '489e1d42', amount: 125, status: 'processing', email: 'example@gmail.com', }, // ... ]
## Project Structure
Start by creating the following file structure:
components └── payments ├── columns.ts ├── data-table.vue ├── data-table-dropdown.vue └── app.vue
I'm using a Nuxt example here but this works for any other Vue framework.
- `columns.ts` It will contain our column definitions.
- `data-table.vue` It will contain our `<DataTable />` component.
- `data-table-dropdown.vue` It will contain our `<DropdownAction />` component.
- `app.vue` This is where we'll fetch data and render our table.
## Basic Table
Let's start by building a basic table.
<Steps>
### Column Definitions
First, we'll define our columns in the `columns.ts` file.
import { h } from 'vue'
export const columns: ColumnDef<Payment>[] = [ { accessorKey: 'amount', header: () => h('div', { class: 'text-right' }, 'Amount'), cell: ({ row }) => { const amount = Number.parseFloat(row.getValue('amount')) const formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted) }, } ]
::callout{class="mt-4"}
**Note:** Columns are where you define the core of what your table
will look like. They define the data that will be displayed, how it will be
formatted, sorted and filtered.
::
### `<DataTable />` component
Next, we'll create a `<DataTable />` component to render our table.
<script setup lang="ts" generic="TData, TValue"> import type { ColumnDef } from '@tanstack/vue-table' import { FlexRender, getCoreRowModel, useVueTable, } from '@tanstack/vue-table'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'
const props = defineProps<{ columns: ColumnDef<TData, TValue>[] data: TData[] }>()
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), }) </script>
<template> <div class="border rounded-md"> <Table> <TableHeader> <TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id"> <TableHead v-for="header in headerGroup.headers" :key="header.id"> <FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" /> </TableHead> </TableRow> </TableHeader> <TableBody> <template v-if="table.getRowModel().rows?.length"> <TableRow v-for="row in table.getRowModel().rows" :key="row.id" :data-state="row.getIsSelected() ? 'selected' : undefined" > <TableCell v-for="cell in row.getVisibleCells()" :key="cell.id"> <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> </TableCell> </TableRow> </template> <template v-else> <TableRow> <TableCell :colspan="columns.length" class="h-24 text-center"> No results. </TableCell> </TableRow> </template> </TableBody> </Table> </div> </template>
::callout
**Tip**: If you find yourself using `<DataTable />` in multiple places, this is the component you could make reusable by extracting it to `components/ui/data-table.vue`.
`<DataTable :columns="columns" :data="data" />`
::
### Render the table
Finally, we'll render our table in our index component.
<script setup lang="ts"> import type { Payment } from './components/columns' import { onMounted, ref } from 'vue' import { columns } from './components/columns' import DataTable from './components/DataTable.vue'
const data = ref<Payment[]>([])
async function getData(): Promise<Payment[]> { // Fetch data from your API here. return [ { id: '728ed52f', amount: 100, status: 'pending', email: 'm@example.com', }, // ... ] }
onMounted(async () => { data.value = await getData() }) </script>
<template> <div class="container py-10 mx-auto"> <DataTable :columns="columns" :data="data" /> </div> </template>
</Steps>
## Cell Formatting
Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.
<Steps>
### Update columns definition
Update the `header` and `cell` definitions for amount as follows:
import { h } from 'vue'
export const columns: ColumnDef<Payment>[] = [ { accessorKey: 'amount', header: () => h('div', { class: 'text-right' }, 'Amount'), cell: ({ row }) => { const amount = Number.parseFloat(row.getValue('amount')) const formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted) }, } ]
You can use the same approach to format other cells and headers.
</Steps>
## Row Actions
Let's add row actions to our table. We'll use a `<Dropdown />` component for this.
<Steps>
### Add the following into your `DataTableDropDown.vue` component
<script setup lang="ts"> import { MoreHorizontal } from '@lucide/vue' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
defineProps<{ payment: { id: string } }>()
function copy(id: string) { navigator.clipboard.writeText(id) } </script>
<template> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="ghost" class="w-8 h-8 p-0"> <span class="sr-only">Open menu</span> <MoreHorizontal class="w-4 h-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem @click="copy(payment.id)"> Copy payment ID </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </template>
### Update columns definition
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component.
import { ColumnDef } from '@tanstack/vue-table' import DropdownAction from '@/components/DataTableDropDown.vue'
export const columns: ColumnDef<Payment>[] = [ // ... { id: 'actions', enableHiding: false, cell: ({ row }) => { const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, { payment, })) }, }, ]
You can access the row data using `row.original` in the `cell` function. Use this to handle actions for your row eg. use the `id` to make a DELETE call to your API.
</Steps>
## Pagination
Next, we'll add pagination to our table.
<Steps>
### Update `<DataTable>`
import { FlexRender, getCoreRowModel, getPaginationRowModel, useVueTable, } from "@tanstack/vue-table"
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), })
This will automatically paginate your rows into pages of 10. See the [pagination docs](https://tanstack.com/table/v8/docs/api/features/pagination) for more information on customizing page size and implementing manual pagination.
### Add pagination controls
We can add pagination controls to our table using the `<Button />` component and the `table.previousPage()`, `table.nextPage()` API methods.
<script lang="ts" generic="TData, TValue"> import { Button } from '@/components/ui/button'
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), }) </script>
<template> <div> <div class="border rounded-md"> <Table> { // .... } </Table> </div> <div class="flex items-center justify-end py-4 space-x-2"> <Button variant="outline" size="sm" :disabled="!table.getCanPreviousPage()" @click="table.previousPage()" > Previous </Button> <Button variant="outline" size="sm" :disabled="!table.getCanNextPage()" @click="table.nextPage()" > Next </Button> </div> </div> </template>
See [Reusable Components](#reusable-components) section for a more advanced pagination component.
</Steps>
## Sorting
Let's make the email column sortable.
<Steps>
### Add the following into your `utils` file
import type { Updater } from '@tanstack/vue-table' import type { ClassValue } from 'clsx'
import type { Ref } from 'vue' import { clsx } from 'clsx' import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
### Update `<DataTable>`
<script setup lang="ts" generic="TData, TValue"> import type { ColumnDef, SortingState, } from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from '@lucide/vue' import { h, ref } from 'vue'
import { FlexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useVueTable, } from '@tanstack/vue-table' import { valueUpdater } from '@/components/ui/table/utils'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'
const props = defineProps<{ columns: ColumnDef<TData, TValue>[] data: TData[] }>()
const sorting = ref<SortingState>([])
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), state: { get sorting() { return sorting.value }, }, }) </script>
<template> <div> <div class="border rounded-md"> <Table>{ ... }</Table> </div> </div> </template>
The `valueUpdater` function updates a Vue `ref` object's value. It handles both direct assignments and transformations using a function. If `updaterOrValue` is a function, it's called with the current `ref` value, and the result is assigned to `ref.value`. If it's not a function, it's directly assigned to `ref.value`. This utility enhances flexibility in updating `ref` values. While Vue `ref` can manage reactive state directly, `valueUpdater` simplifies value updates, improving code readability and maintainability when the new state can be a direct value or a function generating it based on the current one.
### Make header cell sortable
We can now update the `email` header cell to add sorting controls.
// components/payments/columns.ts import type { ColumnDef, } from '@tanstack/vue-table' import { ArrowUpDown, ChevronDown } from '@lucide/vue' import { Button } from '@/components/ui/button'
export const columns: ColumnDef<Payment>[] = [ { accessorKey: 'email', header: ({ column }) => { return h(Button, { variant: 'ghost', onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'), }, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]) }, cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')), }, ]
This will automatically sort the table (asc and desc) when the user toggles on the header cell.
</Steps>
## Filtering
Let's add a search input to filter emails in our table.
<Steps>
### Update `<DataTable>`
<script setup lang="ts" generic="TData, TValue"> import type { ColumnDef, ColumnFiltersState, SortingState, } from '@tanstack/vue-table'
import { valueUpdater } from '@/components/ui/table/utils'
import { ArrowUpDown, ChevronDown } from '@lucide/vue' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { h, ref } from 'vue'
import { FlexRender, getCoreRowModel, getPaginationRowModel, getFilteredRowModel, getSortedRowModel, useVueTable, } from "@tanstack/vue-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"
const props = defineProps<{ columns: ColumnDef<TData, TValue>[] data: TData[] }>()
const sorting = ref<SortingState>([]) const columnFilters = ref<ColumnFiltersState>([])
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), getFilteredRowModel: getFilteredRowModel(), state: { get sorting() { return sorting.value }, get columnFilters() { return columnFilters.value }, }, })
</script>
<template> <div> <div class="flex items-center py-4"> <Input class="max-w-sm" placeholder="Filter emails..." :model-value="table.getColumn('email')?.getFilterValue() as string" @update:model-value=" table.getColumn('email')?.setFilterValue($event)" /> </div> <div class="border rounded-md"> <Table>{ ... }</Table> </div> </div> </template>
Filtering is now enabled for the `email` column. You can add filters to other columns as well. See the [filtering docs](https://tanstack.com/table/v8/docs/guide/filters) for more information on customizing filters.
</Steps>
## Visibility
Adding column visibility is fairly simple using `@tanstack/vue-table` visibility API.
<Steps>
### Update `<DataTable>`
<script setup lang="ts" generic="TData, TValue"> import type { ColumnDef, ColumnFiltersState, SortingState, VisibilityState, } from '@tanstack/vue-table'
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import { valueUpdater } from '@/components/ui/table/utils'
import { ArrowUpDown, ChevronDown } from '@lucide/vue' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { h, ref } from 'vue'
import { FlexRender, getCoreRowModel, getPaginationRowModel, getFilteredRowModel, getSortedRowModel, useVueTable, } from "@tanstack/vue-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"
const props = defineProps<{ columns: ColumnDef<TData, TValue>[] data: TData[] }>()
const sorting = ref<SortingState>([]) const columnFilters = ref<ColumnFiltersState>([]) const columnVisibility = ref<VisibilityState>({})
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), state: { get sorting() { return sorting.value }, get columnFilters() { return columnFilters.value }, get columnVisibility() { return columnVisibility.value }, }, })
</script>
<template> <div> <div class="flex items-center py-4"> <Input class="max-w-sm" placeholder="Filter emails..." :model-value="table.getColumn('email')?.getFilterValue() as string" @update:model-value=" table.getColumn('email')?.setFilterValue($event)" /> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="outline" class="ml-auto"> Columns <ChevronDown class="w-4 h-4 ml-2" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuCheckboxItem v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id" class="capitalize" :modelValue="column.getIsVisible()" @update:modelValue="(value) => { column.toggleVisibility(!!value) }"> {{ column.id }} </DropdownMenuCheckboxItem> </DropdownMenuContent> </DropdownMenu> </div> <div class="border rounded-md"> <Table> <TableHeader> <TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id"> <TableHead v-for="header in headerGroup.headers" :key="header.id"> <FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" /> </TableHead> </TableRow> </TableHeader> <TableBody> <template v-if="table.getRowModel().rows?.length"> <TableRow v-for="row in table.getRowModel().rows" :key="row.id" :data-state="row.getIsSelected() ? 'selected' : undefined"> <TableCell v-for="cell in row.getVisibleCells()" :key="cell.id"> <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> </TableCell> </TableRow> </template> <template v-else> <TableRow> <TableCell :colSpan="columns.length" class="h-24 text-center"> No results. </TableCell> </TableRow> </template> </TableBody> </Table> </div> </div> </template>
This adds a dropdown menu that you can use to toggle column visibility.
</Steps>
## Row Selection
Next, we're going to add row selection to our table.
<Steps>
### Update column definitions
import type { ColumnDef } from '@tanstack/vue-table'
import { Checkbox } from '@/components/ui/checkbox'
export const columns: ColumnDef<Payment>[] = [ { id: 'select', header: ({ table }) => h(Checkbox, { 'modelValue': table.getIsAllPageRowsSelected(), 'onUpdate:modelValue': (value: boolean) => table.toggleAllPageRowsSelected(!!value), 'ariaLabel': 'Select all', }), cell: ({ row }) => h(Checkbox, { 'modelValue': row.getIsSelected(), 'onUpdate:modelValue': (value: boolean) => row.toggleSelected(!!value), 'ariaLabel': 'Select row', }), enableSorting: false, enableHiding: false, }, ]
### Update `<DataTable>`
<script setup lang="ts" generic="TData, TValue"> const props = defineProps<{ columns: ColumnDef<TData, TValue>[] data: TData[] }>()
const sorting = ref<SortingState>([]) const columnFilters = ref<ColumnFiltersState>([]) const columnVisibility = ref<VisibilityState>({}) const rowSelection = ref({})
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection), state: { get sorting() { return sorting.value }, get columnFilters() { return columnFilters.value }, get columnVisibility() { return columnVisibility.value }, get rowSelection() { return rowSelection.value }, }, })
</script>
<template> <div> <div class="border rounded-md"> <Table /> </div> </div> </template>
This adds a checkbox to each row and a checkbox in the header to select all rows.
### Show selected rows
You can show the number of selected rows using the `table.getFilteredSelectedRowModel()` API.
<template> <div> <div class="border rounded-md"> <Table /> </div>
<div class="flex items-center justify-end space-x-2 py-4"> <div class="flex-1 text-sm text-muted-foreground"> {{ table.getFilteredSelectedRowModel().rows.length }} of {{ table.getFilteredRowModel().rows.length }} row(s) selected. </div> <div class="space-x-2"> <PaginationButtons /> </div> </div> </div> </template>
</Steps>
<Steps>
## Expanding
Let's make rows expandable.
### Update `<DataTable>`
<script setup lang="ts" generic="TData, TValue"> import type { ColumnDef, ColumnFiltersState, SortingState, VisibilityState, ExpandedState, } from '@tanstack/vue-table'
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import { valueUpdater } from '@/components/ui/table/utils'
import { ArrowUpDown, ChevronDown } from '@lucide/vue' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { h, ref } from 'vue'
import { FlexRender, getCoreRowModel, getPaginationRowModel, getFilteredRowModel, getSortedRowModel, getExpandedRowModel, useVueTable, } from "@tanstack/vue-table"
const props = defineProps<{ columns: ColumnDef<TData, TValue>[] data: TData[] }>()
const sorting = ref<SortingState>([]) const columnFilters = ref<ColumnFiltersState>([]) const columnVisibility = ref<VisibilityState>({}) const rowSelection = ref({}) const expanded = ref<ExpandedState>({})
const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getExpandedRowModel: getExpandedRowModel(), onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting), onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters), onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility), onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection), onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded), state: { get sorting() { return sorting.value }, get columnFilters() { return columnFilters.value }, get columnVisibility() { return columnVisibility.value }, get rowSelection() { return rowSelection.value }, get expanded() { return expanded.value }, }, }) </script>
<template> <div> <div class="flex items-center py-4"> <Input class="max-w-sm" placeholder="Filter emails..." :model-value="table.getColumn('email')?.getFilterValue() as string" @update:model-value=" table.getColumn('email')?.setFilterValue($event)" /> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="outline" class="ml-auto"> Columns <ChevronDown class="w-4 h-4 ml-2" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuCheckboxItem v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id" class="capitalize" :modelValue="column.getIsVisible()" @update:modelValue="(value) => { column.toggleVisibility(!!value) }"> {{ column.id }} </DropdownMenuCheckboxItem> </DropdownMenuContent> </DropdownMenu> </div> <div class="border rounded-md"> <Table> <TableHeader> <TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id"> <TableHead v-for="header in headerGroup.headers" :key="header.id"> <FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" /> </TableHead> </TableRow> </TableHeader> <TableBody> <template v-if="table.getRowModel().rows?.length"> <template v-for="row in table.getRowModel().rows" :key="row.id"> <TableRow :data-state="row.getIsSelected() ? 'selected' : undefined"> <TableCell v-for="cell in row.getVisibleCells()" :key="cell.id"> <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> </TableCell> </TableRow> <TableRow v-if="row.getIsExpanded()"> <TableCell :colspan="row.getAllCells().length"> {{ JSON.stringify(row.original) }} </TableCell> </TableRow> </template> </template> <template v-else> <TableRow> <TableCell :colSpan="columns.length" class="h-24 text-center"> No results. </TableCell> </TableRow> </template> </TableBody> </Table> </div> </div> </template>
### Add the expand action to the `DataTableDropDown.vue` component
<script setup lang="ts"> import { MoreHorizontal } from '@lucide/vue' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { Button } from '@/components/ui/button'
defineProps<{ payment: { id: string } }>()
defineEmits<{ (e: 'expand'): void }>()
function copy(id: string) { navigator.clipboard.writeText(id) } </script>
<template> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="ghost" class="w-8 h-8 p-0"> <span class="sr-only">Open menu</span> <MoreHorizontal class="w-4 h-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem @click="copy(payment.id)"> Copy payment ID </DropdownMenuItem> <DropdownMenuItem @click="$emit('expand')"> Expand </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </template>
### Make rows expandable
Now we can update the action cell to add the expand control.
<script setup lang="ts"> export const columns: ColumnDef<Payment>[] = [ { id: 'actions', enableHiding: false, cell: ({ row }) => { const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, { payment, onExpand: row.toggleExpanded, })) }, }, ] </script>
</Steps>
## Reusable Components
Here are some components you can use to build your data tables. This is from the [Tasks](/examples/tasks) demo.
### Column header
Make any column header sortable and hideable.
<script setup lang="ts"> import type { Column } from '@tanstack/vue-table' import { type Task } from '../data/schema' import ArrowDownIcon from '~icons/radix-icons/arrow-down' import ArrowUpIcon from '~icons/radix-icons/arrow-up' import CaretSortIcon from '~icons/radix-icons/caret-sort' import EyeNoneIcon from '~icons/radix-icons/eye-none'
import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
interface DataTableColumnHeaderProps { column: Column<Task, any> title: string }
defineProps<DataTableColumnHeaderProps>() </script>
<script lang="ts"> export default { inheritAttrs: false, } </script>
<template> <div v-if="column.getCanSort()" :class="cn('flex items-center space-x-2', $attrs.class ?? '')"> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="ghost" size="sm" class="-ml-3 h-8 data-[state=open]:bg-accent" > <span>{{ title }}</span> <ArrowDownIcon v-if="column.getIsSorted() === 'desc'" class="w-4 h-4 ml-2" /> <ArrowUpIcon v-else-if=" column.getIsSorted() === 'asc'" class="w-4 h-4 ml-2" /> <CaretSortIcon v-else class="w-4 h-4 ml-2" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="start"> <DropdownMenuItem @click="column.toggleSorting(false)"> <ArrowUpIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> Asc </DropdownMenuItem> <DropdownMenuItem @click="column.toggleSorting(true)"> <ArrowDownIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> Desc </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem @click="column.toggleVisibility(false)"> <EyeNoneIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> Hide </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div>
<div v-else :class="$attrs.class"> {{ title }} </div> </template>
export const columns = [ { accessorKey: "email", header: ({ column }) => ( h(DataTableColumnHeader, { column: column, title: 'Email' }) ), }, ]
### Pagination
Add pagination controls to your table including page size and selection count.
<script setup lang="ts"> import { type Table } from '@tanstack/vue-table' import { type Task } from '../data/schema' import ChevronLeftIcon from '~icons/radix-icons/chevron-left' import ChevronRightIcon from '~icons/radix-icons/chevron-right' import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left' import DoubleArrowRightIcon from '~icons/radix-icons/double-arrow-right'
import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'
interface DataTablePaginationProps { table: Table<Task> } defineProps<DataTablePaginationProps>() </script>
<template> <div class="flex items-center justify-between px-2"> <div class="flex-1 text-sm text-muted-foreground"> {{ table.getFilteredSelectedRowModel().rows.length }} of {{ table.getFilteredRowModel().rows.length }} row(s) selected. </div> <div class="flex items-center space-x-6 lg:space-x-8"> <div class="flex items-center space-x-2"> <p class="text-sm font-medium"> Rows per page </p> <Select :model-value="${table.getState().pagination.pageSize}" @update:model-value="table.setPageSize" > <SelectTrigger class="h-8 w-[70px]"> <SelectValue :placeholder="${table.getState().pagination.pageSize}" /> </SelectTrigger> <SelectContent side="top"> <SelectItem v-for="pageSize in [10, 20, 30, 40, 50]" :key="pageSize" :value="${pageSize}"> {{ pageSize }} </SelectItem> </SelectContent> </Select> </div> <div class="flex w-[100px] items-center justify-center text-sm font-medium"> Page {{ table.getState().pagination.pageIndex + 1 }} of {{ table.getPageCount() }} </div> <div class="flex items-center space-x-2"> <Button variant="outline" class="hidden w-8 h-8 p-0 lg:flex" :disabled="!table.getCanPreviousPage()" @click="table.setPageIndex(0)" > <span class="sr-only">Go to first page</span> <DoubleArrowLeftIcon class="w-4 h-4" /> </Button> <Button variant="outline" class="w-8 h-8 p-0" :disabled="!table.getCanPreviousPage()" @click="table.previousPage()" > <span class="sr-only">Go to previous page</span> <ChevronLeftIcon class="w-4 h-4" /> </Button> <Button variant="outline" class="w-8 h-8 p-0" :disabled="!table.getCanNextPage()" @click="table.nextPage()" > <span class="sr-only">Go to next page</span> <ChevronRightIcon class="w-4 h-4" /> </Button> <Button variant="outline" class="hidden w-8 h-8 p-0 lg:flex" :disabled="!table.getCanNextPage()" @click="table.setPageIndex(table.getPageCount() - 1)" > <span class="sr-only">Go to last page</span> <DoubleArrowRightIcon class="w-4 h-4" /> </Button> </div> </div> </div> </template>
<DataTablePagination :table="table" />
### Column toggle
A component to toggle column visibility.
<script setup lang="ts"> import type { Table } from '@tanstack/vue-table' import { computed } from 'vue' import { type Task } from '../data/schema' import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
interface DataTableViewOptionsProps { table: Table<Task> }
const props = defineProps<DataTableViewOptionsProps>()
const columns = computed(() => props.table.getAllColumns() .filter( column => typeof column.accessorFn !== 'undefined' && column.getCanHide(), )) </script>
<template> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="outline" size="sm" class="hidden h-8 ml-auto lg:flex" > <MixerHorizontalIcon class="w-4 h-4 mr-2" /> View </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" class="w-[150px]"> <DropdownMenuLabel>Toggle columns</DropdownMenuLabel> <DropdownMenuSeparator />
<DropdownMenuCheckboxItem v-for="column in columns" :key="column.id" class="capitalize" :modelValue="column.getIsVisible()" @update:modelValue="(value) => column.toggleVisibility(!!value)" > {{ column.id }} </DropdownMenuCheckboxItem> </DropdownMenuContent> </DropdownMenu> </template>
Date Picker
Description: A date picker component with range and presets.
---
::component-preview --- name: DatePickerDemo description: A date picker component. --- ::
Installation
The Date Picker is built using a composition of the <Popover /> and the <Calendar /> components.
See installation instructions for the Popover and the Calendar components.
Usage
```vue showLineNumbers <script setup lang="ts"> import { DateFormatter, getLocalTimeZone, today } from '@internationalized/date' import { CalendarIcon } from '@lucide/vue' import { ref } from 'vue' import { Button } from '@/components/ui/button' import { Calendar } from '@/components/ui/calendar' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' import { cn } from '@/lib/utils'
const date = ref<Date>() const defaultPlaceholder = today(getLocalTimeZone()) </script>
<template> <Popover> <PopoverTrigger as-child> <Button variant="outline" :class="cn( 'w-[280px] justify-start text-left font-normal', !date && 'text-muted-foreground', )" > <CalendarIcon class="mr-2 h-4 w-4" /> {{ date ? date.toDateString() : "Pick a date" }} </Button> </PopoverTrigger> <PopoverContent class="w-auto p-0"> <Calendar v-model="date" :initial-focus="true" :default-placeholder="defaultPlaceholder" layout="month-and-year" /> </PopoverContent> </Popover> </template>
Dialog
Description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
---
::component-preview --- name: DialogDemo description: A dialog with a form. --- ::
::vue-school-link{class="mt-6" lesson="dialogs-and-alerts-with-shadcn-vue" placement="top"} Watch a Vue School video about dialogs and alerts in shadcn-vue. ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add dialog::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' </script>
<template> <Dialog> <DialogTrigger>Open</DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Are you absolutely sure?</DialogTitle> <DialogDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </DialogDescription> </DialogHeader> </DialogContent> </Dialog> </template>
## Example
### Custom close button
::component-preview
---
name: DialogCloseButton
---
::
### Dialog with form
To keep field value after `Dialog` unmounts
::component-preview
---
name: DialogForm
---
::
### Responsive Modal (Dialog & Drawer)
Use a `Drawer` component for smaller viewport sizes and a `Dialog` component otherwise. This can be further made reusable by using slots for various parts of the modal.
::component-preview
---
name: DialogResponsive
---
::
## Notes
To use the `Dialog` component from within a `Context Menu` or `Dropdown Menu`, you must encase the `Context Menu` or
`Dropdown Menu` component in the `Dialog` component.
<template> <Dialog> <ContextMenu> <ContextMenuTrigger>Right click</ContextMenuTrigger> <ContextMenuContent> <ContextMenuItem>Open</ContextMenuItem> <ContextMenuItem>Download</ContextMenuItem> <DialogTrigger as-child> <ContextMenuItem> <span>Delete</span> </ContextMenuItem> </DialogTrigger> </ContextMenuContent> </ContextMenu> <DialogContent> <DialogHeader> <DialogTitle>Are you absolutely sure?</DialogTitle> <DialogDescription> This action cannot be undone. Are you sure you want to permanently delete this file from our servers? </DialogDescription> </DialogHeader> <DialogFooter> <Button type="submit"> Confirm </Button> </DialogFooter> </DialogContent> </Dialog> </template>
Drawer
Description: A drawer component for mobile devices.
---
::component-preview --- name: DrawerDemo description: A drawer component. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add drawer::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from '@/components/ui/drawer' </script>
<template> <Drawer> <DrawerTrigger>Open</DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Are you absolutely sure?</DrawerTitle> <DrawerDescription> This action cannot be undone. </DrawerDescription> </DrawerHeader> <DrawerFooter> <Button>Submit</Button> <DrawerClose> <Button variant="outline"> Cancel </Button> </DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> </template>
## Example
### Responsive Modal (Dialog & Drawer)
Use a `Drawer` component for smaller viewport sizes and a `Dialog` component otherwise. This can be further made reusable by using slots for various parts of the modal.
::component-preview
---
name: DialogResponsive
---
::
Dropdown Menu
Description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
---
::component-preview --- name: DropdownMenuDemo description: A dropdown menu with actions. --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add dropdown-menu::
::tabs-content{value="manual"} ::steps ::step Install the following dependencies: ::
npm install reka-ui::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
::
Usage
```vue showLineNumbers <script setup lang="ts"> import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' </script>
<template> <DropdownMenu> <DropdownMenuTrigger>Open</DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuLabel>My Account</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem>Profile</DropdownMenuItem> <DropdownMenuItem>Billing</DropdownMenuItem> <DropdownMenuItem>Team</DropdownMenuItem> <DropdownMenuItem>Subscription</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </template>
Empty
Description: Use the Empty component to display an empty state.
---
::component-preview --- name: EmptyDemo class: '[&_.preview]:p-0' --- ::
Installation
::code-tabs
::tabs-list
::tabs-trigger{value="cli"} CLI ::
::tabs-trigger{value="manual"} Manual ::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add empty::
::tabs-content{value="manual"} ::steps ::step Copy and paste the GitHub source code into your project. ::
::step Update the import paths to match your project setup. :: :: ::
Usage
```vue showLineNumbers <script setup lang="ts"> import { FolderOpen } from '@lucide/vue' import { Button } from '@/registry/default/ui/button' import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/registry/default/ui/empty' </script>
<template> <Empty> <EmptyHeader> <EmptyMedia variant="icon"> <FolderOpen /> </EmptyMedia> </EmptyHeader> <EmptyTitle>No data</EmptyTitle> <EmptyDescription>No data found</EmptyDescription> <EmptyContent> <Button>Add data</Button> </EmptyContent> </Empty> </template>
## Examples
### Outline
Use the `border` utility class to create an outline empty state.
::component-preview
---
name: EmptyOutlineDemo
class: '[&_.preview]:p-6 md:[&_.preview]:p-10'
---
::
### Background
Use the `bg-*` and `bg-gradient-*` utilities to add a background to the empty state.
::component-preview
---
name: EmptyBackgroundDemo
class: '[&_.preview]:p-0'
---
::
### Avatar
Use the `EmptyMedia` component to display an avatar in the empty state.
::component-preview
---
name: EmptyAvatarDemo
class: '[&_.preview]:p-0'
---
::
### Avatar Group
Use the `EmptyMedia` component to display an avatar group in the empty state.
::component-preview
---
name: EmptyAvatarGroupDemo
class: '[&_.preview]:p-0'
---
::
### InputGroup
You can add an `InputGroup` component to the `EmptyContent` component.
::component-preview
---
name: EmptyInputGroupDemo
class: '[&_.preview]:p-0'
---
::
## API Reference
### Empty
The main component of the empty state. Wraps the `EmptyHeader` and `EmptyContent` components.
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `class` | `string` | |
<template> <Empty> <EmptyHeader /> <EmptyContent /> </Empty> </template>
### EmptyHeader
The `EmptyHeader` component wraps the empty media, title, and description.
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `class` | `string` | |
<template> <EmptyHeader> <EmptyMedia /> <EmptyTitle /> <EmptyDescription /> </EmptyHeader> </template>
### EmptyMedia
Use the `EmptyMedia` component to display the media of the empty state such as an icon or an image. You can also use it to display other components such as an avatar.
| Prop | Type | Default |
| ----------- | --------------------- | --------- |
| `variant` | `"default" \| "icon"` | `default` |
| `class` | `string` | |
<template> <EmptyMedia variant="icon"> <Icon /> </EmptyMedia> </template>
<template> <EmptyMedia> <Avatar> <AvatarImage src="..." /> <AvatarFallback>CN</AvatarFallback> </Avatar> </EmptyMedia> </template>
### EmptyTitle
Use the `EmptyTitle` component to display the title of the empty state.
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `class` | `string` | |
<template> <EmptyTitle>No data</EmptyTitle> </template>
### EmptyDescription
Use the `EmptyDescription` component to display the description of the empty state.
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `class` | `string` | |
<template> <EmptyDescription>You do not have any notifications.</EmptyDescription> </template>
### EmptyContent
Use the `EmptyContent` component to display the content of the empty state such as a button, input or a link.
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `class` | `string` | |
<template> <EmptyContent> <Button>Add Project</Button> </EmptyContent> </template>
Form
Description: Building forms with VeeValidate and Zod.
---
::callout{title="We are not actively developing this component anymore."}
The Form component is an abstraction over the vee-validate library. Going forward, we recommend using the `<Field />` component to build forms. See the Form documentation for more information.
::
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
Well-designed HTML forms are:
- Well-structured and semantically correct.
- Easy to use and navigate (keyboard).
- Accessible with ARIA attributes and proper labels.
- Has support for client and server side validation.
- Well-styled and consistent with the rest of the application.
In this guide, we will take a look at building forms with `vee-validate` and `zod`. We're going to use a <FormField> component to compose accessible forms using Reka UI components.
Features
The <Form /> component is a wrapper around the vee-validate library. It provides a few things:
- Composable components for building forms.
- A
<FormField />component for building controlled form fields. - Form validation using
zod. - Applies the correct
ariaattributes to form fields based on states, handle unique IDs - Built to work with all Reka UI components.
- Bring your own schema library. We use
zodbut you can use any other supported schema validation you want, like `yup` or `valibot`. - You have full control over the markup and styling.
`vee-validate` makes use of two flavors to add validation to your forms.
- Composition API
- Higher-order components (HOC)
Anatomy
<template>
<Form>
<FormField>
<FormItem>
<FormLabel />
<FormControl>
<!-- any Form Input component or native input elements -->
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
</FormField>
</Form>
</template>Example
::tabs{default-value="component"}
::tabs-list
::tabs-trigger{value="component"} Component ::
::tabs-trigger{value="native"} Native ::
::
::tabs-content{value="component"}
Input Component
```vue showLineNumbers <template> <FormField v-slot="{ componentField }"> <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input placeholder="shadcn" v-bind="componentField" /> </FormControl> <FormDescription /> <FormMessage /> </FormItem> </FormField> </template>
::
::tabs-content{value="native"}
#### native `input` element
<template> <FormField v-slot="{ field }"> <FormItem> <FormLabel>Username</FormLabel> <FormControl> <input placeholder="shadcn" v-bind="field"> </FormControl> <FormDescription /> <FormMessage /> </FormItem> </FormField> </template>
::
::
## Installation
::tabs{default-value="cli"}
::tabs-list
::tabs-trigger{value="cli"}
CLI
::
::tabs-trigger{value="manual"}
Manual
::
::
::tabs-content{value="cli"}
npx shadcn-vue@latest add form
::
::tabs-content{value="manual"}
::steps
::step
Install the following dependency:
::
npm install reka-ui vee-validate @vee-validate/zod zod
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/form) into your project.
::
::step
Update the import paths to match your project setup.
::
::
::
::
## Usage
<script setup lang="ts"> import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' </script>
<template> <FormField v-slot="{ componentField }" name="username"> <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input placeholder="shadcn" v-bind="componentField" /> </FormControl> <FormDescription> This is your public display name. </FormDescription> <FormMessage /> </FormItem> </FormField> </template>
### Create a form schema
Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev).
Use `@vee-validate/zod` to integrate Zod schema validation with `vee-validate`
`toTypedSchema` also makes the form values and submitted values typed automatically and caters for both input and output types of that schema.
<script setup lang="ts"> import { toTypedSchema } from '@vee-validate/zod' import * as z from 'zod'
const formSchema = toTypedSchema(z.object({ username: z.string().min(2).max(50), })) </script>
### Define a form
Use the `useForm` composable from `vee-validate` or use `<Form />` component to create a form.
::tabs{default-value="composition"}
::tabs-list
::tabs-trigger{value="composition"}
Composition
::
::tabs-trigger{value="component"}
Component
::
::
::tabs-content{value="composition"}
<script setup lang="ts"> import { useForm } from 'vee-validate' import { toTypedSchema } from '@vee-validate/zod' import * as z from 'zod'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
const formSchema = toTypedSchema(z.object({ username: z.string().min(2).max(50), }))
const form = useForm({ validationSchema: formSchema, })
const onSubmit = form.handleSubmit((values) => { console.log('Form submitted!', values) }) </script>
<template> <form @submit="onSubmit"> ... </form> </template>
::
::tabs-content{value="component"}
<script setup lang="ts"> import { toTypedSchema } from '@vee-validate/zod' import * as z from 'zod' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
const formSchema = toTypedSchema(z.object({ username: z.string().min(2).max(50), }))
function onSubmit(values) { console.log('Form submitted!', values) } </script>
<template> <Form :validation-schema="formSchema" @submit="onSubmit"> ... </Form> </template>
::
### Build your form
Based on last step we can either use `<Form />` component or `useForm` composable
`useForm` is recommended because values are typed automatically
<script setup lang="ts"> import { toTypedSchema } from '@vee-validate/zod' import { useForm } from 'vee-validate' import * as z from 'zod'
import { Button } from '@/components/ui/button' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input'
const formSchema = toTypedSchema(z.object({ username: z.string().min(2).max(50), }))
const form = useForm({ validationSchema: formSchema, })
const onSubmit = form.handleSubmit((values) => { console.log('Form submitted!', values) }) </script>
<template> <form @submit="onSubmit"> <FormField v-slot="{ componentField }" name="username"> <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input type="text" placeholder="shadcn" v-bind="componentField" /> </FormControl> <FormDescription> This is your public display name. </FormDescription> <FormMessage /> </FormItem> </FormField> <Button type="submit"> Submit </Button> </form> </template>
### Done
That's it. You now have a fully accessible form that is type-safe with client-side validation.
::component-preview
---
name: InputForm
class: '[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden'
---
::
::
<!-- ## Extras
This example shows how to add motion to your forms with [Formkit AutoAnimate](https://auto-animate.formkit.com/)
**Note:** You need to install `@formkit/auto-animate` to use this feature:
npm install @formkit/auto-animate
::component-preview
---
name: InputFormAutoAnimate
---
:: -->
Pin Input
Description: Accessible pin input component with copy paste functionality.
---
<Callout title="The pin input component has been deprecated." class="mt-0">
See the input otp documentation for more information.
</Callout>
If you're looking for the old pin input component, see the old docs for more information.
Related skills
Forks & variants (1)
Shadcn Vue has 1 known copy in the catalog totaling 98 installs. They canonicalize to this original listing.
- noartem - 98 installs
How it compares
Use shadcn-vue for Vue/Nuxt copy-in components; use shadcn/ui skills for React or Next.js frontends.
FAQ
Which frameworks does shadcn-vue support?
shadcn-vue targets Nuxt and Vue applications, configuring Tailwind CSS, color mode, and component aliases so developers can add accessible UI primitives with shadcn-vue commands.
What Tailwind setup does shadcn-vue configure?
shadcn-vue applies the shadcn-vue schema with new-york style, slate base color, CSS variables, and Nuxt modules @nuxtjs/tailwindcss and @nuxtjs/color-mode for component styling.
Is Shadcn Vue safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.