Technical Documentation: unovue/shadcn-vue
βΉοΈ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference
Overview (`README.md`)
shadcn-vue
A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code. Use this to build your own component library.
Documentation
Visit http://shadcn-vue.com/docs to view the documentation.
Contributing
Please read the contributing guide.
License
Licensed under the MIT license.
Overview (`apps/v4/README.md`)
Nuxt Minimal Starter
Look at the Nuxt documentation to learn more.
Setup
Make sure to install dependencies:
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun installDevelopment Server
Start the development server on http://localhost:3000:
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run devProduction
Build the application for production:
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run buildLocally preview production build:
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run previewCheck out the deployment documentation for more information.
Overview (`packages/cli/README.md`)
shadcn-vue
A CLI for adding components to your project.
Usage
create
Use the create command to scaffold a new Vue project with shadcn-vue pre-configured.
npx shadcn-vue createThe create command will:
- Create a new Vue project (Nuxt, Vite, or Vite + Vue Router)
- Configure Tailwind CSS
- Set up shadcn-vue with your chosen design system
Options
| Option | Description |
|---|---|
-t, --template <template> |
Framework template: nuxt, vite, or vite-router |
-p, --preset |
Use a preset: reka-vega, reka-nova, reka-maia, reka-lyra, reka-mira |
--base |
Component library: reka |
--style <style> |
Visual style: vega, nova, maia, lyra, mira |
--icon-library <lib> |
Icon library: lucide, tabler, hugeicons, phosphor, remixicon |
--font |
Font: inter, figtree, jetbrains-mono, geist, geist-mono |
-b, --base-color <color> |
Base color: neutral, gray, zinc, stone, slate |
-y, --yes |
Skip confirmation prompts |
Examples
# Interactive mode
npx shadcn-vue create
# Use a preset
npx shadcn-vue create --preset reka-nova
# Full customization
npx shadcn-vue create --template nuxt --style maia --icon-library hugeicons --font figtreeinit
Use the init command to initialize dependencies for an existing project.
The init command installs dependencies, adds the cn util, configures tailwind.config.js, and CSS variables for the project.
npx shadcn-vue initOptions
| Option | Description |
|---|---|
--base |
Component library: reka |
--style <style> |
Visual style: vega, nova, maia, lyra, mira |
--icon-library <lib> |
Icon library: lucide, tabler, hugeicons, phosphor, remixicon |
--font |
Font: inter, figtree, jetbrains-mono, geist, geist-mono |
-b, --base-color <color> |
Base color: neutral, gray, zinc, stone, slate |
-d, --defaults |
Use default configuration |
-y, --yes |
Skip confirmation prompts |
add
Use the add command to add components to your project.
The add command adds a component to your project and installs all required dependencies.
npx shadcn-vue add [component]Example
npx shadcn-vue add alert-dialogYou can also run the command without any arguments to view a list of all available components:
npx shadcn-vue addVisual Styles
shadcn-vue supports 5 visual styles that transform how components look:
| Style | Description |
|---|---|
| Vega | The classic shadcn/ui look. Clean, neutral, and familiar. |
| Nova | Reduced padding and margins for compact layouts. |
| Maia | Soft and rounded, with generous spacing. |
| Lyra | Boxy and sharp. Pairs well with mono fonts. |
| Mira | Compact. Made for dense interfaces. |
Presets
Presets are pre-configured combinations of style, icons, and fonts:
| Preset | Style | Icons | Font |
|---|---|---|---|
reka-vega |
Vega | Lucide | Inter |
reka-nova |
Nova | Hugeicons | Inter |
reka-maia |
Maia | Hugeicons | Figtree |
reka-lyra |
Lyra | Hugeicons | JetBrains Mono |
reka-mira |
Mira | Hugeicons | Inter |
Documentation
Visit http://shadcn-vue.com to view the documentation.
License
Licensed under the MIT license.
Overview (`packages/module/README.md`)
Shadcn Nuxt
Shadcn Vue module for Nuxt.
Features
- β° Auto-import correct and relevant components
- more to come...
Quick Setup
- Add
shadcn-nuxtdependency to your project
# Using pnpm
pnpm add -D shadcn-nuxt
# Using yarn
yarn add --dev shadcn-nuxt
# Using npm
npm install --save-dev shadcn-nuxt- Add
shadcn-nuxtto themodulessection ofnuxt.config.ts
export default defineNuxtConfig({
modules: [
'shadcn-nuxt'
],
shadcn: {
/**
* Prefix for all the imported component
*/
prefix: '',
/**
* Directory that the component lives in.
* @default "./components/ui"
*/
componentDir: './components/ui'
}
})That's it! You can now use Shadcn Nuxt in your Nuxt app β¨
Multiple component directories
Projects that split their base UI components from custom extensions can configure multiple directories, each with its own optional prefix:
export default defineNuxtConfig({
modules: ['shadcn-nuxt'],
shadcn: {
prefix: 'Ui',
componentDir: [
'@/components/ui',
{
path: '@/components/ai',
prefix: 'Ai',
},
],
},
})Each directory is ignored by Nuxt's default auto-import scanning and re-registered through the module, ensuring clean separation without console warnings. See issue #1593 for the background.
Development
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run releaseOverview (`deprecated/www/__registry__/README.md`)
Files inside this directory is autogenerated by
./scripts/build-registry.ts. Do not edit them manually.
Apps V4 Content Docs Directory Index (`apps/v4/content/docs/directory/index.md`)
title: Registry Directory navigation: title: Directory
These registries are built into the CLI with no additional configuration required. To add a component, run: npx shadcn-vue@latest add @<registry>/<component>.
::callout{class="bg-muted font-semibold"}
Community registries are maintained by third-party developers. Always review code on installation to ensure it meets your security and quality standards.
::
Don't see a registry? Learn how to add it here.
::registry-directory
::
Documentation
You can use the shadcn-vue CLI to run your own code registry. Running your own registry allows you to distribute your custom components, hooks, pages, config, rules and other files to any project.
Apps V4 Content Docs Registry Getting Started (`apps/v4/content/docs/registry/getting-started.md`)
title: Getting Started
This guide will walk you through the process of setting up your own component registry.
It assumes you already have a project with components and would like to turn it into a registry.
registry.json
The registry.json file is only required if you're using the shadcn-vue CLI to build your registry.
If you're using a different build system, you can skip this step as long as your build system produces valid JSON files that conform to the registry-item schema specification.
Add a registry.json file
Create a registry.json file in the root of your project. Your project can be a Nuxt, Vite, or any other project that supports Vue.
{
"$schema": "https://shadcn-vue.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
// ...
]
}This registry.json file must conform to the registry schema specification.
Add a registry item
Create your component
Add your first component. Here's an example of a simple <HelloWorld /> component:
<script setup lang="ts">
import { Button } from "@/components/ui/button"
</script>
<template>
<Button>Hello World</Button>
</template> Note: This example places the component in the registry/new-york
directory. You can place it anywhere in your project as long as you set the
correct path in the registry.json file and you follow the registry/[NAME]
directory structure.
registry
βββ new-york
βββ HelloWorld
βββ HelloWorld.vue Important: If you're placing your component in a custom directory, make
sure it is configured in your tailwind.config.ts file.
// tailwind.config.ts
export default {
content: ["./registry/**/*.{js,ts,jsx,tsx,vue}"],
}Add your component to the registry
To add your component to the registry, you need to add your component definition to registry.json.
{
"$schema": "https://shadcn-vue.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "hello-world",
"type": "registry:block",
"title": "Hello World",
"description": "A simple hello world component.",
"files": [
{
"path": "registry/new-york/HelloWorld/HelloWorld.vue",
"type": "registry:component"
}
]
}
]
}You define your registry item by adding a name, type, title, description and files.
For every file you add, you must specify the path and type of the file. The path is the relative path to the file from the root of your project. The type is the type of the file.
You can read more about the registry item schema and file types in the registry item schema docs.
Build your registry
Install the shadcn-vue CLI
Note: the build command is currently only available in the shadcn-vue@canary version of the CLI.
npm install shadcn-vue@latestAdd a build script
Add a registry:build script to your package.json file.
{
"scripts": {
"registry:build": "shadcn-vue build"
}
}Run the build script
Run the build script to generate the registry JSON files.
npm run registry:buildNote: By default, the build script will generate the registry JSON files in public/r e.g public/r/hello-world.json.
You can change the output directory by passing the --output option. See the shadcn-vue build command for more information.
Serve your registry
If you're running your registry on Nuxt, you can now serve your registry by running the nuxt server. The command might differ for other frameworks.
npm run devYour files will now be served at http://localhost:3000/r/[NAME].json eg. http://localhost:3000/r/hello-world.json.
Publish your registry
To make your registry available to other developers, you can publish it by deploying your project to a public URL.
Adding Auth
The shadcn-vue CLI does not offer a built-in way to add auth to your registry. We recommend handling authorization on your registry server.
A common simple approach is to use a token query parameter to authenticate requests to your registry. e.g. http://localhost:3000/r/hello-world.json?token=[SECURE_TOKEN_HERE].
Use the secure token to authenticate requests and return a 401 Unauthorized response if the token is invalid. Both the shadcn CLI and Open in v0 will handle the 401 response and display a message to the user.
Guidelines
Here are some guidelines to follow when building components for a registry.
- Place your registry item in the
registry/[STYLE]/[NAME]directory. I'm usingnew-yorkas an example. It can be anything you want as long as it's nested under theregistrydirectory. - The following properties are required for the block definition:
name,description,typeandfiles. - Make sure to list all registry dependencies in
registryDependencies. A registry dependency is the name of the component in the registry eg.input,button,card, etc or a URL to a registry item eg.http://localhost:3000/r/editor.json. - Make sure to list all dependencies in
dependencies. A dependency is the name of the package in the registry eg.zod,sonner, etc. To set a version, you can use thename@versionformat eg.zod@^3.20.0. - Imports should always use the
@/registrypath. eg.import { HelloWorld } from "@/registry/new-york/hello-world/hello-world" - Ideally, place your files within a registry item in
components,hooks,libdirectories.
Install using the CLI
To install a registry item using the shadcn-vue CLI, use the add command followed by the URL of the registry item.
npx shadcn-vue@latest add http://localhost:3000/r/hello-world.jsonInstall from GitHub
If your registry lives in a public GitHub repository with a registry.json
at its root, you do not have to build, host or configure anything. The CLI can
read it directly:
npx shadcn-vue@latest add owner/repo/hello-worldThe CLI resolves the repository's default branch, reads registry.json from the
root, finds the item by name, and fetches each of its files[].path from the
same commit. Every file of an item comes from one commit, so a branch that moves
mid-install cannot give you a half-updated component.
Pinning a branch, tag or commit
Append #ref to install from somewhere other than the default branch:
npx shadcn-vue@latest add owner/repo/hello-world#main
npx shadcn-vue@latest add owner/repo/hello-world#v1.2.0
npx shadcn-vue@latest add owner/repo/hello-world#1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0bBranches win over tags when a name is ambiguous, and an annotated tag resolves to
the commit it points at.
Listing what a repository publishes
npx shadcn-vue@latest search owner/repoOnly install from repositories you trust
Adding a registry item runs third-party code on your machine. This is true of
every registry β hosted, local or GitHub β but the GitHub form is worth calling
out because it needs no configuration, so a single command from a README is
enough to install from a repository you have never looked at.
A registry item can list npm dependencies, which the CLI installs with your
package manager, which in turn runs that package's install scripts. It can also
write files into your project. Treat npx shadcn-vue@latest add owner/repo/item
with the same care as npm install owner-repo β read the registry first if you
do not know who owns it.
The CLI does reject path and target values that are absolute or use .. to
escape the project, both for the item you asked for and at the point files are
written. That is a guard against mistakes and a hostile item's easiest trick; it
is not a sandbox.
Notes
- Item names may contain slashes.
owner/repo/forms/logininstalls the item
namedforms/loginfrom the rootregistry.json. It is not a path to a
nestedregistry.json. - The repository must be public.
raw.githubusercontent.comdoes not serve
private repositories. Use a hosted registry with auth instead. - Git is required. The CLI shells out to
git ls-remoteto resolve the ref,
which is what lets it find the default branch instead of guessingmain. registryDependenciesare resolved the usual way. A full URL is fetched
as-is; a bare name such asbuttonresolves against the defaultshadcn-vue
registry, exactly as it does for any other registry item. Note that a
dependency can point anywhere, so trusting a repository means trusting what it
depends on too.
If you would rather not type the repository on every command, register a
namespace in components.json and install by that instead. A namespace maps to
a hosted registry URL template β not to a GitHub repository β so this is an
alternative to the GitHub form above rather than a shorthand for it:
{
"registries": {
"@acme": "https://acme.com/r/{name}.json"
}
}npx shadcn-vue@latest add @acme/hello-worldApps V4 Content Docs Registry Index (`apps/v4/content/docs/registry/index.md`)
title: Registry
Note: This feature is currently experimental. Help us improve it by
testing it out and sending feedback. If you have any questions, please reach
out to us.
You can use the shadcn-vue CLI to run your own component registry. Running your own registry allows you to distribute your custom components, hooks, pages, and other files to any Vue project.
Registry items are automatically compatible with the shadcn-vue CLI.
Requirements
You are free to design and host your custom registry as you see fit. The only requirement is that your registry items must be valid JSON files that conform to the registry-item schema specification.
Deprecated Www Src Content Docs Registry Getting Started (`deprecated/www/src/content/docs/registry/getting-started.md`)
title: Getting Started
This guide will walk you through the process of setting up your own component registry.
It assumes you already have a project with components and would like to turn it into a registry.
registry.json
The registry.json file is only required if you're using the shadcn-vue CLI to build your registry.
If you're using a different build system, you can skip this step as long as your build system produces valid JSON files that conform to the registry-item schema specification.
Add a registry.json file
Create a registry.json file in the root of your project. Your project can be a Nuxt, Vite, or any other project that supports Vue.
{
"$schema": "https://shadcn-vue.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
// ...
]
}This registry.json file must conform to the registry schema specification.
Add a registry item
Create your component
Add your first component. Here's an example of a simple <HelloWorld /> component:
<script setup lang="ts">
import { Button } from "@/components/ui/button"
</script>
<template>
<Button>Hello World</Button>
</template> Note: This example places the component in the registry/new-york
directory. You can place it anywhere in your project as long as you set the
correct path in the registry.json file and you follow the registry/[NAME]
directory structure.
registry
βββ new-york
βββ HelloWorld
βββ HelloWorld.vue Important: If you're placing your component in a custom directory, make
sure it is configured in your tailwind.config.ts file.
// tailwind.config.ts
export default {
content: ["./registry/**/*.{js,ts,jsx,tsx,vue}"],
}Add your component to the registry
To add your component to the registry, you need to add your component definition to registry.json.
{
"$schema": "https://shadcn-vue.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "hello-world",
"type": "registry:block",
"title": "Hello World",
"description": "A simple hello world component.",
"files": [
{
"path": "registry/new-york/HelloWorld/HelloWorld.vue",
"type": "registry:component"
}
]
}
]
}You define your registry item by adding a name, type, title, description and files.
For every file you add, you must specify the path and type of the file. The path is the relative path to the file from the root of your project. The type is the type of the file.
You can read more about the registry item schema and file types in the registry item schema docs.
Build your registry
Install the shadcn-vue CLI
Note: the build command is currently only available in the shadcn-vue@canary version of the CLI.
npm install shadcn-vue@latestAdd a build script
Add a registry:build script to your package.json file.
{
"scripts": {
"registry:build": "shadcn-vue build"
}
}Run the build script
Run the build script to generate the registry JSON files.
npm run registry:buildNote: By default, the build script will generate the registry JSON files in public/r e.g public/r/hello-world.json.
You can change the output directory by passing the --output option. See the shadcn build command for more information.
Serve your registry
If you're running your registry on Nuxt, you can now serve your registry by running the nuxt server. The command might differ for other frameworks.
npm run devYour files will now be served at http://localhost:3000/r/[NAME].json eg. http://localhost:3000/r/hello-world.json.
Publish your registry
To make your registry available to other developers, you can publish it by deploying your project to a public URL.
Adding Auth
The shadcn-vue CLI does not offer a built-in way to add auth to your registry. We recommend handling authorization on your registry server.
A common simple approach is to use a token query parameter to authenticate requests to your registry. e.g. http://localhost:3000/r/hello-world.json?token=[SECURE_TOKEN_HERE].
Use the secure token to authenticate requests and return a 401 Unauthorized response if the token is invalid. Both the shadcn CLI and Open in v0 will handle the 401 response and display a message to the user.
Guidelines
Here are some guidelines to follow when building components for a registry.
- Place your registry item in the
registry/[STYLE]/[NAME]directory. I'm usingnew-yorkas an example. It can be anything you want as long as it's nested under theregistrydirectory. - The following properties are required for the block definition:
name,description,typeandfiles. - Make sure to list all registry dependencies in
registryDependencies. A registry dependency is the name of the component in the registry eg.input,button,card, etc or a URL to a registry item eg.http://localhost:3000/r/editor.json. - Make sure to list all dependencies in
dependencies. A dependency is the name of the package in the registry eg.zod,sonner, etc. To set a version, you can use thename@versionformat eg.zod@^3.20.0. - Imports should always use the
@/registrypath. eg.import { HelloWorld } from "@/registry/new-york/hello-world/hello-world" - Ideally, place your files within a registry item in
components,hooks,libdirectories.
Install using the CLI
To install a registry item using the shadcn-vue CLI, use the add command followed by the URL of the registry item.
npx shadcn-vue@latest add http://localhost:3000/r/hello-world.jsonDeprecated Www Src Content Docs Registry Index (`deprecated/www/src/content/docs/registry/index.md`)
title: Registry
Note: This feature is currently experimental. Help us improve it by
testing it out and sending feedback. If you have any questions, please reach
out to us.
You can use the shadcn-vue CLI to run your own component registry. Running your own registry allows you to distribute your custom components, hooks, pages, and other files to any Vue project.
Registry items are automatically compatible with the shadcn-vue CLI.
Requirements
You are free to design and host your custom registry as you see fit. The only requirement is that your registry items must be valid JSON files that conform to the registry-item schema specification.
Superpowers Specs 2026 07 11 Reka Rhea Style Design (`docs/superpowers/specs/2026-07-11-reka-rhea-style-design.md`)
reka-rhea style β design spec
Date: 2026-07-11
Status: Approved, ready for implementation
Goal
Add a new style, rhea, to shadcn-vue that faithfully mirrors shadcn-ui React'sbase-rhea style, so users can pick the look that is closest to upstream
shadcn/ui. The style must be fully integrated: authored token map, generated
per-style components, published registry JSON, runtime-scoped preview, and
selectable in the customizer/style switcher.
Background: how styles work in this repo
A "style" is a single file β apps/v4/registry/styles/style-{name}.css β that
maps cn-* placeholder tokens (authored in apps/v4/registry/bases/reka/ui/**,
the single source-of-truth component set) to expanded Tailwind utility classes,
scoped under a .style-{name} { ... } wrapper.
Each style-{name}.css serves two purposes:
- Build time.
scripts/lib/build-styles.ts(buildStyles()) reads the
file, builds a tokenβutilities map (createStyleMap), and transforms every
file inregistry/bases/reka/ui/**viavue-metamorph, replacingcn-*
tokens with their expansions. Output βapps/v4/styles/reka-{name}/ui/**
(a fully-overwritten build artifact).buildStylesRegistry()then publishesapps/v4/public/r/styles/reka-{name}/*.jsonfor CLI installs.
Both loop over theSTYLESarray and run insidepnpm registry:build. - Runtime.
assets/css/main.css@imports eachstyle-{name}.cssunderlayer(base), so.style-{name}-wrapped DOM renders that style live β this
powers the customizer preview.
Token-expansion rules (expand-cn-classes.ts):
- A
cn-*token present in the map is replaced by its utilities (merged viatailwind-merge). - A
cn-*token absent from the map is stripped (unless allowlisted:cn-menu-target,cn-menu-translucent,cn-logical-sides,cn-rtl-flip,cn-font-headingβ these stay as literal classes, defined globally).
STYLES (registry/styles.ts) supplies switcher metadata (name/title/
description/icon). PRESETS (registry/config.ts) supplies a
base+style+theme+font bundle that the PresetPicker/Customizer render.
Source of truth
The React repo already ships the exact token map:/Users/zernonia/Desktop/Coding/shadcn-ui/apps/v4/registry/styles/style-rhea.css
(1550 lines, .style-rhea {} wrapper). This Vue pipeline is a deliberate
mirror of React's build-registry flow, so the file adapts near-verbatim.
Token-alignment analysis (verified)
- Vue base references 388 unique
cn-*tokens; the style-variant subset
that shipping styles (e.g. vega) actually define is 367. - React
style-rhea.cssdefines 373 tokens and is a superset of vega's
367 β every style-variant token the Vue base needs has a rhea value. - React-extra tokens (~31:
cn-drawer-popup,cn-select-value, various*-logicaldirection variants, etc.) that the Vue base never references:
harmless, remain unused in the map. Kept for future-proofing. - Vue-base tokens with cross-repo name drift (drawer / combobox /
pagination / navigation-menu families) that React's map does not name
identically: these get stripped on expansion β which is exactly what
every existing Vue style (vega, nova, β¦) already does. Those components fall
back to their inline base classes, sorheais no less complete than the
shipping styles. Full per-token reconciliation was explicitly deferred
(chosen approach: near-verbatim + verify).
Approach: near-verbatim port + verify
Copy React's style-rhea.css essentially as-is (keeping the .style-rhea
wrapper and all rules), then wire the 4 integration points and run the build.
Verify by rendering, not by hand-diffing 1550 lines.
Changes
1. `apps/v4/registry/styles/style-rhea.css` (new)
Adapted from React style-rhea.css. .style-rhea { β¦ } wrapper preserved.
No content edits beyond what verification surfaces (e.g. any Vue-base token that
must be added because the Vue base β unlike React's β genuinely requires a
per-style value; expected: none, confirmed at verify step).
2. `apps/v4/registry/styles.ts` (edit)
Append a STYLES entry:
{
name: "rhea",
title: "Rhea",
description: "The latest shadcn/ui look. Refined spacing and modern defaults.",
icon: `<svg β¦ 24Γ24 β¦>` // authored to match the existing icon set's visual language; user may swap
}3. `apps/v4/registry/config.ts` (edit)
Append a PRESETS entry mirroring React's base-rhea preset:
{
name: "reka-rhea",
title: "Rhea",
description: "Rhea / Lucide / Inter",
base: "reka",
style: "rhea",
baseColor: "neutral",
theme: "neutral",
chartColor: "neutral",
iconLibrary: "lucide",
font: "inter",
fontHeading: "inherit",
item: "Item",
menuAccent: "subtle",
menuColor: "default",
radius: "default",
}4. `apps/v4/assets/css/main.css` (edit)
Add alongside the other style imports:
@import "../../registry/styles/style-rhea.css" layer(base);5. Generated artifacts (run the build)
Run pnpm registry:build (or the standalone buildStyles() +buildStylesRegistry() runners) to produce:
apps/v4/styles/reka-rhea/ui/**(66 components + composables)apps/v4/public/r/styles/reka-rhea/*.json
These are build outputs, committed like the otherreka-*style outputs.
Out of scope (explicit)
- Default style unchanged.
DEFAULT_CONFIG.stylestaysnova.rheais
additive/selectable. - Homepage cards repoint (
@/styles/reka-nova/*β@/styles/reka-rhea/*)
is an optional follow-up, not part of this task. Can be done afterrhea
is verified if the landing page should visually match upstream shadcn/ui. - Full token reconciliation of drift-named edge components β deferred.
Verification plan
buildStyles()runs clean; grep generatedstyles/reka-rhea/ui/**for
leftover unexpandedcn-*tokens (expect only allowlisted ones, matching
other styles).eslintthe generated output (the build already runseslint --fix).- Boot dev server; render a
.style-rhea-wrapped preview (or temporarily
point a card demo at@/styles/reka-rhea/*) and visually confirm parity
with Reactbase-rhea. - Confirm
Rheaappears in the style switcher (PresetPicker) and selecting it
renders correctly. - Confirm
public/r/styles/reka-rhea/*.jsongenerated for CLI installs.
Success criteria
styles/reka-rhea/ui/**generated, compiles, no straycn-*.Rheaselectable in the customizer and renders faithfully to base-rhea.- Registry JSON published.
- No regression to existing styles (shared base + pipeline untouched except the
additiveSTYLES/PRESETS/main.css/style-rhea.cssentries).
Superpowers Specs 2026 07 11 Unifont Font Loading Design (`docs/superpowers/specs/2026-07-11-unifont-font-loading-design.md`)
Replace `@nuxt/fonts` with unifont (client-side font loading)
Date: 2026-07-11
Scope: apps/v4 docs/customizer app only. Not the CLI-facing font registry.
Problem
The v4 app used the @nuxt/fonts module to auto-download @font-face files by
scanning CSS for font-family names. That module is now disabled (commented out innuxt.config.ts, not in the modules array), which means:
- Docs have no real Geist: the font vars name
"Geist"but nothing downloads
the@font-face, so text falls back to Arial. - The customizer's font pickers don't work end-to-end:
FontPicker.vue+useDesignSystemProvider.tsswap the--font-sans/--font-headingCSS vars
to a family string, but nothing loads the actual@font-facefor the ~25 fonts
inlib/fonts.tswhen one is picked.
We want to control font downloading programmatically with unifont instead of
the auto-magic module: load only Geist by default (for docs), and fetch other
fonts on demand when the user selects them in the customizer.
Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Where unifont runs | Client-side (browser) | User preference; no server route needed. |
| Provider | bunny (unifont providers.bunny) |
Only provider whose endpoints (/list, /css) are fully CORS-enabled for browser fetch. Google's fonts.google.com/metadata/fonts returns no access-control-allow-origin, so unifont's google provider fails in the browser. Bunny mirrors Google Fonts with identical family names, so lib/fonts.ts names map 1:1 with zero remapping. Verified Bunny carries Geist, Geist Mono, and all 25 fonts. |
| Default Geist loading | Eager Bunny <link> in nuxt.config head |
No JS dependency, fast LCP, no flash of fallback for docs. |
| unifont bundle inclusion | Dynamic import('unifont') inside the composable |
unifont pulls in css-tree (non-trivial). Lazy-loading keeps docs/marketing pages that never open the customizer lean. |
CORS verification (empirical, `curl -I -H "Origin: β¦"`)
fonts.google.com/metadata/fontsβ 200, noaccess-control-allow-originβ browser-blocked.fonts.googleapis.com/css2βaccess-control-allow-origin: *(but the google provider still needs the metadata endpoint above).fonts.bunny.net/listand/cssβaccess-control-allow-origin: *β works client-side. β
Architecture
Three independent parts.
1. Default Geist for docs β eager, no JS
- Re-enable
@import "./fonts.css"inapps/v4/assets/css/main.css(currently
commented out) and keep the@themefont vars as-is. - Add a plain Bunny stylesheet link to
app.head.linkinapps/v4/nuxt.config.ts:https://fonts.bunny.net/css?family=geist:400,500,600,700|geist-mono:400,500- Include
<link rel="preconnect" href="https://fonts.bunny.net">for connection warm-up.
- Result: every page renders Geist immediately with no JS dependency.
2. On-demand fonts in the customizer β unifont + Bunny (client)
New composable apps/v4/composables/useFontLoader.ts:
- Memoized unifont instance: a module-level singleton promise created via a
dynamicimport('unifont')βcreateUnifont([providers.bunny()]). Created lazily on firstloadFontcall. loadFont(family: string): Promise<void>:- No-op on the server (
import.meta.clientguard) and for empty/inheritvalues. - Skip families already covered by the eager default (
Geist,Geist Mono). - Dedupe via a module-level
Set<string>keyed by family (also guard against
concurrent in-flight loads keyed the same way) so re-selecting a font never
refetches or re-injects. resolveFont(family, { weights: ['400','500','600','700'] })(Bunny returns
only the weights it actually has).- Build
@font-faceCSS from the returnedFontFaceData[](family, weight,
style,font-display: swap,srclist fromRemoteFontSource/LocalFontSource,unicode-range). - Inject a single
<style data-font="<family>">element intodocument.head. - Wrap in try/catch β on failure
console.warnand resolve anyway
(non-blocking; the CSS var is still set and the family falls back through the
existing CSS font stack).
- No-op on the server (
3. Wire into the design-system provider
In apps/v4/composables/useDesignSystemProvider.ts, the existing watcher appliesselectedFont / selectedHeadingFont by setting --font-sans / --font-heading.
Call loadFont(font.name) for the relevant selected font(s) in that same watcher,
right before/as the CSS var is set. This single integration point covers the body,
heading, and mono pickers (all flow through this provider). Loading is fire-and-forget
(non-blocking); font-display: swap handles the visual swap when the file arrives.
4. Tailwind v4 wiring (make the font actually apply)
Fonts must render through TW4's @theme system, not as loose CSS vars. The chain:
main.css @theme inline { --font-sans: var(--font-sans); ... }
β generates utilities: .font-sans { font-family: var(--font-sans) }
β these reference the runtime-overridable custom property --font-sans
fonts.css @theme { --font-sans: "Geist", ...; --font-heading: ...; --font-mono: ... }
β emits the DEFAULT --font-sans/-heading/-mono onto :rootBecause main.css uses @theme inline, Tailwind does not emit a--font-sans value itself β it only inlines var(--font-sans) into the utilities
and expects the custom property to exist. That property is provided by the
non-inline @theme in fonts.css.
Critical fix: @import "./fonts.css" is currently commented out inmain.css. While commented, var(--font-sans) has no default value, so everyfont-sans / font-heading / font-mono utility resolves to nothing until the
customizer's JS runs. Re-enabling the import (Part 1) is what makes Geist apply
correctly through TW4 by default.
fonts.css must use :root, NOT @theme. The @theme inline block inmain.css already registers the font-sans / font-heading / font-mono theme
keys (mapping the utilities to var(--font-*)). If fonts.css also declares those
keys via @theme, the two collide: the inline block wins and emits a
self-referential --font-sans: var(--font-sans) into @layer theme, which is
cyclic and resolves to nothing β every font var (and --default-font-family)
breaks. Providing the concrete defaults as a plain unlayered :root { --font-sans: β¦ } block avoids the collision and, being unlayered, wins over the layered
theme output. This mirrors shadcn's own color pattern (@theme inline { --color-background: var(--background) } + :root { --background: β¦ }). Runtime
overrides (documentElement.style.setProperty('--font-sans', β¦), inline style) sit
above :root and still win.
Requirements for correctness:
- Re-enable
@import "./fonts.css"so the default--font-*custom properties
exist on:root. Leave themain.css@theme inlineblock unchanged β itsvar(--font-*)mapping is what lets both the CSS default and the runtime JS
override (documentElement.style.setProperty('--font-sans', β¦)) flow into thefont-sans/font-heading/font-monoutilities. - Family-name match: the
@font-facefamily thatuseFontLoaderinjects must
exactly equal the primary family in the stringuseDesignSystemProviderwrites
to--font-sans(e.g.lib/fonts.tsfontFamily: "'Inter', sans-serif"β
injected@font-face { font-family: 'Inter' }). Bunny returns the canonical
Google family name, which matcheslib/fonts.tsnames, so this holds β but the
builder must not rename the family. - The eager Bunny
<link>(Part 1) provides the actual@font-facefor theGeist/Geist Monofamilies that the--font-sans/--font-monodefaults
reference by name, and.theme-mono(--font-sans: var(--font-mono)) inherits
correctly.
Flagged, related pre-existing bug (decide separately): themes.css has
font-theme presets .theme-inter, .theme-noto-sans, .theme-nunito-sans,.theme-figtree that set --font-sans: var(--font-inter) etc., but the--font-inter / --font-noto-sans / β¦ custom properties are defined nowhere
(they have no fallback), so these presets silently render an invalid font-family.
This is independent of the customizer path (which writes the family string directly).
Not fixed in this change unless requested; recommended follow-up is to either
delete these dead presets or route them through useFontLoader + real values.
5. Remove `@nuxt/fonts`
- Delete the commented
fonts: { β¦ }config block innuxt.config.ts. - Remove
"@nuxt/fonts"fromapps/v4/package.jsondependencies.
Data flow
FontPicker sets param
β useDesignSystemSearchParams updates font / fontHeading
β useDesignSystemProvider watcher runs
β useFontLoader.loadFont(family) (dynamic import unifont, bunny resolveFont, inject @font-face)
β sets --font-sans / --font-heading CSS var
β text swaps to loaded font (font-display: swap)Error handling
- unifont / Bunny fetch failure β caught,
console.warn, non-blocking. The CSS var
is still set; text uses the fallback in the existing font stack. - Dedupe
Set+ in-flight guard prevents duplicate fetches / duplicate<style>. - SSR:
loadFontis a no-op on the server; injection happens client-only.
Out of scope
apps/v4/registry/fonts.tsβ CLI-facing registry for consumers of theshadcn-vueCLI; separate concern, untouched.- The in-progress theme-color edits currently in the working tree
(main.cssoklch tweaks) β unrelated, left alone. - Self-hosting fonts / build-time generation β not needed; Bunny CDN is used at
runtime.
Testing / verification
- TW4 default wiring: with
fonts.cssre-imported, inspect afont-sans
element β computedfont-familyresolves to"Geist", β¦(not empty / Arial-only)
with no customizer JS run. Confirms@theme inlineβvar(--font-sans)βfonts.css @themedefault chain is intact. - Docs baseline: load a docs page (customizer never opened) β Network shows the
eager Bunny Geist request; body renders in Geist;unifont/css-treenot in
the loaded JS. - Customizer: open the customizer, pick
Interβ Network shows afonts.bunny.netrequest; a<style data-font="Inter">appears in<head>;
preview text renders in Inter. Re-selecting Inter β no new request. Pick a heading
font and a mono font β same behavior. - Failure path: simulate offline / block bunny.net β no crash; text falls back;
a single warning is logged.
---
### Apps V4 Content Docs .About (`apps/v4/content/docs/.about.md`)
---
title: About
description: Powered by amazing open source projects.
---
## About
[shadcn-vue](https://shadcn-vue.com) is a port of [shadcn/ui](https://ui.shadcn.com) for Vue/Nuxt. It's maintained by [unovue](https://github.com/unovue).
## Credits
- [shadcn](https://twitter.com/shadcn) - The brilliant mind behind the designs, methodology, and implementation of the original [shadcn/ui](https://ui.shadcn.com).
- [Reka UI](https://reka-ui.com) - The headless components that power this project.
- [Radix UI](https://radix-ui.com) - The headless components and examples that the original [shadcn/ui](https://ui.shadcn.com) was built on.
- [Shu Ding](https://shud.in) - The typography style is adapted from his work on Nextra.
- [Cal](https://cal.com) - Where shad copied the styles for the first component: the `Button`.
## License
MIT Β© [shadcn](https://shadcn.com) & [unovue](https://github.com/unovue)
---
### Apps V4 Content Docs .Charts (`apps/v4/content/docs/.charts.md`)
---
title: Charts
description: Versatile visualization tool, allowing users to represent data using various types of charts for effective analysis.
label: Legacy
---
<script setup>
import Area from '~icons/gravity-ui/chart-area-stacked'
import Bar from '~icons/gravity-ui/chart-column'
import Line from '~icons/gravity-ui/chart-line'
import Pie from '~icons/gravity-ui/chart-pie'
</script>
<Callout>
**Legacy**: Component will be moved to [extended repo](https://github.com/unovue/shadcn-vue/issues/1077) with Tailwind v4 support.
---
Only works with Vue >3.3
</Callout>
`Charts` components were built on top of [Unovis](https://unovis.dev/) (a modular data visualization framework), and inspired by [tremor](https://www.tremor.so).
## Chart type
<div class="grid gap-4 mt-8 sm:grid-cols-2 sm:gap-6 not-docs">
<LinkedCard href="/docs/charts/area">
<Area class="text-foreground/80 w-11 h-11" />
<p class="mt-2 font-medium">Area</p>
</LinkedCard>
<LinkedCard href="/docs/charts/line">
<Line class="text-foreground/80 w-11 h-11" />
<p class="mt-2 font-medium">Line</p>
</LinkedCard>
<LinkedCard href="/docs/charts/bar">
<Bar class="text-foreground/80 w-11 h-11" />
<p class="mt-2 font-medium">Bar</p>
</LinkedCard>
<LinkedCard href="/docs/charts/donut">
<Pie class="text-foreground/80 w-11 h-11" />
<p class="mt-2 font-medium">Donut</p>
</LinkedCard>
</div>
## Installation
<Steps>
### Update `css`
Add the following tooltip styling to your `tailwind.css` file:
```css
@layer base {
:root {
/* ... */
--vis-tooltip-background-color: none !important;
--vis-tooltip-border-color: none !important;
--vis-tooltip-text-color: none !important;
--vis-tooltip-shadow-color: none !important;
--vis-tooltip-backdrop-filter: none !important;
--vis-tooltip-padding: none !important;
--vis-primary-color: var(--primary);
/* change to any hsl value you want */
--vis-secondary-color: 160 81% 40%;
--vis-text-color: var(--muted-foreground);
}
}If you are not using css-variables for your component, you need to update the --vis-primary-color and --vis-text-color to your desired hsl value.
You may use this tool to help you find the hsl value for your primary color and text color. Be sure to provide dark mode styling as well.
Colors
By default, we construct the primary theme color, and secondary (--vis-secondary-color) color with different opacity for the graph.
However, you can always pass in the desired color into each chart.
<template>
<AreaChart
:data="data"
:colors="['blue', 'pink', 'orange', 'red']"
/>
</template>Custom tooltip
If you want to customize the Tooltip for the chart, you can pass customTooltip prop with a custom Vue component.
The custom component would receive title and data props, check out ChartTooltip.vue component for example.
The expected prop definition would be:
defineProps<{
title?: string
data: {
name: string
color: string
value: any
}[]
}>()Apps V4 Content Docs .Contribution (`apps/v4/content/docs/.contribution.md`)
title: Contribution description: Learn on how to contribute to shadcn/vue.
Introduction
Thanks for your interest in contributing to shadcn-vue.com. We're happy to have you here.
Please take a moment to review this document before submitting your first pull request. We also strongly recommend that you check for open issues and pull requests to see if someone else is working on something similar.
If you need any help, feel free to reach out to the core team on Discord.
This guide provides detailed information to help new contributors.
About this repository
This repository is a monorepo.
- We use pnpm and
workspacesfor development.
Project Structure
The GitHub repository consists of the several folders. here's a quick view.
packages -
Contains source code for supportingnuxtas a module and theclito add new components.apps/www -
The main folder that holds the source code for the website and everyshadcn/vuecomponent. This folder contains important sub-folders and is a subproject with its ownpackage.json..vitepress -
Contains the configuration and source code forvitepressand theshadcn/vuewebsite.src -
Hosts the main source code for everyshadcn/vuecomponent or demo and their documentation on the website.__registry__ -
Holds the registry file generated byscripts/build-registry.tsto serve components for thecli. This folder's content is auto-generated and should not be edited manually.scripts -
Contains various helper scripts, such asbuild-registry.ts, which automatically generates the__registry__folder.content -
This folder holds all the documentation for the/docsroute. Each component has one.mdfile documenting the installation and usage of the component.examples -
Holds all examples not part of/docs, like the main page.registry -
The main folder hosts the source code for different styles of every component. This is likely the main folder you'll be changing.
We support two different styles for every component in shadcn/vue:
- Default
- New York
Every component added to the repository must support both versions, including the main source code and associated demos.
When adding or modifying components, please ensure that:
- You make the changes for every style.
- You update the documentation.
- You run
pnpm build:registryto update the registry.
Development
Start by cloning the repository:
git clone [email protected]:unovue/shadcn-vue.gitInstall dependencies
pnpm installRun a workspace
You can use the pnpm --filter=[WORKSPACE] command to start the development process for a workspace or some of the shortcut command that we have setup.
Examples
- To run the
shadcn-vue.comwebsite:
pnpm dev- To run the
shadcn-vuecli package:
pnpm dev:cliDocumentation
The documentation for this project is located in the www workspace. You can run the documentation locally by running the following command:
pnpm devDocumentation is written using md. You can find the documentation files in the apps/www/src/content directory.
CLI
The shadcn-vue package is a CLI for adding components to your project. You can find the documentation for the CLI here.
Any changes to the CLI should be made in the packages/cli directory. If you can, it would be great if you could add tests for your changes.
Testing
Tests are written using Vitest. You can run all the tests from the root of the repository.
pnpm testPlease ensure that the tests are passing when submitting a pull request. If you're adding new features, please include tests.
Commit Convention
Before you create a Pull Request, please check whether your commits comply with
the commit conventions used in this repository.
When you create a commit we kindly ask you to follow the conventioncategory(scope or module): message in your commit message while using one of
the following categories:
feat / feature: all changes that introduce completely new code or new
featuresfix: changes that fix a bug (ideally you will additionally reference an
issue if present)refactor: any code related change that is not a fix nor a featuredocs: changing existing or creating new documentation (i.e. README, docs for
usage of a lib or cli usage)build: all changes regarding the build of the software, changes to
dependencies or the addition of new dependenciestest: all changes regarding tests (adding new tests or changing existing
ones)ci: all changes regarding the configuration of continuous integration (i.e.
github actions, ci system)chore: all changes to the repository that do not fit into any of the above
categoriese.g.
feat(components): add new prop to the avatar component
If you are interested in the detailed specification you can visit Conventional Commits.
SFC - Single File Components
Multiple components are integrated into one file in shadcn/ui - the React version of shadcn - while Vue only supports one component per file, hence the name Single File Component (SFC). In such cases, you need to create separate files for each component part and then export them all in an index.ts file.
See the Accordion source code as an example.
Wrapping Reka UI Components
Reka UI hosts many low-level UI components that are used to build reusable components.
There are many cases that you need to wrap Reka UI components.
Props & Events
All of the Reka UI components expose their prop and emit types. We need to forward any props/events that are coming from outside to the Reka UI component.
To do so, we have a helper function named useForwardPropsEmits that combines props and events that must be binded to the child radix component.
To be more clear, the function useForwardPropsEmits takes in props and an optional emit function, and returns a
computed object that combines the parsed props and emits as props.
Here's an example from Accordion root component.
<script setup lang="ts">
import {
AccordionRoot,
type AccordionRootEmits,
type AccordionRootProps,
useForwardPropsEmits,
} from 'reka-ui'
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>As you can see, AccordionRootEmits and AccordionRootProps types are imported from radix, combined with useForwardPropsEmits and then are binded using v-bind syntax.
CSS Classes
There are cases when we want to accept class as a prop in our shadcn/vue component and then combine it with a default tailwind class on our Reka UI component via cn utility function.
In these cases, we can not use v-bind, because this would lead in double class binding.
Take a look at DrawerDescription.vue.
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DrawerDescription } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DrawerDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DrawerDescription
data-slot="drawer-description"
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DrawerDescription>
</template>As you can see, we have created a computed property named delegatedProps to remove class from props, and only then bind
the returned value to our radix component (DrawerDescription in this case).
As for our class, we first declared it as type of HTMLAttributes['class'] and used cn to merge tailwind classes from class prop and our own classes.
This pattern only needs to be applied when the cn utility is required. For instances where there are no default Tailwind classes that need to be merged with user-provided classes, this pattern is not necessary. A good example of this is the SelectValue.vue component.
<script setup lang="ts">
import { SelectValue, type SelectValueProps } from 'reka-ui'
const props = defineProps<SelectValueProps>()
</script>
<template>
<SelectValue v-bind="props">
<slot />
</SelectValue>
</template>Boolean Props
When you are building a wrapper for a component, in some cases you want to ignore Vue Props Boolean Casting.
You can either set default value as undefined for all the boolean field, or you can use useForwardProps composable.
Take a look at AccordionItem.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem
v-bind="forwardedProps"
:class="cn('border-b', props.class)"
>
<slot />
</AccordionItem>
</template>Since AccordionItemProps type has atleast one boolean property, we need to use useForwardProps on the entire props object.
Note that useForwardPropsEmits use useForwardProps under the hood.
Component as Root
Whenever your root component is a Component Primitive from vue, it's easier to use Primitive instead.
Let's take a look at Button.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { type ButtonVariants, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps(), {
as: 'button',
})
</script>
<template>
<slot />
</template>You'll need to extend PrimitiveProps in your props to support Primitive component. In most cases you would also need a default value for as property.
Updating with `shadcn/ui`
shadcn/vue is an unofficial, community-led Vue port of shadcn/ui, as time goes by, they might get out of sync.
As of today, we are in sync with this commit of shadcn/ui.
Click on the following link to check if there are newer commits that we should be synced with.
- There are no changes - If you see "There isnβt anything to compare", nothing needs to be done as we are synced with latest version.
- If there are changes, you should review thoese changes and try to apply them on
shadcn/vuecodebase and create a PR, remember to update thelatestSyncCommitTagin this file too.
Debugging
Here are some tools and techniques that can help you debug more effectively while contributing to shadcn/vue or developing your own projects.
Install Vue Dev Tools
To easily inspect component props, attributes, events, and more, you can leverage the Vue DevTools extension for browsers. This extension provides a user-friendly interface for debugging Vue components and can improve your development experience.
Enable Custom Formmaters
Vue wraps values stored in a ref in a way that, when logged, results in a nested object and requires manual inspection to access the value stored in the ref.
You can enable Custom Formatters in your browser to automate this process.
Apps V4 Content Docs .Tailwind V4 (`apps/v4/content/docs/.tailwind-v4.md`)
title: Tailwind v4 description: How to use shadcn-vue with Tailwind v4.
It's here! Tailwind v4. Ready for you to try out. You can start using it today.
What's New
- The CLI can now initialize projects with Tailwind v4.
- Full support for the new
@themedirective and@theme inlineoption. - All components are updated for Tailwind v4.
- Every primitive now has a
data-slotattribute for styling. - We've fixed and cleaned up the style of the components.
- We're deprecating the
toastcomponent in favor ofsonner. - Buttons now use the default cursor.
- We're deprecating the
defaultstyle. New projects will usenew-york. - HSL colors are now converted to OKLCH.
Note: this is non-breaking. Your existing apps with Tailwind v3 will still work. When you add new components, they'll still be in v3 until you upgrade. Only new projects start with Tailwind v4.
See it Live
I put together a demo with all the updated components here: https://v4.shadcn-vue.com
Take a look and test the components. If you find any bugs, please let me know on GitHub.
Try It Out
See the framework specific guides below for how to get started.
Vite
Nuxt
Astro
Laravel
Manual
Upgrade Your Project
Important: Before upgrading, please read the Tailwind v4 Compatibility
Docs and make sure your project
is ready for the upgrade. Tailwind v4 uses bleeding-edge browser features and
is designed for modern browsers.
One of the major advantages of using shadcn-vue is that the code you end up with is exactly what you'd write yourself. There are no hidden abstractions.
This means when a dependency has a new release, you can just follow the official upgrade paths.
Here's how to upgrade your existing projects (full docs are on the way):
1. Follow the Tailwind v4 Upgrade Guide
- Upgrade to Tailwind v4 by following the official upgrade guide: https://tailwindcss.com/docs/upgrade-guide
- Use the
@tailwindcss/upgrade@nextcodemod to remove deprecated utility classes and update tailwind config.
2. Update your CSS variables
The codemod will migrate your CSS variables as references under the @theme directive.
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
}
}
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
}This works. But to make it easier to work with colors and other variables, we'll need to move the hsl wrappers and use @theme inline.
Here's how you do it:
- Move
:rootand.darkout of the@layerbase. - Wrap the color values in
hsl() - Add the
inlineoption to@themei.e@theme inline - Remove the
hsl()wrappers from@theme
:root {
--background: hsl(0 0% 100%); // <-- Wrap in hsl
--foreground: hsl(0 0% 3.9%);
}
.dark {
--background: hsl(0 0% 3.9%); // <-- Wrap in hsl
--foreground: hsl(0 0% 98%);
}
@theme inline {
--color-background: var(--background); // <-- Remove hsl
--color-foreground: var(--foreground);
}This change makes it much simpler to access your theme variables in both utility classes and outside of CSS for eg. using color values in JavaScript.
3. Use new `size-*` utility
The new size-* utility (added in Tailwind v3.4), is now fully supported by tailwind-merge. You can replace w-* h-* with the new size-* utility:
- w-4 h-4
+ size-44. Install and Update your dependencies
pnpm i tw-animate-css
pnpm up reka-ui @lucide/vue tailwind-merge clsx --latestApps V4 Content Docs .Typography (`apps/v4/content/docs/.typography.md`)
title: Typography description: Styles for headings, paragraphs, lists...etc component: true
h1
h2
h3
h4
p
blockquote
table
list
Inline code
Lead
Large
Small
Muted
Apps V4 Content Docs 01.Introduction (`apps/v4/content/docs/01.introduction.md`)
title: Introduction description: shadcn-vue is a set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks and AI models. Open Source. Open Code.
This is not a component library. It is how you build your component library.
::vue-school-link{lesson="what-is-shadcn-vue" placement="top"}
Watch a Vue School intro video about shadcn-vue.
::
You know how most traditional component libraries work: you install a package from NPM, import the components, and use them in your app.
This approach works well until you need to customize a component to fit your design system or require one that isn't included in the library. Often, you end up wrapping library components, writing workarounds to override styles, or mixing components from different libraries with incompatible APIs.
This is what shadcn-vue aims to solve. It is built around the following principles:
- Open Code: The top layer of your component code is open for modification.
- Composition: Every component uses a common, composable interface, making them predictable.
- Distribution: A flat-file schema and command-line tool make it easy to distribute components.
- Beautiful Defaults: Carefully chosen default styles, so you get great design out-of-the-box.
- AI-Ready: Open code for LLMs to read, understand, and improve.
Open Code
shadcn-vue hands you the actual component code. You have full control to customize and extend the components to your needs. This means:
- Full Transparency: You see exactly how each component is built.
- Easy Customization: Modify any part of a component to fit your design and functionality requirements.
- AI Integration: Access to the code makes it straightforward for LLMs to read, understand, and even improve your components.
In a typical library, if you need to change a button's behavior, you have to override styles or wrap the component. With shadcn-vue, you simply edit the button code directly.
shadcn-vue follows a headless component architecture. This means the core of your app can receive fixes by updating your dependencies, for instance, radix-ui or input-otp.
The topmost layer, i.e., the one closest to your design system, is not coupled with the implementation of the library. It stays open for modification.
Composition
Every component in shadcn-vue shares a common, composable interface. If a component does not exist, we bring it in, make it composable, and adjust its style to match and work with the rest of the design system.
A shared, composable interface means it's predictable for both your team and LLMs. You are not learning different APIs for every new component. Even for third-party ones.
Distribution
shadcn-vue is also a code distribution system. It defines a schema for components and a CLI to distribute them.
- Schema: A flat-file structure that defines the components, their dependencies, and properties.
- CLI: A command-line tool to distribute and install components across projects with cross-framework support.
You can use the schema to distribute your components to other projects or have AI generate completely new components based on existing schema.
Beautiful Defaults
shadcn-vue comes with a large collection of components that have carefully chosen default styles. They are designed to look good on their own and to work well together as a consistent system:
- Good Out-of-the-Box: Your UI has a clean and minimal look without extra work.
- Unified Design: Components naturally fit with one another. Each component is built to match the others, keeping your UI consistent.
- Easily Customizable: If you want to change something, it's simple to override and extend the defaults.
AI-Ready
The design of shadcn-vue makes it easy for AI tools to work with your code. Its open code and consistent API allow AI models to read, understand, and even generate new components.
An AI model can learn how your components work and suggest improvements or even create new components that integrate with your existing design.
Apps V4 Content Docs 02.Components (`apps/v4/content/docs/02.components.md`)
title: Components description: Here you can find all the components available in the library. We are working on adding more components.
Apps V4 Content Docs 02.Installation (`apps/v4/content/docs/02.installation.md`)
title: Installation description: How to install dependencies and structure your app.
Pick Your Framework
Start by selecting your framework of choice. Then follow the instructions to install the dependencies and structure your app. shadcn-vue is built to work with all Vue frameworks.
Vite
Nuxt
Astro
Laravel
Manual
VSCode extension
Install the shadcn-vue extension by @selemondev in Visual Studio Code to easily add Shadcn Vue components to your project.
This extension offers a range of features:
- Ability to initialize the Shadcn Vue CLI
- Install components
- Open documentation
- Navigate to a specific component's documentation page directly from your IDE.
- Handy snippets for quick and straightforward component imports and markup.
Apps V4 Content Docs 03.Components Json (`apps/v4/content/docs/03.components-json.md`)
title: components.json description: Configuration for your project.
The components.json file holds configuration for your project.
We use it to understand how your project is set up and how to generate components customized for your project.
It is only required if you're using the CLI to add components to your
project. If you're using the copy and paste method, you don't need this file.
You can create a components.json file in your project by running the following command:
npx shadcn-vue@latest initSee the CLI section for more information.
$schema
You can see the JSON Schema for components.json here.
{
"$schema": "https://shadcn-vue.com/schema.json"
}style
The style for your components. This cannot be changed after initialization.
{
"style": "new-york"
}The default style has been deprecated. Use the new-york style instead.
tailwind
Configuration to help the CLI understand how Tailwind CSS is set up in your project.
See the installation section for how to set up Tailwind CSS.
tailwind.config
Path to where your tailwind.config.js file is located. For Tailwind CSS v4, leave this blank.
{
"tailwind": {
"config": "tailwind.config.js" | "tailwind.config.ts"
}
}tailwind.css
Path to the CSS file that imports Tailwind CSS into your project.
{
"tailwind": {
"css": "styles/global.css"
}
}tailwind.baseColor
This is used to generate the default color palette for your components. This cannot be changed after initialization.
{
"tailwind": {
"baseColor": "gray" | "neutral" | "slate" | "stone" | "zinc"
}
}tailwind.cssVariables
You can choose between using CSS variables or Tailwind CSS utility classes for theming.
To use utility classes for theming set tailwind.cssVariables to false. For CSS variables, set tailwind.cssVariables to true.
{
"tailwind": {
"cssVariables": `true` | `false`
}
}For more information, see the theming docs.
This cannot be changed after initialization. To switch between CSS variables and utility classes, you'll have to delete and re-install your components.
tailwind.prefix
The prefix to use for your Tailwind CSS utility classes. Components will be added with this prefix.
{
"tailwind": {
"prefix": "tw-"
}
}typescript
Choose between TypeScript or JavaScript components.
Setting this option to false allows components to be added as JavaScript in .vue file.
{
"typescript": `true` | `false`
}aliases
The CLI uses these values and the paths config from your tsconfig.json or jsconfig.json file to place generated components in the correct location.
Path aliases have to be set up in your tsconfig.json or jsconfig.json file.
Important: If you're using the src directory, make sure it is included
under paths in your tsconfig.json or jsconfig.json file.
aliases.utils
Import alias for your utility functions.
{
"aliases": {
"utils": "@/lib/utils"
}
}aliases.components
Import alias for your components.
{
"aliases": {
"components": "@/components"
}
}aliases.ui
Import alias for ui components.
The CLI will use the aliases.ui value to determine where to place your ui components. Use this config if you want to customize the installation directory for your ui components.
{
"aliases": {
"ui": "@/app/ui"
}
}aliases.lib
Import alias for lib functions such as cn.
{
"aliases": {
"lib": "@/lib"
}
}aliases.composables
Import alias for composables such as useMediaQuery or useToast.
{
"aliases": {
"composables": "@/composables"
}
}Apps V4 Content Docs 04.Theming (`apps/v4/content/docs/04.theming.md`)
title: Theming description: Using CSS variables and theme tokens.
::vue-school-link{lesson="theming-with-shadcn-vue" placement="top"}
Watch a Vue School video about theming with shadcn-vue.
::
Want to build your theme visually? Use shadcn-vue/create to preview colors, radius, fonts, and icons, then generate a preset for your project.
We use and recommend CSS variables for theming.
This gives you semantic theme tokens like background, foreground, and primary that components use by default. Override those tokens in your CSS to change the look of your app without rewriting component classes.
To use CSS variables for theming, set tailwind.cssVariables to true in your components.json file. This is the default.
{
"style": "default",
"typescript": true,
"tailwind": {
"config": "",
"css": "assets/css/tailwind.css",
"baseColor": "neutral",
"cssVariables": true
}
}Tailwind maps these tokens into utilities like bg-background, text-foreground, border-border, and ring-ring.
Dark mode works by overriding the same tokens inside a .dark selector. See the dark mode docs for adding a theme provider and toggling the .dark class.
Token Convention
We use semantic background and foreground pairs. The base token controls the surface color and the -foreground token controls the text and icon color that sits on that surface.
The background suffix is omitted for the surface token. For example, primary pairs with primary-foreground.
Given the following CSS variables:
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);The background color of the following component will be var(--primary) and the foreground color will be var(--primary-foreground).
HelloTheme Tokens
These tokens live in your CSS file under :root and .dark.
| Token | What it controls | Used by |
|---|---|---|
background / foreground |
The default app background and text color. | The page shell, page sections, and default text. |
card / card-foreground |
Elevated surfaces and the content inside them. | Card, dashboard panels, settings panels. |
popover / popover-foreground |
Floating surfaces and the content inside them. | Popover, DropdownMenu, ContextMenu, and other overlays. |
primary / primary-foreground |
High-emphasis actions and brand surfaces. | Default Button, selected states, badges, and active accents. |
secondary / secondary-foreground |
Lower-emphasis filled actions and supporting surfaces. | Secondary buttons, secondary badges, and supporting UI. |
muted / muted-foreground |
Subtle surfaces and lower-emphasis content. | Descriptions, placeholders, empty states, helper text, and subdued surfaces. |
accent / accent-foreground |
Interactive hover, focus, and active surfaces. | Ghost buttons, menu highlight states, hovered rows, and selected items. |
destructive |
Destructive actions and error emphasis. | Destructive buttons, invalid states, and destructive menu items. |
border |
Default borders and separators. | Cards, menus, tables, separators, and layout dividers. |
input |
Form control borders and input surface treatment. | Input, Textarea, Select, and outline-style controls. |
ring |
Focus rings and outlines. | Buttons, inputs, checkboxes, menus, and other focusable controls. |
chart-1 ... chart-5 |
The default chart palette. | Charts and chart-driven dashboard blocks. |
sidebar / sidebar-foreground |
The base sidebar surface and default sidebar text. | The Sidebar container and its default content. |
sidebar-primary / sidebar-primary-foreground |
High-emphasis actions inside the sidebar. | Active items, icon tiles, badges, and sidebar CTAs. |
sidebar-accent / sidebar-accent-foreground |
Hover and selected states inside the sidebar. | Sidebar menu hover states, open items, and interactive rows. |
sidebar-border |
Sidebar-specific borders and separators. | Sidebar headers, groups, and internal dividers. |
sidebar-ring |
Sidebar-specific focus rings. | Focused controls inside the sidebar. |
radius |
The base corner radius scale. | Cards, inputs, buttons, popovers, and the derived radius-* tokens. |
The chart tokens are covered in more detail in the Chart theming docs.
Radius Scale
--radius is the base radius token for your theme.
We derive a small radius scale from it so components can use consistent corner sizes while still sharing a single source of truth.
@theme inline {
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}This means:
radius-lgis the base value.- Smaller radii scale down from
--radius. - Larger radii scale up from
--radius. - Changing
--radiusupdates the entire radius scale.
Adding New Tokens
To add a new token, define it under :root and .dark, then expose it to Tailwind with @theme inline.
:root {
--warning: oklch(0.84 0.16 84);
--warning-foreground: oklch(0.28 0.07 46);
}
.dark {
--warning: oklch(0.41 0.11 46);
--warning-foreground: oklch(0.99 0.02 95);
}
@theme inline {
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}You can now use bg-warning and text-warning-foreground in your components.
Base Colors
tailwind.baseColor controls the default token values generated for your project when you run init or use a preset.
The available base colors are: Neutral, Gray, Zinc, Stone, and Slate.
Default Theme CSS
The following is the full default neutral theme scaffold. Copy it into your global CSS file and adjust the tokens as needed.
::code-collapsible-wrapper
/* Detailed source-code truncated for AI context efficiency. */::
Without CSS Variables
If you do not want to use CSS variables, the CLI can generate components with inline Tailwind color utilities instead.
npx shadcn-vue@latest init --no-css-variablesThis sets tailwind.cssVariables to false in your components.json file.
This is an installation-time choice. To switch an existing project, delete and re-install your components.
Apps V4 Content Docs 05.Dark Mode (`apps/v4/content/docs/05.dark-mode.md`)
title: Dark Mode description: Adding dark mode to your site.
Vite
Nuxt
Vitepress
Astro
Apps V4 Content Docs 06.Cli (`apps/v4/content/docs/06.cli.md`)
title: CLI description: Use the shadcn-vue CLI to add components to your project.
init
Use the init command to initialize configuration and dependencies for a new project.
The init command installs dependencies, adds the cn util and configures CSS variables for the project.
npx shadcn-vue@latest initOptions
/* Detailed source-code truncated for AI context efficiency. */The create command is an alias for init:
npx shadcn-vue@latest createadd
Use the add command to add components and dependencies to your project.
npx shadcn-vue@latest add [component]A component can be named in any of these ways:
| Form | Resolves to |
|---|---|
button |
an item in the default shadcn-vue registry |
@acme/button |
an item in a registry configured under registries in components.json |
https://acme.com/r/button.json |
a registry item fetched directly from that URL |
./button.json |
a registry item on disk |
owner/repo/button |
an item in the root registry.json of a public GitHub repository |
owner/repo/button#v1.0.0 |
the same, pinned to a branch, tag or commit |
See Install from GitHub for
the GitHub form.
Options
Usage: shadcn-vue add [options] [components...]
add a component to your project
Arguments:
components names, url or local path to component
Options:
-y, --yes skip confirmation prompt. (default: false)
-o, --overwrite overwrite existing files. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory.
-a, --all add all available components (default: false)
-p, --path the path to add the component to.
-s, --silent mute output. (default: false)
-h, --help display help for commandapply
Use the apply command to apply a preset to an existing project.
npx shadcn-vue@latest apply --preset novaOptions
Usage: shadcn-vue apply [options] [preset]
apply a preset to an existing project
Arguments:
preset the preset to apply
Options:
--preset preset configuration to apply
-y, --yes skip confirmation prompt. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory.
-s, --silent mute output. (default: false)
-h, --help display help for commandview
Use the view command to view items from the registry before installing them.
npx shadcn-vue@latest view [item]You can view multiple items at once:
npx shadcn-vue@latest view button card dialogOr view items from namespaced registries:
npx shadcn-vue@latest view @acme/auth @v0/dashboardOptions
Usage: shadcn-vue view [options]
view items from the registry
Arguments:
items the item names or URLs to view
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-h, --help display help for commandsearch
Use the search command to search for items from registries.
npx shadcn-vue@latest search [registry]You can search with a query:
npx shadcn-vue@latest search @shadcn-vue -q "button"Or search multiple registries at once:
npx shadcn-vue@latest search @shadcn-vue @v0 @acmeThe list command is an alias for search:
npx shadcn-vue@latest list @acmeOptions
Usage: shadcn-vue search|list [options] <registries...>
search items from registries
Arguments:
registries the registry names or urls to search items from. Names
must be prefixed with @.
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-q, --query <query> query string
-l, --limit <number> maximum number of items to display per registry (default: "100")
-o, --offset <number> number of items to skip (default: "0")
-h, --help display help for commandbuild
Use the build command to generate the registry JSON files.
npx shadcn-vue@latest buildThis command reads the registry.json file and generates the registry JSON files in the public/r directory.
Options
Usage: shadcn-vue build [options] [registry]
build components for a shadcn-vue registry
Arguments:
registry path to registry.json file (default: "./registry.json")
Options:
-o, --output destination directory for json files (default: "./public/r")
-c, --cwd <cwd> the working directory. defaults to the current directory.
-h, --help display help for commandTo customize the output directory, use the --output option.
npx shadcn-vue@latest build --output ./public/registrydocs
Use the docs command to fetch documentation and API references for components.
npx shadcn-vue@latest docs [component]Options
Usage: shadcn-vue docs [options] <components...>
get docs, api references and usage examples for components
Arguments:
components component names
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-b, --base the base to use (reka). defaults to project base.
--json output as JSON. (default: false)
-h, --help display help for commandinfo
Use the info command to get information about your project.
npx shadcn-vue@latest infoOptions
Usage: shadcn-vue info [options]
get information about your project
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
--json output as JSON. (default: false)
-h, --help display help for commandmigrate
Use the migrate command to run migrations on your project.
npx shadcn-vue@latest migrate [migration]Available Migrations
| Migration | Description |
|---|---|
icons |
Migrate your UI components to a different icon library. |
rtl |
Migrate your components to support RTL (right-to-left). |
Options
Usage: shadcn-vue migrate [options] [migration] [path]
run a migration.
Arguments:
migration the migration to run.
path optional path or glob pattern to migrate.
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-l, --list list all migrations. (default: false)
-y, --yes skip confirmation prompt. (default: false)
-h, --help display help for commandmigrate rtl
The rtl migration transforms your components to support RTL (right-to-left) languages.
npx shadcn-vue@latest migrate rtlThis will:
- Update
components.jsonto setrtl: true - Transform physical CSS properties to logical equivalents (e.g.,
ml-4βms-4,text-leftβtext-start) - Add
rtl:variants where needed (e.g.,space-x-4βspace-x-4 rtl:space-x-reverse)
Migrate specific files
You can migrate specific files or use glob patterns:
# Migrate a specific file
npx shadcn-vue@latest migrate rtl src/components/ui/button/Button.vue
# Migrate files matching a glob pattern
npx shadcn-vue@latest migrate rtl "src/components/ui/**"If no path is provided, the migration will transform all files in your ui directory (from components.json).
Apps V4 Content Docs 07.Javascript (`apps/v4/content/docs/07.javascript.md`)
title: JavaScript description: How to use shadcn-vue with JavaScript
This project and the components are written in TypeScript. We recommend using TypeScript for your project as well.
However we provide a JavaScript version of the components as well. The JavaScript version is available via the cli.
To opt-out of TypeScript, you can use the typescript flag in your components.json file.
{
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"typescript": false,
"aliases": {
"utils": "~/lib/utils",
"components": "~/components"
}
}To configure import aliases, you can use the following jsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}Apps V4 Content Docs 08.Rtl (`apps/v4/content/docs/08.rtl.md`)
title: RTL description: Right-to-left support for shadcn-vue components.
shadcn-vue components have first-class support for right-to-left (RTL) layouts. Text alignment, positioning, and directional styles automatically adapt for languages like Arabic, Hebrew, and Persian.
When you install components, the CLI automatically transforms physical positioning classes to logical equivalents, so your components work seamlessly in both LTR and RTL contexts.
Get Started
Enable RTL
Create a new project using the --rtl flag:
npx shadcn-vue@latest create --rtlThis will create a components.json file with the rtl: true flag.
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "nova",
"rtl": true
}For an existing project, set rtl: true in your components.json and see Migrating existing components below.
Set the direction
Add the dir="rtl" and lang="ar" attributes to the html tag. Update lang="ar" to your target language.
For Vite, set them in your index.html:
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<script type="module" src="/src/main.ts"></script>
</html>For Nuxt, set them via useHead in your app.vue:
<script setup lang="ts">
useHead({
htmlAttrs: {
lang: 'ar',
dir: 'rtl',
},
})
</script>Add ConfigProvider
Wrap your app with the ConfigProvider component from reka-ui with the dir="rtl" prop, so all components read the correct direction:
<script setup lang="ts">
import { ConfigProvider } from 'reka-ui'
</script>
<template>
<ConfigProvider dir="rtl">
<RouterView />
</ConfigProvider>
</template>Add Font
For the best RTL experience, we recommend using fonts that have proper support for your target language. Noto is a great font family for this and it pairs well with Inter and Geist.
Install the font using Fontsource:
npm install @fontsource-variable/noto-sans-arabicImport the font in your css file:
@import "tailwindcss";
@import "tw-animate-css";
@import "@fontsource-variable/noto-sans-arabic";
@theme inline {
--font-sans: "Noto Sans Arabic Variable", sans-serif;
}For other languages, eg. Hebrew, you can use @fontsource-variable/noto-sans-hebrew.
Add Components
You are now ready to add components to your project. The CLI will take care of handling RTL support for you.
npx shadcn-vue@latest add cardHow it works
When you add components with rtl: true set in your components.json, the CLI automatically transforms classes to be RTL compatible:
- Physical positioning classes like
left-*andright-*are converted to logical equivalents likestart-*andend-*. - Text alignment and spacing classes are adjusted accordingly.
- Supported icons are automatically flipped using
rtl:rotate-180.
Font Recommendations
For the best RTL experience, we recommend using fonts that have proper support for your target language. Noto is a great font family for this and it pairs well with Inter and Geist.
See the Get Started section for details on installing and configuring RTL fonts.
Animations
The CLI also handles animation classes, automatically transforming physical directional animations to their logical equivalents. For example, slide-in-from-right becomes slide-in-from-end.
This ensures animations like dropdowns, popovers, and tooltips animate in the correct direction based on the document's text direction.
A note on tw-animate-css:
There is a known issue with the tw-animate-css library where the logical slide utilities are not working as expected. For now, make sure you pass in the dir attribute to portal elements.
Open
Content<Tooltip>
<TooltipTrigger>Open</TooltipTrigger>
<TooltipContent dir="rtl">
Content
</TooltipContent>
</Tooltip>Migrating existing components
If you have existing components installed before enabling RTL, you can migrate them using the CLI as follows:
Run the migrate command
npx shadcn-vue@latest migrate rtl [path][path] accepts a path or glob pattern to migrate. If you don't provide a path, it will migrate all the files in the ui directory.
# Migrate a specific file
npx shadcn-vue@latest migrate rtl src/components/ui/button/Button.vue
# Migrate files matching a glob pattern
npx shadcn-vue@latest migrate rtl "src/components/ui/**"This will also update your components.json to set rtl: true. See the CLI documentation for more details.
Manual Migration (Optional)
The following components are not automatically migrated by the CLI and may need manual adjustments:
Migrate Icons
Some icons like ArrowRight or ChevronLeft might need the rtl:rotate-180 class to be flipped correctly. Add the rtl:rotate-180 class to the icon component to flip it correctly.
<ArrowRight class="rtl:rotate-180" />Add ConfigProvider
Wrap your app with the ConfigProvider component from reka-ui. See the Get Started section for details.
Apps V4 Content Docs 09.Figma (`apps/v4/content/docs/09.figma.md`)
title: Figma description: Every component recreated in Figma. With customizable props, typography and icons.
Note: The Figma files are contributed by the community. If you have any
questions or feedback, please reach out to the Figma file maintainers.
Paid
- shadcn/ui kit by Matt Wierzbicki - A premium, always up-to-date UI kit for Figma - shadcn/ui compatible and optimized for smooth design-to-dev handoff.
Free
- shadcn/ui design system by Pietro Schirano - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
- Obra shadcn/ui by Obra Studio - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
Apps V4 Content Docs 10.Changelog (`apps/v4/content/docs/10.changelog.md`)
title: Changelog description: Latest updates and announcements.
August 2026 - Typeset, Questionnaire & Upgrades
A new styling system, a new component, and two upgrades under the hood.
- Typeset: One CSS file that styles your rendered markdown.
- Questionnaire: Multi-step questions, one at a time.
- Drawer: Now built on Reka UI.
- Data Table: Now on TanStack Table v9.
Typeset
You render markdown and get back plain unstyled HTML. So you style it: headings, paragraphs, lists, tables. Then you do it again for the docs. Then again for the chat app.
We ported shadcn/typeset to fix that. It's one CSS file that styles everything inside a typeset container, and it lives in your project, so you can change it whenever you need to.
We read a lot about type β scale ratios, tracking, optical sizing, measure, leading β tried exposing all of it, and it was too much. So we condensed it into three controls: size, leading, and flow. Heading sizes, list indents, the gap under a heading, the space around a rule: everything derives from those three. We call it rhythm.
.typeset-docs {
--typeset-font-body: var(--font-geist-sans);
--typeset-font-heading: var(--font-geist-sans);
--typeset-font-mono: var(--font-geist-mono);
--typeset-size: 15px;
--typeset-leading: 1.75;
--typeset-flow: 1.25em;
}It fits its container, so the same content scales down in a chat bubble and up in an article. It uses your theme tokens for colors, fonts, and radius. And it's built for streaming: when a new block arrives, earlier blocks don't shift their margins or borders.
Head to the typeset builder to pick your fonts and rhythm, preview them on real content, and copy out the CSS. Full details in the Typeset docs.
Questionnaire
Questionnaire is a new component for asking a series of questions, one at a time: single-choice, multiple-choice, freeform, and skippable.
::component-preview
name: QuestionnaireDemo class: style-nova align: end previewClass: min-h-[560px] p-4 sm:p-8
::
It handles navigation, answer registration, validation, and progress for you. Every part accepts as and as-child, so you can compose it into a card or a dialog and let the dialog keep its accessible name from the question title.
npx shadcn-vue@latest add questionnaireSee the Questionnaire docs for keyboard shortcuts, conditional questions, controlled answers, and resuming a session.
Drawer
This is a breaking change. Drawer now uses Reka UI's first-class Drawer primitive instead of vaul-vue.
You can drop the vaul-vue dependency. The direction prop becomes swipe-direction β the direction the drawer dismisses toward, not the side it sits on β and should-scale-background is gone.
<Drawer direction="bottom">
<Drawer swipe-direction="down">The values are up, right, down, and left, with down as the default β so a plain <Drawer> is still the bottom drawer.
Re-run the CLI to pick up the new component:
npx shadcn-vue@latest add drawerWe also added demos for sides and scrollable content.
::component-preview
name: DrawerSides
::
Data Table
The Data Table guide, the tasks and dashboard examples, and the dashboard-01 block now use TanStack Table v9.
v9 is feature-based: you opt into sorting, filtering, pagination, and the rest with tableFeatures(), and anything you don't register is tree-shaken out of your bundle. useVueTable becomes useTable, row models move onto the features object, and the valueUpdater helper is gone β v9 manages that state internally.
export const features = tableFeatures({
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
})One thing to watch: the built-in filter and sort functions have to be registered under filterFns and sortFns. If you reference one by string without registering it, it silently does nothing.
::component-preview
name: DataTableDemo align: start previewClass: items-start h-auto px-4 md:px-8
::
The guide has been rewritten end to end for v9.
July 2026 - Rhea & Components for Chat Interfaces
Two big additions this month: a new style, Rhea, and a set of components for building chat interfaces.
Rhea
Say hello to Rhea, a new style for shadcn-vue.
Rhea is the latest shadcn/ui look, mirroring the upstream base-rhea style as closely as possible. It keeps the rounded, soft aesthetic you already know but refines the details: denser surfaces, ring-based focus states, and subtle press interactions on buttons. Built for focused product interfaces.
Rhea is opt-in. The default style stays the same, so nothing changes for your existing projects. Head to the customizer and pick Rhea from the style switcher to preview it, then copy the theme into your app.
Chat interface components
We're also releasing a new set of components for building chat interfaces: Message Scroller, Message, Bubble, Attachment, and Marker. Plus two new CSS utilities to round them out.
- Message Scroller: The scroll container for a conversation.
- Message: Lay out a row in the conversation.
- Bubble: Render the message surface.
- Attachment: Render files and images.
- Marker: Render status updates and separators.
Message Scroller
The scroll container for a conversation. It handles the parts that are easy to get wrong: anchored turns, streamed replies, saved thread restore, prepended history, jump-to-message, scroll controls, and visibility tracking.
::component-preview
name: MessageScrollerStreamingDemo class: "style-luma rounded-[34px] sm:rounded-4xl" previewClass: h-auto theme-green bg-surface dark:bg-background p-4 min-[480px]:p-8 min-[560px]:p-10 sm:px-10 sm:py-16
::
Message
Message lays out a row in the conversation with avatar, alignment, header, content, footer, and grouped messages.
::component-preview
name: MessageDemo previewClass: h-auto theme-blue
::
Bubble
Bubble renders the message surface, with variants, alignment, reactions, links, buttons, and collapsible content.
::component-preview
name: BubbleVariantsDemo previewClass: h-auto theme-blue
::
Attachment
Attachment renders files and images with media, metadata, upload state, actions, and a full-card trigger.
::component-preview
name: AttachmentDemo previewClass: h-auto theme-blue bg-surface dark:bg-background
::
Marker
Marker renders status updates, system notes, bordered rows, and labeled separators.
::component-preview
name: MarkerDemo class: style-luma previewClass: h-auto theme-blue
::
scroll-fade and shimmer
Two CSS utilities to finish off the set. scroll-fade dissolves content at the edges of a scroll container, and shimmer adds a shimmer effect to text, perfect for streaming and loading states. Both ship with the shadcn-vue package, so there's nothing to install after init.
::component-preview
name: ScrollFadeDemo previewClass: h-auto
::
November 2025 - Catch up
We've upgraded shadcn-vue.com to Nuxt v4 and Tailwind v4. The site now uses the upgraded new-york components.
We've also made some minor design updates to make the site faster and easier to navigate.
Chart
We also refactored the Chart component to match the original component, and stick to the API as close as possible.
October 2025 - New Components
For this round of components, I looked at what we build every day, the boring stuff we rebuild over and over, and made reusable abstractions you can actually use.
- Spinner: An indicator to show a loading state.
- Kbd: Display a keyboard key or group of keys.
- Button Group: A group of buttons for actions and split buttons.
- Input Group: Input with icons, buttons, labels and more.
- Field: One component. All your forms.
- Item: Display lists of items, cards, and more.
- Empty: Use this one for empty states.
Spinner
Okay let's start with the easiest ones: Spinner and Kbd. Pretty basic. We all know what they do.
Here's how you render a spinner:
<script setup lang="ts">
import { Spinner } from '@/components/ui/spinner'
</script>
<template>
<Spinner />
</template>Here's what it looks like:
::component-preview
name: SpinnerBasic class: "[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
::
Here's what it looks like in a button:
::component-preview
name: SpinnerButtonDemo class: "[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
::
You can edit the code and replace it with your own spinner.
::component-preview
name: SpinnerCustomDemo class: "[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
::
Kbd
Kbd is a component that renders a keyboard key.
<script setup lang="ts">
import { Kbd, KbdGroup } from '@/components/ui/kbd'
</script>
<template>
<Kbd>Ctrl</Kbd>
</template>Use KbdGroup to group keyboard keys together.
<template>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>B</Kbd>
</KbdGroup>
</template>::component-preview
name: KbdDemo class: "[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
::
You can add it to buttons, tooltips, input groups, and more.
Button Group
I got a lot of requests for this one: Button Group. It's a container that groups related buttons together with consistent styling. Great for action groups, split buttons, and more.
::component-preview
name: ButtonGroupDemo class: "[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
::
Here's the code:
<script setup lang="ts">
import { ButtonGroup } from '@/components/ui/button-group'
</script>
<template>
Button 1
Button 2
</template>You can nest button groups to create more complex layouts with spacing.
<template>
Button 1
Button 2
Button 3
Button 4
</template>Use ButtonGroupSeparator to create split buttons. Classic dropdown pattern.
::component-preview
name: ButtonGroupWithDropdownMenuDemo class: "[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
::
You can also use it to add prefix or suffix buttons and text to inputs.
::component-preview
name: ButtonGroupWithSelectDemo class: "[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
::
<template>
Prefix
Button
</template>Input Group
Input Group lets you add icons, buttons, and more to your inputs. You know, all those little bits you always need around your inputs.
<script setup lang="ts">
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group'
</script>
<template>
<SearchIcon />
</template>Here's a preview with icons:
::component-preview
name: InputGroupWithIcon class: "[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
::
You can also add buttons to the input group.
::component-preview
name: InputGroupWithButton class: "[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
::
Or text, labels, tooltips,...
::component-preview
name: InputGroupWithText class: "[&_.preview]:h-[400px] [&_pre]:!h-[400px]"
::
It also works with textareas so you can build really complex components with lots of knobs and dials or yet another prompt form.
::component-preview
name: InputGroupWithTextarea class: "[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
::
Oh here are some cool ones with spinners:
::component-preview
name: InputGroupWithSpinner class: "[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
::
Field
Introducing Field, a component for building really complex forms. The abstraction here is beautiful.
It took me a long time to get it right but I made it work with all your form libraries: Vee Validate, TanStack Form, Bring Your Own Form.
Here's a basic field with an input:
<script setup lang="ts">
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from '@/components/ui/field'
</script>
<template>
<Field>
<FieldLabel html-for="username">
Username
</FieldLabel>
<FieldDescription>
Choose a unique username for your account.
</FieldDescription>
</Field>
</template>::component-preview
name: FieldInputDemo class: "[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
::
It works with all form controls. Inputs, textareas, selects, checkboxes, radios, switches, sliders, you name it. Here's a full example:
::component-preview
name: FieldDemo class: "[&_.preview]:h-[850px] [&_pre]:!h-[850px]"
::
Here are some checkbox fields:
::component-preview
name: FieldCheckboxDemo class: "[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
::
You can group fields together using FieldGroup and FieldSet. Perfect for
multi-section forms.
<template>
<FieldSet>
<FieldLegend />
<FieldGroup>
<Field />
<Field />
</FieldGroup>
</FieldSet>
</template>::component-preview
name: FieldFieldsetDemo class: "[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
::
Making it responsive is easy. Use orientation="responsive" and it switches
between vertical and horizontal layouts based on container width. Done.
::component-preview
name: FieldResponsiveDemo class: "[&_pre]:!h-[600px]" previewClass: h-[700px] sm:h-[600px]
::
Wait here's more. Wrap your fields in FieldLabel to create a selectable field group. Really easy. And it looks great.
::component-preview
name: FieldChoiceCardDemo class: "[&_.preview]:h-[600px] [&_pre]:!h-[600px]"
::
Item
This one is a straightforward flex container that can house nearly any type of content.
I've built this so many times that I decided to create a component for it. Now I use it all the time. I use it to display lists of items, cards, and more.
Here's a basic item:
<script setup lang="ts">
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from '@/components/ui/item'
</script>
<template>
<HomeIcon />
Dashboard
Overview of your account and activity.
</template>::component-preview
name: ItemDemo class: "[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
::
You can add icons, avatars, or images to the item.
::component-preview
name: ItemIconDemo class: "[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
::
::component-preview
name: ItemAvatarDemo class: "[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
::
And here's what a list of items looks like with ItemGroup:
::component-preview
name: ItemGroupDemo class: "[&_.preview]:h-[500px] [&_.preview]:p-4 [&_pre]:!h-[500px]"
::
Need it as a link? Use the asChild prop:
<template>
<a href="/dashboard">
<HomeIcon />
Dashboard
Overview of your account and activity.
</a>
</template>::component-preview
name: ItemLinkDemo class: "[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
::
Empty
Okay last one: Empty. Use this to display empty states in your app.
Here's how you use it:
<script setup lang="ts">
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
</script>
<template>
No messages
You don't have any messages yet.
Send a message
</template>::component-preview
name: EmptyDemo class: "[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
::
You can use it with avatars:
::component-preview
name: EmptyAvatarDemo class: "[&_.preview]:h-[400px] [&_pre]:!h-[400px]"
::
Or with input groups for things like search results or email subscriptions:
::component-preview
name: EmptyInputGroupDemo class: "[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
::
That's it. Seven new components. Works with all your libraries. Ready for your projects.
February 2025 - Reka UI & npx shadcn-vue@latest init
We've updated the latest registry to support Reka UI instead of Radix Vue.
The updated CLI is now available. You can now install components, themes, composables, utils and more using npx shadcn-vue add.
This is a major step towards distributing code that you and your LLMs can access and use.
::callout
With the released of Reka UI v2, shadcn-vue@latest command will now install Reka UI. If you want to keep using Radix Vue, please visit here and run shadcn-vue@radix command instead.
::
First up, when you init into a new app, we update your existing Tailwind files instead of overriding.
A component now ship its own dependencies. Take the accordion for example, it can define its Tailwind keyframes. When you add it to your project, weβll update your tailwind.config.ts file accordingly.
You can also install remote components using url.
npx shadcn-vue add https://acme.com/registry/navbar.json.We have created a new schema that you can use to ship your own component registry. And since it has support for urls, you can even use it to distribute private components.
And a few more updates like better error handling and monorepo support.
You can try the new cli today.
npx shadcn-vue@latest init Sidebar01 Login01Apps V4 Content Docs 11.Legacy (`apps/v4/content/docs/11.legacy.md`)
title: Legacy Docs description: View the legacy docs for shadcn-vue and Tailwind v3.
You're looking at the docs for shadcn-vue + Tailwind v4. If you're looking for the docs for shadcn/ui + Tailwind v3, you can find them here.
Apps V4 Content Docs Forms (`apps/v4/content/docs/forms.md`)
title: Forms description: Build forms with Vue and shadcn-vue.
Pick Your Framework
Start by selecting your framework. Then follow the instructions to learn how to build forms with shadcn-vue and the form library of your choice.
VeeValidate
TanStack Form
Formisch
Composable
(Coming Soon)
Apps V4 Content Docs Mcp (`apps/v4/content/docs/mcp.md`)
title: MCP Server description: Use the shadcn MCP server to browse, search, and install components from registries.
The shadcn MCP Server allows AI assistants to interact with items from registries. You can browse available components, search for specific ones, and install them directly into your project using natural language.
For example, you can ask an AI assistant to "Build a landing page using components from the acme registry" or "Find me a login form from the shadcn registry".
Registries are configured in your project's components.json file.
{
"registries": {
"@acme": "https://acme.com/r/{name}.json"
}
}Quick Start
Select your MCP client and follow the instructions to configure the shadcn MCP server. If you'd like to do it manually, see the Configuration section.
::::tabs{default-value="claude" class="mt-4"}
:::tabs-list
::tabs-trigger{value="claude"}
Claude Code
::
::tabs-trigger{value="cursor"}
Cursor
::
::tabs-trigger{value="vscode"}
VS Code
::
::tabs-trigger{value="codex"}
Codex
::
::tabs-trigger{value="opencode"}
Opencode
:::::
::tabs-content{value="claude"}
Run the following command in your project:
npx shadcn-vue@latest mcp init --client claudeRestart Claude Code and try the following prompts:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
Note: You can use /mcp command in Claude Code to debug the MCP server.
::
::tabs-content{value="cursor"}
Run the following command in your project:
npx shadcn-vue@latest mcp init --client cursorOpen Cursor Settings and Enable the MCP server for shadcn. Then try the following prompts:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
::
::tabs-content{value="vscode"}
Run the following command in your project:
npx shadcn-vue@latest mcp init --client vscodeOpen .vscode/mcp.json and click Start next to the shadcn server. Then try the following prompts with GitHub Copilot:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
::
:::tabs-content{value="codex"}
::callout{type="warning"}
Note: The shadcn-vue CLI cannot automatically update ~/.codex/config.toml. You'll need to add the configuration manually for Codex.
::
Run the following command in your project:
npx shadcn-vue@latest mcp init --client codexThen, add the following configuration to ~/.codex/config.toml:
[mcp_servers.shadcn]
command = "npx"
args = ["shadcn-vue@latest", "mcp"]Restart Codex and try the following prompts:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
:::
::tabs-content{value="opencode"}
You need to make sure opencode is installed.
To install check Opencode Documentation
Run the following command in your project:
npx shadcn-vue@latest mcp init --client opencodeRestart opencode and try the following prompts:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
Note: You can use Ctrl+x s to see the status of the mcp servers.
::
::::
What is MCP?
Model Context Protocol (MCP) is an open protocol that enables AI assistants to securely connect to external data sources and tools. With the shadcn MCP server, your AI assistant gains direct access to:
- Browse Components - List all available components, blocks, and templates from any configured registry
- Search Across Registries - Find specific components by name or functionality across multiple sources
- Install with Natural Language - Add components using simple conversational prompts like "add a login form"
- Support for Multiple Registries - Access public registries, private company libraries, and third-party sources
How It Works
The MCP server acts as a bridge between your AI assistant, component registries and the shadcn CLI.
- Registry Connection - MCP connects to configured registries (shadcn/ui, private registries, third-party sources)
- Natural Language - You describe what you need in plain English
- AI Processing - The assistant translates your request into registry commands
- Component Delivery - Resources are fetched and installed in your project
Supported Registries
The shadcn MCP server works out of the box with any shadcn-compatible registry.
- shadcn/ui Registry - The default registry with all shadcn/ui components
- Third-Party Registries - Any registry following the shadcn registry specification
- Private Registries - Your company's internal component libraries
- Namespaced Registries - Multiple registries configured with
@namespacesyntax
Configuration
You can use any MCP client to interact with the shadcn MCP server. Here are the instructions for the most popular ones.
Claude Code
To use the shadcn MCP server with Claude Code, add the following configuration to your project's .mcp.json file:
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn-vue@latest", "mcp"]
}
}
}After adding the configuration, restart Claude Code and run /mcp to see the shadcn MCP server in the list. If you see Connected, you're good to go.
See the Claude Code MCP documentation for more details.
Cursor
To configure MCP in Cursor, add the shadcn server to your project's .cursor/mcp.json configuration file:
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn-vue@latest", "mcp"]
}
}
}After adding the configuration, enable the shadcn MCP server in Cursor Settings.
Once enabled, you should see a green dot next to the shadcn server in the MCP server list and a list of available tools.
See the Cursor MCP documentation for more details.
VS Code
To configure MCP in VS Code with GitHub Copilot, add the shadcn server to your project's .vscode/mcp.json configuration file:
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn-vue@latest", "mcp"]
}
}
}After adding the configuration, open .vscode/mcp.json and click Start next to the shadcn server.
See the VS Code MCP documentation for more details.
Codex
::callout{type="warning"}
Note: The shadcn-vue CLI cannot automatically update ~/.codex/config.toml. You'll need to add the configuration manually.
::
To configure MCP in Codex, add the shadcn server to ~/.codex/config.toml:
[mcp_servers.shadcn]
command = "npx"
args = ["shadcn-vue@latest", "mcp"]After adding the configuration, restart Codex to load the MCP server.
Opencode
To configure MCP in opencode add the shadcn server to opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"shadcnVue": {
"type": "local",
"enabled": true,
"command": ["npx", "shadcn-vue@latest", "mcp"]
}
}
}See the Opencode Documentation for more details.
Configuring Registries
The MCP server supports multiple registries through your project's components.json configuration. This allows you to access components from various sources including private registries and third-party providers.
Configure additional registries in your components.json:
{
"registries": {
"@acme": "https://registry.acme.com/{name}.json",
"@internal": {
"url": "https://internal.company.com/{name}.json",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}"
}
}
}
}::callout{type="info"}
Note: No configuration is needed to access the standard shadcn/ui registry.
::
Authentication
For private registries requiring authentication, set environment variables in your .env.local:
REGISTRY_TOKEN=your_token_here
API_KEY=your_api_key_hereFor more details on registry authentication, see the Authentication documentation.
Example Prompts
Once the MCP server is configured, you can use natural language to interact with registries. Try one of the following prompts:
Browse & Search
- Show me all available components in the shadcn registry
- Find me a login form from the shadcn registry
Install Items
- Add the button component to my project
- Create a login form using shadcn components
- Install the Cursor rules from the acme registry
Work with Namespaces
- Show me components from acme registry
- Install @internal/auth-form
- Build me a landing page using hero, features and testimonials sections from the acme registry
Troubleshooting
MCP Not Responding
If the MCP server isn't responding to prompts:
- Check Configuration - Verify the MCP server is properly configured and enabled in your MCP client
- Restart MCP Client - Restart your MCP client after configuration changes
- Verify Installation - Ensure
shadcn-vueis installed in your project - Check Network - Confirm you can access the configured registries
Registry Access Issues
If components aren't loading from registries:
- Check components.json - Verify registry URLs are correct
- Test Authentication - Ensure environment variables are set for private registries
- Verify Registry - Confirm the registry is online and accessible
- Check Namespace - Ensure namespace syntax is correct (
@namespace/component)
Installation Failures
If components fail to install:
- Check Project Setup - Ensure you have a valid
components.jsonfile - Verify Paths - Confirm the target directories exist
- Check Permissions - Ensure write permissions for component directories
- Review Dependencies - Check that required dependencies are installed
No Tools or Prompts
If you see the No tools or prompts message, try the following:
- Clear the npx cache - Run
npx clear-npx-cache - Re-enable the MCP server - Try to re-enable the MCP server in your MCP client
- Check Logs - In Cursor, you can see the logs under View -> Output and select
MCP: project-*in the dropdown.
Learn More
- Registry Documentation - Complete guide to shadcn registries
- Namespaces - Configure multiple registry sources
- Authentication - Secure your private registries
- MCP Specification - Learn about Model Context Protocol
Apps V4 Content Docs Skills (`apps/v4/content/docs/skills.md`)
title: Skills description: Give your AI assistant deep knowledge of shadcn-vue components, patterns, and best practices.
Skills give AI assistants like Claude Code project-aware context about shadcn-vue. When installed, your AI assistant knows how to find, install, compose, and customize components using the correct APIs and patterns for your project.
For example, you can ask your AI assistant to:
- "Add a login form with email and password fields."
- "Create a settings page with a form for updating profile information."
- "Build a dashboard with a sidebar, stats cards, and a data table."
- "Switch to --preset [CODE]"
The skill reads your project's components.json and provides the assistant with your framework, aliases, installed components, icon library, and base library so it can generate correct code on the first try.
Install
npx skills add unovue/shadcn-vueThis installs the shadcn-vue skill into your project. Once installed, your AI assistant automatically loads it when working with shadcn-vue components.
Learn more about skills at skills.sh.
What's Included
The skill provides your AI assistant with the following knowledge:
Project Context
On every interaction, the skill runs shadcn-vue info --json to get your project's configuration: framework, Tailwind version, aliases, base library (reka), icon library, installed components, and resolved file paths.
CLI Commands
Full reference for all CLI commands: init, add, search, view, docs, diff, info, and build. Includes flags, smart merge workflows, presets, and templates.
Theming and Customization
How CSS variables, OKLCH colors, dark mode, custom colors, border radius, and component variants work. Includes guidance for both Tailwind v3 and v4.
Registry Authoring
How to build and publish custom component registries: registry.json format, item types, file objects, dependencies, CSS variables, building, hosting, and user configuration.
MCP Server
Setup and tools for the shadcn-vue MCP server, which lets AI assistants search, browse, and install components from registries.
How It Works
- Project detection β The skill activates when it finds a
components.jsonfile in your project. - Context injection β It runs
shadcn-vue info --jsonto read your project configuration and injects the result into the assistant's context. - Pattern enforcement β The assistant follows shadcn-vue composition rules: using
FieldGroupfor forms,ToggleGroupfor option sets, semantic colors, and correct base-specific APIs. - Component discovery β The assistant uses
shadcn-vue docs,shadcn-vue search, or MCP tools to find components and their documentation before generating code.
Learn More
- CLI β Full CLI command reference
- MCP Server β Connect the MCP server for registry access
- Theming β CSS variables and customization
- Registry β Building and publishing custom registries
- skills.sh β Learn more about AI skills
Apps V4 Content Docs Typeset (`apps/v4/content/docs/typeset.md`)
title: Typeset description: A styling system for HTML and rendered markdown, from blog posts to streaming chat. One CSS file you own.
You render markdown and get back plain unstyled HTML: headings, paragraphs, lists, and tables. So you style the elements one by one: font sizes, line heights, spacing.
You do it for your blog. Then you do it again for the docs. Then again for the chat app. Every time you're fighting the same thing: sizing and spacing.
To fix this, we ported shadcn/typeset. It's one CSS file that styles everything inside a typeset container. The file lives in your project, so you can change it directly when you need to.
A typeset is just a small preset class. You can have multiple typesets in your app, for different contexts.
.typeset-docs {
--typeset-font-body: var(--font-geist-sans);
--typeset-font-heading: var(--font-geist-sans);
--typeset-font-mono: var(--font-geist-mono);
--typeset-size: 15px;
--typeset-leading: 1.75;
--typeset-flow: 1.25em;
}Principles
We read a lot about type: scale ratios, tracking, kerning, optical sizing, measure, leading, the space above and below every element. We tried exposing all of it, and it was too much. Nobody wants to set a dozen variables to make markdown look right.
So we sat down and condensed everything into three controls: size, leading, and flow. Everything else, heading sizes, list indents, the gap under a heading, the space around a rule, derives from them. Three controls. We called it rhythm.
Features
- It fits its container. Put it in a chat bubble and it follows the smaller type around it. Put it in an article and it scales up with the page. On smaller screens, it gets a small bump for readability.
- It uses your theme. Colors, fonts, and radius come from your app. Dark mode follows the same tokens.
- It's easy to tune. Three values control the base size, line height, and space between blocks. Change them in a preset and the whole document follows.
- It works well with streaming. When a new block arrives, Typeset doesn't make earlier blocks switch margins, borders, or styles.
Building Your Typeset
Create your typeset in the typeset builder. Pick your fonts and rhythm, then preview them on docs, chat, articles, and other real content.
The panel gives you the typeset.css file, the font setup for your framework, a preset class with your choices, and the wrapper to add around your content.
Copy typeset.css next to your main CSS file and import it after Tailwind:
@import "tailwindcss";
@import "./typeset.css";Then wrap your rendered markdown with typeset and your preset class:
<template>
<ContentRenderer :value="page" />
</template>typeset turns the styles on. typeset-docs is the preset you created in the builder.
Custom Typesets
The file includes defaults, so you can use typeset by itself. Most of the reading rhythm comes from three values:
.typeset {
--typeset-font-body: inherit;
--typeset-font-heading: var(--font-heading);
--typeset-font-mono: var(--font-mono);
--typeset-size: 1em; /* body font-size */
--typeset-leading: 1.75; /* line-height */
--typeset-flow: 1.25em; /* space between blocks */
}--typeset-sizesets the base text size.1emfollows the surrounding layout. On smaller screens, Typeset bumps it up a little.--typeset-leadingsets the space between lines.--typeset-flowsets the space between blocks. Headings and other elements derive their spacing from it.
The font variables tell Typeset which families to use. Leave them alone and it follows your app. Colors and radius come from your theme too.
Typeset doesn't set a maximum width. Your layout owns that. The Measure control in the builder adds a max-width to the wrapper instead of hiding it in the stylesheet.
You can keep more than one preset in the same app. Here is a tighter one for chat and a roomier one for docs:
.typeset-chat {
--typeset-flow: 1em;
--typeset-leading: 1.6;
}
.typeset-docs {
--typeset-size: 15px;
--typeset-flow: 1.5em;
}<template>
<article class="typeset typeset-docs" v-html="page" />
</template>For a one-off change, skip the preset and set a value on the container:
<template>
<article class="typeset [--typeset-flow:1.75em]">...</article>
</template>Custom Themes
A preset can change the whole feel of the content, not just the spacing. You can give readers a serif reading mode, a compact UI mode, or any other style that fits your product.
/* Reading: serif, larger type, roomy rhythm. */
.typeset-reading {
--typeset-font-body: var(--font-lora);
--typeset-font-heading: var(--font-lora);
--typeset-size: 18px;
--typeset-leading: 1.9;
--typeset-flow: 2em;
}
/* Compact: sans, smaller type, tighter rhythm. */
.typeset-compact {
--typeset-font-body: var(--font-geist-sans);
--typeset-font-heading: var(--font-geist-sans);
--typeset-size: 14px;
--typeset-leading: 1.6;
--typeset-flow: 1em;
}Accessibility and Dark Mode
For readers who prefer larger type and more space, create a roomier typeset and expose it as a setting:
.typeset-large {
--typeset-size: 16px;
--typeset-leading: 2;
--typeset-flow: 2em;
}Dark mode already follows your theme colors. If the text feels a little tight on a dark surface, you can loosen the leading there:
.dark .typeset {
--typeset-leading: 1.9;
}Responsive Table
Tables stay real tables and wrap to fit. To scroll a wide one horizontally instead, wrap it in typeset-scroll. The wrapper is styled as part of the content, so it only works inside a typeset container:
<template>
<article class="typeset typeset-docs">
</article>
</template>Do this in your renderer's table component or a small rehype plugin, where the output already sits inside the container. It works for any wide block, not just tables.
Overrides
Typeset lives in the components layer and uses :where() for its element selectors. Tailwind utilities on an element win without !important:
<template>
...
</template>Plain CSS can override Typeset with a normal selector too.
Opting Out
To keep a component out of Typeset, add not-typeset or data-not-typeset:
<template>
Styled prose.
<Card class="not-typeset">
Untouched component.
</Card>
</template>Both options cover the component and everything inside it. Another typeset container inside that subtree stays opted out too.
Streaming
Typeset is written so that adding a new block does not change the styles of the blocks already on screen.
- No forward-looking selectors.
:last-child,:has(), and:emptyare left out of layout rules because their matches can change as content is added. - Spacing flows in one direction, using
margin-block-startonly. A new block adds its own space. - Table separators live on the cells being added, so a new row does not restyle the row above it.
Text that is still streaming can grow and wrap normally. Typeset just avoids restyling the blocks that came before it.
Prior Art
The prose class from @tailwindcss/typography is excellent at what it was built for: adding beautiful typographic defaults to plain HTML, including content rendered from Markdown or a CMS.
Typeset takes a different approach with container-aware sizing, app theme tokens, presets for different contexts, and streaming stability. Here's where they differ:
| @tailwindcss/typography | Typeset | |
|---|---|---|
| Sizing | Fixed rem scale, prose-sm to prose-2xl |
Relative to the container, any size |
| Dark mode | prose-invert, a second palette |
Your tokens flip, nothing to add |
| Theming | Prose color variables; scale baked in | Your theme tokens, plus font and rhythm controls |
| Overrides | prose-a:, prose-headings: modifier API |
Plain utilities and CSS win |
| Streaming | No append-stability contract | Designed for stable appends |
| Distribution | npm plugin, generated CSS | One CSS file you own |
Typeset borrows the two best ideas from the plugin: the zero-specificity :where() guard pattern, and the escape-hatch class (not-typeset, in the spirit of not-prose).
Apps V4 Content Docs Components Accordion (`apps/v4/content/docs/components/accordion.md`)
title: Accordion description: A vertically stacked set of interactive headings that each reveal a section of content. component: true links: doc: https://reka-ui.com/docs/components/accordion api: https://reka-ui.com/docs/components/accordion#api-reference
::component-preview
name: AccordionDemo class: '[&_.preview>div]:sm:max-w-[80%]' description: An accordion with three items align: start previewClass: h-[27rem] sm:h-[25rem]
::
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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/accordion) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<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>Apps V4 Content Docs Components Alert Dialog (`apps/v4/content/docs/components/alert-dialog.md`)
title: Alert Dialog description: A modal dialog that interrupts the user with important content and expects a response. component: true links: doc: https://reka-ui.com/docs/components/alert-dialog api: https://reka-ui.com/docs/components/alert-dialog#api-reference
::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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/alert-dialog) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<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>Apps V4 Content Docs Components Alert (`apps/v4/content/docs/components/alert.md`)
title: Alert description: Displays a callout for user attention. component: true
::component-preview
name: AlertDemo title: An alert with an icon, title and description. description: An alert with an icon, title and description. previewClass: h-auto sm:h-[22rem] p-6
::
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
<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>Apps V4 Content Docs Components Aspect Ratio (`apps/v4/content/docs/components/aspect-ratio.md`)
title: Aspect Ratio description: Displays content within a desired ratio. component: true links: doc: https://reka-ui.com/docs/components/aspect-ratio api: https://reka-ui.com/docs/components/aspect-ratio#api-reference
::component-preview
name: AspectRatioDemo description: A component that displays an image with a 16:9 aspect ratio. previewClass: h-[26rem]
::
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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/aspect-ratio) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script lang="ts">
import { AspectRatio } from '@/components/ui/aspect-ratio'
</script>
<template>
<AspectRatio :ratio="16 / 9">
</AspectRatio>
</template>Apps V4 Content Docs Components Attachment (`apps/v4/content/docs/components/attachment.md`)
title: Attachment description: Displays a file or image attachment with media, metadata, upload state, and actions. component: true
::component-preview
name: AttachmentDemo previewClass: h-auto theme-blue bg-surface dark:bg-background
::
The Attachment component displays a file or image attachment, its media, name, and metadata, with optional actions and upload state. Use it for files and images in chat composers, message threads, and upload lists.
Installation
:::::code-tabs
:::tabs-list
::tabs-trigger{value="cli"}
CLI
::
::tabs-trigger{value="manual"}
Manual
::
:::
::tabs-content{value="cli"}
npx shadcn-vue@latest add attachment::
::::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
<script setup lang="ts">
import { FileTextIcon, XIcon } from '@lucide/vue'
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@/components/ui/attachment'
</script>
<template>
<Attachment>
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
<AttachmentDescription>PDF Β· 2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove sales-dashboard.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</template>Composition
Use the following composition to build an attachment:
Attachment
βββ AttachmentMedia
βββ AttachmentContent
β βββ AttachmentTitle
β βββ AttachmentDescription
βββ AttachmentActions
β βββ AttachmentAction
βββ AttachmentTriggerUse AttachmentGroup to lay out multiple attachments in a scrollable row:
AttachmentGroup
βββ Attachment
βββ AttachmentFeatures
- Icon and image media through
AttachmentMedia - Upload states:
idle,uploading,processing,error, anddonewith built-in styling and a shimmer while in progress - Three sizes and horizontal or vertical orientation
- A full-card
AttachmentTriggerthat opens a link or dialog while the actions stay independently clickable - Scrollable, snapping
AttachmentGroupwith an edge fade - Customizable styling through the Vue
classattribute on every part
Examples
Image
Set variant="image" on AttachmentMedia and render an <img> inside it. Use orientation="vertical" to stack the media above the content.
::component-preview
name: AttachmentImageDemo previewClass: h-auto theme-blue bg-surface dark:bg-background
::
States
Set state to reflect the upload lifecycle. uploading and processing shimmer the title, and error switches to a destructive treatment.
::component-preview
name: AttachmentStatesDemo previewClass: h-auto theme-blue bg-surface dark:bg-background
::
Sizes
Use size to switch between default, sm, and xs.
::component-preview
name: AttachmentSizesDemo previewClass: h-auto theme-blue bg-surface dark:bg-background
::
Group
Wrap attachments in AttachmentGroup to lay them out in a horizontally scrollable, snapping row with an edge fade.
::component-preview
name: AttachmentGroupDemo previewClass: h-auto theme-blue bg-surface dark:bg-background
::
Trigger
Add an AttachmentTrigger to make the whole card open a link or dialog. It fills the card behind the actions, so the actions stay clickable.
::component-preview
name: AttachmentTriggerDemo previewClass: h-auto theme-blue bg-surface dark:bg-background
::
<template>
<Dialog>
<Attachment>
<DialogTrigger as-child>
<AttachmentTrigger aria-label="Preview research-summary.pdf" />
</DialogTrigger>
</Attachment>
<DialogContent>
</DialogContent>
</Dialog>
</template>Accessibility
AttachmentAction renders a Button, and AttachmentTrigger renders a real <button> (or your element via as-child). Follow the guidance below so both are operable and announced.
Label icon-only actions
AttachmentAction is usually icon-only, so give each one an aria-label describing the action and its target.
<template>
<AttachmentAction aria-label="Remove sales-dashboard.pdf">
<XIcon />
</AttachmentAction>
</template>Label the trigger
AttachmentTrigger covers the card with no text of its own, so give it an aria-label for what activating it does.
<template>
<AttachmentTrigger as-child>
<a
:href="url"
target="_blank"
rel="noreferrer"
aria-label="Open workspace.png"
/>
</AttachmentTrigger>
</template>The trigger sits behind the actions in the stacking order, so an AttachmentAction and the AttachmentTrigger never trap each other β both remain separately focusable and clickable.
Keyboard scrolling
An AttachmentGroup scrolls horizontally. When its attachments are interactive: a trigger or actions, keyboard users reach off-screen items by tabbing to them. For a row of presentational attachments, make the group itself focusable and scrollable by adding tabindex="0", role="group", and an aria-label.
Meaning beyond color
The error state uses a destructive color. Keep the failure reason in AttachmentDescription so the state is not conveyed by color alone.
API Reference
Attachment
The root attachment container.
| Prop | Type | Default | Description |
|---|---|---|---|
state |
"idle" | "uploading" | "processing" | "error" | "done" |
"done" |
The upload state. Drives styling and the shimmer. |
size |
"default" | "sm" | "xs" |
"default" |
The attachment size. |
orientation |
"horizontal" | "vertical" |
"horizontal" |
Lay the media beside or above the content. |
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the root element. |
AttachmentMedia
The media slot for an icon or image preview.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"icon" | "image" |
"icon" |
Whether the media holds an icon or an <img>. |
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the media slot. |
AttachmentContent
Wraps the title and description.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the content slot. |
AttachmentTitle
The attachment name. Shimmers while the attachment is uploading or processing.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the title. |
AttachmentDescription
Secondary metadata such as the file type, size, or upload status.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the description. |
AttachmentActions
A container for one or more actions, aligned to the end of the attachment.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the actions. |
AttachmentAction
An action button. Renders a Button and accepts Vue fallthrough attributes such as aria-label.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
ButtonVariants["variant"] |
"ghost" |
The button variant. |
size |
ButtonVariants["size"] |
"icon-xs" |
The button size. |
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the action. |
AttachmentTrigger
A full-card overlay that activates the attachment. Renders a <button> by default and accepts Vue fallthrough attributes such as aria-label.
| Prop | Type | Default | Description |
|---|---|---|---|
as |
PrimitiveProps["as"] |
"button" |
Element or component to render. |
as-child |
boolean |
false |
Render as the child element, such as a link. |
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the trigger. |
AttachmentGroup
Lays out attachments in a horizontally scrollable, snapping row.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
HTMLAttributes["class"] |
- | Additional classes to apply to the group. |
Apps V4 Content Docs Components Avatar (`apps/v4/content/docs/components/avatar.md`)
title: Avatar description: An image element with a fallback for representing the user. component: true links: doc: https://reka-ui.com/docs/components/avatar api: https://reka-ui.com/docs/components/avatar#api-reference
::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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/avatar) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<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>Apps V4 Content Docs Components Badge (`apps/v4/content/docs/components/badge.md`)
title: Badge description: Displays a badge or a component that looks like a badge. component: true
::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
<script setup lang="ts">
import { Badge } from '@/components/ui/badge'
</script>
<template>
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](#)
</template>Apps V4 Content Docs Components Breadcrumb (`apps/v4/content/docs/components/breadcrumb.md`)
title: Breadcrumb description: Displays the path to the current resource using a hierarchy of links. component: true
::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
<script setup lang="ts">
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
</script>
<template>
Home
Components
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>
Home
<SlashIcon />
Components
</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>
<DropdownMenu>
<DropdownMenuTrigger>
Components
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>Documentation</DropdownMenuItem>
<DropdownMenuItem>Themes</DropdownMenuItem>
<DropdownMenuItem>GitHub</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</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>
</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>
<NuxtLink to="/">
Home
</NuxtLink>
{/* ... */}
</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
::
Apps V4 Content Docs Components Bubble (`apps/v4/content/docs/components/bubble.md`)
title: Bubble description: Displays conversational content in a message bubble. Supports variants, alignment, grouping, reactions, and collapsible content. component: true
::component-preview
name: BubbleDemo previewClass: h-auto theme-blue
::
The Bubble component displays framed conversational content. Use it for chat text, short structured output, quoted replies, suggestions, and reactions.
For full-featured chat interfaces, use the Message component. Bubble is intentionally scoped to the bubble surface. Place avatars, names, timestamps, metadata, and message-level actions in Message.
Installation
:::::code-tabs
:::tabs-list
::tabs-trigger{value="cli"}
CLI
::
::tabs-trigger{value="manual"}
Manual
::
:::
::tabs-content{value="cli"}
npx shadcn-vue@latest add bubble::
::::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
<script setup lang="ts">
import { Bubble, BubbleContent, BubbleReactions } from '@/components/ui/bubble'
</script>
<template>
I checked the registry output and removed the stale route.
π
</template>Composition
Use the following composition to build a bubble:
Bubble
βββ BubbleContent
βββ BubbleReactionsUse BubbleGroup to group consecutive bubbles from the same sender:
BubbleGroup
βββ Bubble
β βββ BubbleContent
βββ Bubble
βββ BubbleContentFeatures
- Seven visual variants, from a strong primary bubble to unframed ghost content
- Start and end alignment for sender and receiver bubbles
- Reactions that anchor to the bubble edge with configurable side and alignment
- Bubbles size to their content, up to 80% of the container width
- Polymorphic content via
asoras-childfor link and button bubbles - Customizable styling through the
classprop on every part
Examples
Variants
Use variant to change the visual treatment of the bubble.
::component-preview
name: BubbleVariantsDemo previewClass: h-auto theme-blue
::
| Variant | Description |
|---|---|
default |
A strong primary bubble, usually for the current user. |
secondary |
The standard neutral bubble for conversation content. |
muted |
A lower-emphasis bubble for quiet supporting content. |
tinted |
A subtle primary-tinted bubble. |
outline |
A bordered bubble for secondary or rich content. |
ghost |
Unframed content for assistant text or rich content. |
destructive |
A destructive bubble for error or failed actions. |
A bubble sizes to its content, up to 80% of the container width. The ghost variant removes the max-width so assistant text and rich content can span the full row.
Alignment
Use align on Bubble to align the bubble to the start or end of the conversation.
::component-preview
name: BubbleAlignmentDemo previewClass: h-auto theme-blue
::
| align | Description |
|---|---|
start |
Align the bubble to the start of the conversation. |
end |
Align the bubble to the end of the conversation. |
Note: When building chat interfaces, you probably want to set align on the Message component itself. Bubbles inside MessageContent automatically follow the message alignment.
Bubble Group
Use BubbleGroup to group consecutive bubbles from the same sender. Note the align prop should be set on the Bubble component itself, not the BubbleGroup component.
BubbleGroup
βββ Bubble
β βββ BubbleContent
βββ Bubble
βββ BubbleContent::component-preview
name: BubbleGroupDemo previewClass: h-auto theme-blue
::
Links and Buttons
Use as-child to merge BubbleContent styling and attributes onto a link or button passed through its default slot.
::component-preview
name: BubbleLinksAndButtonsDemo previewClass: h-auto theme-blue
::
<script setup lang="ts">
import { Bubble, BubbleContent } from '@/components/ui/bubble'
</script>
<template>
Click here
</template>Reactions
Use BubbleReactions for bubble reactions. You can use it to display reactions or quick action buttons. Use side and align to position the row β side="top" anchors it to the upper edge. Reactions overlap the bubble edge, so leave vertical space between rows β the examples below use a larger gap for this reason.
::component-preview
name: BubbleReactionsDemo previewClass: h-auto theme-blue
::
Show More / Collapsible
Long bubble content can be composed with Collapsible to allow for a show more or show less interaction. Use the CollapsibleTrigger component to trigger the collapsible content.
::component-preview
name: BubbleCollapsibleDemo previewClass: h-auto theme-blue
::
Tooltip
Wrap a bubble in a Tooltip to reveal metadata on hover, such as when a message was read.
::component-preview
name: BubbleTooltipDemo previewClass: h-auto theme-blue
::
Popover
Pair a bubble with a Popover to surface more information on demand, such as the full error message for a failed action.
::component-preview
name: BubblePopoverDemo previewClass: h-auto theme-blue
::
Accessibility
Bubble renders the presentational message surface. Keep conversation-level semantics on the surrounding container and follow the guidelines below.
Labeling Reactions
Reactions render as a row of emoji. A screen reader reads each glyph with no context, and counters like +8 are announced as "plus eight". Group the row as a single image with a descriptive aria-label so it announces once. role="img" also hides the individual emoji from assistive tech, so no aria-hidden is needed.
π
π₯
+8When reactions are interactive, render buttons instead and give icon-only buttons an aria-label.
Interactive Bubbles
When a bubble is clickable, pass a real <button> or <a> through BubbleContent with as-child so it is focusable and exposes the correct role. BubbleContent ships a visible focus ring for interactive elements, and the accessible name comes from the bubble text. No extra label is needed.
I forgot my passwordMeaning Beyond Color
Bubble variants signal role and tone with color. Pair them with text, alignment, or icons so meaning is not conveyed by color alone. For a destructive bubble, keep the error context in the message text rather than relying on the color treatment.
API Reference
All Bubble parts render a <div> by default. Use as to choose another element, or as-child to merge the component's attributes and styles onto the single element or component in its default slot.
Bubble
The root bubble wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive" |
"default" |
The bubble visual treatment. |
align |
"start" | "end" |
"start" |
The inline alignment of the bubble. |
as |
string | Component |
"div" |
The element or component to render. |
as-child |
boolean |
false |
Render the default slot as the root and merge props onto it. |
class |
string |
- | Additional classes to apply to the root element. |
BubbleContent
The bubble content wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
as |
string | Component |
"div" |
The element or component to render. |
as-child |
boolean |
false |
Render the default slot as the root and merge props onto it. |
class |
string |
- | Additional classes to apply to the content element. |
BubbleReactions
Displays overlapped reactions for a bubble.
| Prop | Type | Default | Description |
|---|---|---|---|
side |
"top" | "bottom" |
"bottom" |
The side of the bubble to anchor the reactions. |
align |
"start" | "end" |
"end" |
The inline alignment of the reactions. |
as |
string | Component |
"div" |
The element or component to render. |
as-child |
boolean |
false |
Render the default slot as the root and merge props onto it. |
class |
string |
- | Additional classes to apply to the reaction row. |
BubbleGroup
Groups consecutive bubbles from the same sender.
| Prop | Type | Default | Description |
|---|---|---|---|
as |
string | Component |
"div" |
The element or component to render. |
as-child |
boolean |
false |
Render the default slot as the root and merge props onto it. |
class |
string |
- | Additional classes to apply to the group root. |
Apps V4 Content Docs Components Button Group (`apps/v4/content/docs/components/button-group.md`)
title: Button Group description: A container that groups related buttons together with consistent styling. component: true
::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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/button-group) 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>
Button 1
Button 2
</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>
Button 1
Button 2
</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" |
"horizontal" |
<template>
Button 1
Button 2
</template>Nest multiple button groups to create complex layouts with spacing. See the nested example for more details.
<template>
</template>ButtonGroupSeparator
The ButtonGroupSeparator component visually divides buttons within a group.
| Prop | Type | Default |
|---|---|---|
orientation |
"horizontal" | "vertical" |
vertical |
<template>
Button 1
Button 2
</template>ButtonGroupText
Use this component to display text within a button group.
| Prop | Type | Default |
|---|---|---|
as-child |
boolean | false |
<template>
Text
Button
</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>
<Label for="name">Text</Label>
</template>Apps V4 Content Docs Components Button (`apps/v4/content/docs/components/button.md`)
title: Button description: Displays a button or a component that looks like a button. featured: true component: true
::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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/button) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>
<template>
Button
</template>Cursor
Tailwind v4 switched 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>
<CircleFadingArrowUpIcon />
</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 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>
[Login](/login)
</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 |
Apps V4 Content Docs Components Calendar (`apps/v4/content/docs/components/calendar.md`)
title: Calendar description: A date field component that allows users to enter and edit date. component: true links: doc: https://reka-ui.com/docs/components/calendar api: https://reka-ui.com/docs/components/calendar#api-reference
::component-preview
name: CalendarDemo description: A calendar component. previewClass: h-[26rem]
::
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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/calendar) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar'
</script>
<template>
<Calendar />
</template>Calendar Systems (Persian / Hijri / Jalali for example)
@internationalized/date 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.
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"
/>
<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.
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. previewClass: h-[30rem]
::
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, especially the section on 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: 'mt-8' previewClass: h-[600px]
::
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. previewClass: h-[32rem]
::
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]'
::
Apps V4 Content Docs Components Card (`apps/v4/content/docs/components/card.md`)
title: Card description: Displays a card with header, content, and footer. component: true
::component-preview
name: CardDemo description: A card with a form previewClass: h-[32rem] sm:h-[30rem]
::
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
<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>
Card Content
</CardContent>
<CardFooter>
Card Footer
</CardFooter>
</Card>
</template>Apps V4 Content Docs Components Carousel (`apps/v4/content/docs/components/carousel.md`)
title: Carousel description: A carousel with motion and swipe built using Embla. component: true
::component-preview
name: CarouselDemo title: Carousel description: A carousel component. previewClass: h-[25rem] sm:h-[32rem]
::
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:
::
```bash
npm install embla-carousel-vue
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/carousel) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<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. previewClass: h-[25rem] sm:h-72
::
// 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. previewClass: h-[25rem] sm:h-72
::
<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. previewClass: h-[32rem]
::
<Carousel orientation="vertical | horizontal">
...
</Carousel>Options
You can pass options to the carousel using the opts prop. See the Embla Carousel docs 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. previewClass: h-[27rem] sm:h-[32rem]
::
Method 2
You can access it through setting a template ref on the <Carousel /> component.
<script setup lang="ts">
const carouselContainerRef = ref | 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 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. previewClass: h-[25rem] sm:h-[32rem]
::
See the Embla Carousel docs for more information on using plugins.
Apps V4 Content Docs Components Chart (`apps/v4/content/docs/components/chart.md`)
title: Chart description: Beautiful charts. Built using Unovis. Copy and paste into your apps. component: true
::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.
<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
::
```bash
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
::
```css
@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
/* Detailed source-code truncated for AI context efficiency. */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?
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 ChartConfigUsing 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.
Apps V4 Content Docs Components Checkbox (`apps/v4/content/docs/components/checkbox.md`)
title: Checkbox description: A control that allows the user to toggle between checked and not checked. component: true links: doc: https://reka-ui.com/docs/components/checkbox api: https://reka-ui.com/docs/components/checkbox#api-reference
::component-preview
name: CheckboxDemo description: A checkbox previewClass: h-88 sm:h-80
::
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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/checkbox) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script setup lang="ts">
import { Checkbox } from '@/components/ui/checkbox'
</script>
<template>
<Checkbox />
</template>Checked State
Use default-value for uncontrolled checkboxes, or v-model to control the state.
<script setup lang="ts">
import { ref } from 'vue'
import { Checkbox } from '@/components/ui/checkbox'
const checked = ref(false)
</script>
<template>
<Checkbox v-model="checked" />
</template>Apps V4 Content Docs Components Collapsible (`apps/v4/content/docs/components/collapsible.md`)
title: Collapsible description: An interactive component which expands/collapses a panel. component: true links: doc: https://reka-ui.com/docs/components/collapsible api: https://reka-ui.com/docs/components/collapsible#api-reference
::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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/collapsible) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<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>Apps V4 Content Docs Components Combobox (`apps/v4/content/docs/components/combobox.md`)
title: Combobox description: Autocomplete input and command palette with a list of suggestions. component: true links: doc: https://reka-ui.com/docs/components/combobox api: https://reka-ui.com/docs/components/combobox#api-reference
::component-preview
name: ComboboxDemo description: A combobox with a list of frameworks.
::
A combobox can be built in two ways:
- Using the
Comboboxcomponents, built on top of the Reka UI Combobox. - Composing the
Popoverand theCommandcomponents as a listbox.
Installation
Combobox
:::::code-tabs
:::tabs-list
::tabs-trigger{value="cli"}
CLI
::
::tabs-trigger{value="manual"}
Manual
::
:::
::tabs-content{value="cli"}
npx shadcn-vue@latest add combobox::
::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::
```bash
npm install reka-ui @vueuse/core
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/combobox) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Popover and Command
The listbox variant is built using a composition of the Popover and the Command components.
See installation instructions for the Popover and the Command components.
Usage
Combobox
/* Detailed source-code truncated for AI context efficiency. */Popover and Command
/* Detailed source-code truncated for AI context efficiency. */Examples
User
A combobox with custom item rendering and an action item.
::component-preview
name: ComboboxUserDemo description: A combobox with a list of users.
::
Timezone
A combobox with grouped items and a scrollable viewport.
::component-preview
name: ComboboxTimezoneDemo description: A combobox with a list of timezones.
::
Multiple
A combobox with multiple selection.
::component-preview
name: ComboboxMultipleDemo description: A combobox with multiple selection.
::
With Listbox
A combobox built with the Popover and the Command components.
::component-preview
name: ComboboxWithListboxDemo description: A combobox built with the Popover and Command components.
::
Apps V4 Content Docs Components Command (`apps/v4/content/docs/components/command.md`)
title: Command description: Fast, composable, unstyled command menu for Vue. component: true links: doc: https://reka-ui.com/docs/components/listbox api: https://reka-ui.com/docs/components/listbox#api-reference
::component-preview
name: CommandDemo description: A command component. class: "[&_.preview>div]:max-w-[450px]" previewClass: h-[24.5rem]
::
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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/command) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<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>Apps V4 Content Docs Components Context Menu (`apps/v4/content/docs/components/context-menu.md`)
title: Context Menu description: Displays a menu to the user β such as a set of actions or functions β triggered by a right click. component: true links: doc: https://reka-ui.com/docs/components/context-menu api: https://reka-ui.com/docs/components/context-menu#api-reference
::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:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/context-menu) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<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>Apps V4 Content Docs Components Data Table (`apps/v4/content/docs/components/data-table.md`)
title: Data Table description: Powerful table and datagrids built using TanStack Table. component: true
::component-preview
name: DataTableDemo description: A data table with sorting, filtering, and pagination. align: start previewClass: items-start h-auto px-4 md:px-8
::
::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:
- Set up Table Features
- Basic Table
- Row Actions
- Pagination
- Sorting
- Filtering
- Visibility
- Row Selection
- Expanding
- Reusable Components
Installation
- Add the
<Table />component to your project:
npx shadcn-vue@latest add table- Add the
@tanstack/vue-tabledependency. This guide uses TanStack Table v9:
npm install @tanstack/vue-tablePrerequisites
We are going to build a table to show recent payments. Here's what our data looks like:
interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
export const payments: Payment[] = [
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: '[email protected]',
},
{
id: '489e1d42',
amount: 125,
status: 'processing',
email: '[email protected]',
},
// ...
]Project Structure
Start by creating the following file structure:
components
βββ payments
βββ columns.ts
βββ features.ts
βββ data-table.vue
βββ data-table-dropdown.vue
βββ app.vueI'm using a Nuxt example here but this works for any other Vue framework.
columns.tsIt will contain our column definitions.features.tsIt will contain the sharedfeaturesobject that tells TanStack Table which behavior to enable.data-table.vueIt will contain our<DataTable />component.data-table-dropdown.vueIt will contain our<DropdownAction />component.app.vueThis is where we'll fetch data and render our table.
Set up Table Features
TanStack Table v9 is feature-based: you opt into the behavior you want β sorting, filtering, pagination, and so on β by declaring it with tableFeatures(). Anything you don't register is tree-shaken out of your bundle. That includes the built-in filter and sort functions: register the ones your columns rely on under filterFns and sortFns. Our email filter uses includesString, and string columns sort with alphanumeric / text.
We'll define the features object once in features.ts and share it between our column definitions and the <DataTable /> component.
// components/payments/features.ts
import {
columnFilteringFeature,
columnVisibilityFeature,
createExpandedRowModel,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
rowExpandingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_text,
tableFeatures,
} from '@tanstack/vue-table'
// New in v9: declare the features this table uses β anything you don't
// register is tree-shaken out of the bundle.
export const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowExpandingFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
expandedRowModel: createExpandedRowModel(),
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
})
// Pass this as the first generic argument to `ColumnDef`, `Column`, `Table`,
// and `Row` so each type knows which feature APIs are available.
export type DataTableFeatures = typeof features::callout{class="mt-4"}
Note: The core row model is always included, so you never register it yourself. Row models for optional features are created with create*RowModel() and registered on the features object β there are no more get*RowModel table options.
::
Basic Table
Let's start by building a basic table.
Column Definitions
First, we'll define our columns in the columns.ts file using a column helper typed with our features.
// components/payments/columns.ts
import { createColumnHelper } from '@tanstack/vue-table'
import type { DataTableFeatures } from './features'
// This type defines the shape of our data.
// You can use a Zod schema here if you want.
export interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
// Use `accessor` for data columns and `display` for columns without one.
const columnHelper = createColumnHelper<DataTableFeatures, Payment>()
export const columns = columnHelper.columns([
columnHelper.accessor('status', {
header: 'Status',
}),
columnHelper.accessor('email', {
header: 'Email',
}),
columnHelper.accessor('amount', {
header: 'Amount',
}),
])::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.
::
` ` component
Next, we'll create a <DataTable /> component to render our table.
useTable must be called during component setup so it can wire itself into Vue's reactivity. From there the table manages its own state: reads like table.getRowModel() in your template are reactive, so your markup updates automatically β no refs or change handlers required. We pass data and columns through getters so the table always reads the current prop values.
/* Detailed source-code truncated for AI context efficiency. */<FlexRender /> takes the header or cell instance you pass it and renders whatever the column definition provides β a plain string, a render function, or a component.
::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" />
::
::callout{class="mt-4"}
Controlled state: because v9 owns table state internally, you won't write refs or on*Change handlers for most of this guide. If something outside the table needs to own a state slice (syncing filters to the URL, server-driven pagination, etc.), controlled state still exists β we'll manage the row selection slice externally in the Row Selection section. See the migration guide for details.
::
Render the table
Finally, we'll render our table in our index component.
<script setup lang="ts">
import type { Payment } from './components/payments/columns'
import { onMounted, ref } from 'vue'
import { columns } from './components/payments/columns'
import DataTable from './components/payments/data-table.vue'
const data = ref([])
async function getData(): Promise {
// Fetch data from your API here.
return [
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: '[email protected]',
},
// ...
]
}
onMounted(async () => {
data.value = await getData()
})
</script>
<template>
<DataTable :columns="columns" :data="data" />
</template>Cell Formatting
Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.
Update columns definition
Update the header and cell definitions for amount as follows:
// components/payments/columns.ts
import { h } from 'vue'
export const columns = columnHelper.columns([
// ...
columnHelper.accessor('amount', {
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
// Format the amount as a dollar 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.
Row Actions
Let's add row actions to our table. We'll use a <Dropdown /> component for this.
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>
Open menu
<MoreHorizontal class="w-4 h-4" />
</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. Because the column doesn't read a data field, we define it with columnHelper.display.
// components/payments/columns.ts
import DropdownAction from '@/components/DataTableDropDown.vue'
export const columns = columnHelper.columns([
// ...
columnHelper.display({
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.
Pagination
Next, we'll add pagination to our table.
Pagination is already enabled
Because our features object registers rowPaginationFeature and createPaginatedRowModel(), the table automatically paginates rows into pages of 10 β there's nothing to add to useTable. See the pagination docs 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 setup lang="ts" generic="TData">
import { Button } from '@/components/ui/button'
// ...the rest of the script stays the same
</script>
<template>
Previous
Next
</template>See Reusable Components section for a more advanced pagination component.
Change the page size
To change the page size, call table.setPageSize(). To read the current pagination state β say, for a page indicator β read table.atoms.pagination.get().
<script setup lang="ts">
import { computed } from 'vue'
// Reactive read β updates whenever the user changes pages.
const pagination = computed(() => table.atoms.pagination.get())
</script>
<template>
Show 20 rows
Page {{ pagination.pageIndex + 1 }} of {{ table.getPageCount() }}
</template>::callout{class="mt-4"}
Note: Atom reads like table.atoms.pagination.get() are reactive inside Vue tracking scopes β a template expression or a computed. A bare .get() at the top level of <script setup> is a one-time snapshot, so wrap script-side reads in computed().
::
Sorting
Let's make the email column sortable.
The rowSortingFeature and sorted row model are already registered in our features object β along with the alphanumeric and text sort functions that string columns resolve through the default auto setting β so there's nothing to change in <DataTable />. We just add the UI.
Make header cell sortable
We can update the email header cell to add sorting controls.
// components/payments/columns.ts
import { ArrowUpDown } from '@lucide/vue'
import { Button } from '@/components/ui/button'
export const columns = columnHelper.columns([
// ...
columnHelper.accessor('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. The table owns the sorting state β no wiring required.
Filtering
Let's add a search input to filter emails in our table.
The columnFilteringFeature, filtered row model, and the includesString filter function are already registered in our features object, so the only work left is rendering an input.
Add the search input
<script setup lang="ts" generic="TData">
import { Input } from '@/components/ui/input'
// ...the rest of the script stays the same
</script>
<template>
</template>Filtering is now enabled for the email column. You can add filters to other columns as well β just remember that string-based filter references only resolve functions you've registered, so if another column needs a different built-in filter, add it to filterFns in features.ts first. See the filtering docs for more information on customizing filters.
Visibility
Adding column visibility is fairly simple using @tanstack/vue-table visibility API. The columnVisibilityFeature is already registered in our features object, so we only need to add the dropdown.
Add the column toggle dropdown
<script setup lang="ts" generic="TData">
import { ChevronDown } from '@lucide/vue'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
// ...the rest of the script stays the same
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
Columns <ChevronDown class="w-4 h-4 ml-2" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())"
:key="column.id"
class="capitalize"
:model-value="column.getIsVisible()"
@update:model-value="(value) => {
column.toggleVisibility(!!value)
}"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</template>This adds a dropdown menu that you can use to toggle column visibility.
Row Selection
Next, we're going to add row selection to our table. The rowSelectionFeature is already registered in our features object, so the table tracks selection for us β we just render the checkboxes.
Update column definitions
// components/payments/columns.ts
import { Checkbox } from '@/components/ui/checkbox'
export const columns = columnHelper.columns([
columnHelper.display({
id: 'select',
header: ({ table }) => h(Checkbox, {
'modelValue': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
}),
// ...
])This adds a checkbox to each row and a checkbox in the header to select all rows.
::callout{class="mt-4"}
Note: In v9, table.getIsSomePageRowsSelected() returns true whenever at least one page row is selected β even when all are. The header checkbox still works because table.getIsAllPageRowsSelected() || ... short-circuits to true at full selection before the 'indeterminate' branch is reached.
::
Manage the selection state externally
The table tracks selection internally by default. To show how a slice can live outside the table β handy when your page needs to read or drive the selection β we'll own just this one slice with a Vue ref and leave everything else internal. Expose the current value with a state getter, and resolve the updater in the matching callback:
// components/payments/data-table.vue
import type { RowSelectionState } from '@tanstack/vue-table'
import { ref } from 'vue'
// Keep row selection outside the table so the rest of the app can read or update it.
const rowSelection = ref<RowSelectionState>({})
const table = useTable({
features,
get data() { return props.data },
get columns() { return props.columns },
state: {
get rowSelection() { return rowSelection.value },
},
onRowSelectionChange: (updater) => {
rowSelection.value = typeof updater === 'function' ? updater(rowSelection.value) : updater
},
})rowSelection.value can now be read or written anywhere in the component, and the table stays in sync.
Show selected rows
You can show the number of selected rows using the table.getFilteredSelectedRowModel() API.
<template>
</template>Apps V4 Content Docs Components Tabs (`apps/v4/content/docs/components/tabs.md`)
title: Tabs description: A set of layered sections of contentβknown as tab panelsβthat are displayed one at a time. component: true links: doc: https://reka-ui.com/docs/components/tabs api: https://reka-ui.com/docs/components/tabs#api-reference
::component-preview
name: TabsDemo description: A tabs component with account and password tabs. previewClass: h-[32rem] sm:h-[30rem]
::
Installation
:::::code-tabs
:::tabs-list
::tabs-trigger{value="cli"}
CLI
::
::tabs-trigger{value="manual"}
Manual
::
:::
::tabs-content{value="cli"}
npx shadcn-vue@latest add tabs::
::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/tabs) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
</script>
<template>
<Tabs default-value="account">
<TabsList>
<TabsTrigger value="account">
Account
</TabsTrigger>
<TabsTrigger value="password">
Password
</TabsTrigger>
</TabsList>
<TabsContent value="account">
Make changes to your account here.
</TabsContent>
<TabsContent value="password">
Change your password here.
</TabsContent>
</Tabs>
</template>Apps V4 Content Docs Components Tags Input (`apps/v4/content/docs/components/tags-input.md`)
title: Tags Input description: Tag inputs render tags inside an input, followed by an actual text input. component: true links: doc: https://reka-ui.com/docs/components/tags-input api: https://reka-ui.com/docs/components/tags-input#api-reference
::component-preview
name: TagsInputDemo
::
Installation
:::::code-tabs
:::tabs-list
::tabs-trigger{value="cli"}
CLI
::
::tabs-trigger{value="manual"}
Manual
::
:::
::tabs-content{value="cli"}
npx shadcn-vue@latest add tags-input::
::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/tags-input) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Examples
Tags with Listbox
::component-preview
name: TagsInputWithListbox
::
Apps V4 Content Docs Components Textarea (`apps/v4/content/docs/components/textarea.md`)
title: Textarea description: Displays a form textarea or a component that looks like a textarea. component: true
::component-preview
name: TextareaDemo description: A textarea 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 textarea::
::::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
<script setup lang="ts">
import { Textarea } from '@/components/ui/textarea'
</script>
<template>
<Textarea placeholder="Type your message here." />
</template>Apps V4 Content Docs Components Toast (`apps/v4/content/docs/components/toast.md`)
title: Toast description: A succinct message that is displayed temporarily. component: true links: doc: https://reka-ui.com/docs/components/toast api: https://reka-ui.com/docs/components/toast#api-reference
See the sonner documentation for more information.
If you're looking for the old toast component, see the old docs for more information.
Apps V4 Content Docs Components Toggle Group (`apps/v4/content/docs/components/toggle-group.md`)
title: Toggle Group description: A set of two-state buttons that can be toggled on or off. component: true links: doc: https://reka-ui.com/docs/components/toggle-group api: https://reka-ui.com/docs/components/toggle-group#api-reference
::component-preview
name: ToggleGroupDemo description: A toggle group 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 toggle-group::
::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/toggle-group) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script setup lang="ts">
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
</script>
<template>
<ToggleGroup type="multiple">
<ToggleGroupItem value="a">
A
</ToggleGroupItem>
<ToggleGroupItem value="b">
B
</ToggleGroupItem>
<ToggleGroupItem value="c">
C
</ToggleGroupItem>
</ToggleGroup>
</template>Examples
Default
::component-preview
name: ToggleGroupDefaultDemo
::
Outline
::component-preview
name: ToggleGroupDemo
::
Single
::component-preview
name: ToggleGroupSingleDemo
::
Small
::component-preview
name: ToggleGroupSmallDemo
::
Large
::component-preview
name: ToggleGroupLargeDemo
::
Disabled
::component-preview
name: ToggleGroupDisabledDemo
::
Apps V4 Content Docs Components Toggle (`apps/v4/content/docs/components/toggle.md`)
title: Toggle description: A two-state button that can be either on or off. component: true links: doc: https://reka-ui.com/docs/components/toggle api: https://reka-ui.com/docs/components/toggle#api-reference
::component-preview
name: ToggleDemo description: A toggle 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 toggle::
::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/toggle) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script setup lang="ts">
import { Toggle } from '@/components/ui/toggle'
</script>
<template>
<Toggle>Toggle</Toggle>
</template>Pressed State
Use default-value for uncontrolled toggles, or v-model to control the state.
<script setup lang="ts">
import { ref } from 'vue'
import { Toggle } from '@/components/ui/toggle'
const pressed = ref(false)
</script>
<template>
<Toggle v-model="pressed">
Toggle
</Toggle>
</template>Examples
Default
::component-preview
name: ToggleDemo
::
Outline
::component-preview
name: ToggleOutlineDemo
::
With Text
::component-preview
name: ToggleTextDemo
::
Small
::component-preview
name: ToggleSmallDemo
::
Large
::component-preview
name: ToggleLargeDemo
::
Disabled
::component-preview
name: ToggleDisabledDemo
::
Apps V4 Content Docs Components Tooltip (`apps/v4/content/docs/components/tooltip.md`)
title: Tooltip description: A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it. component: true links: doc: https://reka-ui.com/docs/components/tooltip api: https://reka-ui.com/docs/components/tooltip#api-reference
::component-preview
name: TooltipDemo description: A tooltip 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 tooltip::
::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::
```bash
npm install reka-ui
```
::step
Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/tooltip) into your project.
::
::step
Update the import paths to match your project setup.
:: :::
::::
:::::
Usage
<script setup lang="ts">
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
</script>
<template>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>Hover</TooltipTrigger>
<TooltipContent>
Add to library
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>Apps V4 Content Docs Components Typography (`apps/v4/content/docs/components/typography.md`)
title: Typography description: Styles for headings, paragraphs, lists...etc component: true
We do not ship any typography styles by default. This page is an example of how you can use utility classes to style your text.
::component-preview
name: TypographyDemo description: A collection of typographic elements. class: "[&_.preview]:!h-auto" hideCode: true
::
h1
::component-preview
name: TypographyH1
::
h2
::component-preview
name: TypographyH2
::
h3
::component-preview
name: TypographyH3
::
h4
::component-preview
name: TypographyH4
::
p
::component-preview
name: TypographyP
::
blockquote
::component-preview
name: TypographyBlockquote
::
table
::component-preview
name: TypographyTable
::
list
::component-preview
name: TypographyList
::
Inline code
::component-preview
name: TypographyInlineCode
::
Lead
::component-preview
name: TypographyLead
::
Large
::component-preview
name: TypographyLarge
::
Small
::component-preview
name: TypographySmall
::
Muted
::component-preview
name: TypographyMuted
::
Apps V4 Content Docs Dark Mode 01.Vite (`apps/v4/content/docs/dark-mode/01.vite.md`)
title: Vite description: Adding dark mode to your vite app.
Dark mode
::vue-school-link{lesson="dark-mode-with-shadcn-vue" placement="top"}
Watch a Vue School video about how to enable dark mode with shadcn-vue.
::
Install Dependencies
npm install @vueuse/coreOptional, to include icons for theme button.
npm install -D @iconify/vue @iconify-json/radix-iconsAdd a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using useColorMode from @vueuse/core.
Reactive color mode (dark / light / customs) with auto data persistence.
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { useColorMode } from '@vueuse/core'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
// Pass { disableTransition: false } to enable transitions
const mode = useColorMode()
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
Toggle theme
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="mode = 'light'">
Light
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'dark'">
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'auto'">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>Apps V4 Content Docs Dark Mode 02.Nuxt (`apps/v4/content/docs/dark-mode/02.nuxt.md`)
title: Nuxt description: Adding dark mode to your nuxt app.
Dark mode
Install Dependencies
npm install -D @nuxtjs/color-modeThen, add @nuxtjs/color-mode to the modules section of your nuxt.config.ts
export default defineNuxtConfig({
modules: [
'...',
'@nuxtjs/color-mode'
],
colorMode: {
classSuffix: ''
}
})Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using useColorMode from Nuxt Color Mode.
Optional, to include icons for theme button.
npm install -D @iconify/vue @iconify-json/radix-icons<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
const colorMode = useColorMode()
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
Toggle theme
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="colorMode.preference = 'light'">
Light
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'dark'">
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'system'">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>Apps V4 Content Docs Dark Mode 03.Vitepress (`apps/v4/content/docs/dark-mode/03.vitepress.md`)
title: Vitepress description: Adding dark mode to your vitepress app.
Dark mode
Install Dependencies
npm install @vueuse/coreOptional, to include icons for theme button.
npm install -D @iconify/vue @iconify-json/radix-iconsAdd a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using useToggle from @vueuse/core.
A boolean switcher with utility functions.
<script setup lang="ts">
import { useToggle } from '@vueuse/core'
import { useData } from 'vitepress'
import { Button } from '@/registry/default/ui/button'
const { frontmatter, isDark } = useData()
const toggleDark = useToggle(isDark)
</script>
<template>
Toggle theme
</template>Apps V4 Content Docs Dark Mode 04.Astro (`apps/v4/content/docs/dark-mode/04.astro.md`)
title: Astro description: Adding dark mode to your astro app.
Dark mode
Create an inline theme script
---
import '../styles/globals.css'
---
<script is:inline>
const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
const isDark = getThemePreference() === 'dark';
document.documentElement.classList[isDark ? 'add' : 'remove']('dark');
if (typeof localStorage !== 'undefined') {
const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
</script>
<html lang="en">
<h1>Astro</h1>
</html>
</script>Install Dependencies
npm install @vueuse/coreOptional, to include icons for theme button.
npm install -D @iconify/vue @iconify-json/radix-iconsAdd a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
We're using useColorMode from @vueuse/core.
Reactive color mode (dark / light / customs) with auto data persistence.
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { useColorMode } from '@vueuse/core'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
const mode = useColorMode()
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
Toggle theme
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="mode = 'light'">
Light
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'dark'">
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="mode = 'auto'">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>Display the mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
---
import '../styles/globals.css'
import ModeToggle from '@/components/ModeToggle.vue';
---
<html lang="en">
<h1>Astro</h1>
<ModeToggle client:load />
</html>Apps V4 Content Docs Forms 01.Vee Validate (`apps/v4/content/docs/forms/01.vee-validate.md`)
title: VeeValidate description: Build forms in Vue using VeeValidate and Zod. links: doc: https://vee-validate.logaretm.com/v4
In this guide, we will take a look at building forms with VeeValidate. We'll cover building forms with the <Field /> component, adding schema validation using Zod, error handling, accessibility, and more.
::vue-school-link{class="mt-6" lesson="forms-and-form-validation-with-shadcn-vue" placement="top"}
Watch a Vue School video about building forms and validation with shadcn-vue.
::
Demo
We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
::callout
icon: true
Note: For the purpose of this demo, we have intentionally disabled browser validation to show how schema validation and form errors work in VeeValidate. It is recommended to add basic browser validation in your production code.
::
::component-preview
name: VeeValidateDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
Approach
This form leverages VeeValidate for performant, flexible form handling. We'll build our form using the <Field /> component, which gives you complete flexibility over the markup and styling.
- Uses VeeValidate's
useFormcomposable for form state management. - VeeValidate's
<Field />component with scoped slots for controlled inputs with validation. - shadcn-vue
<Field />components for building accessible forms. - Client-side validation using Zod with
toTypedSchema.
Anatomy
Here's a basic example of a form using VeeValidate's <Field /> component with scoped slots and shadcn-vue <Field /> components.
Bind componentField when the control is a Vue component with v-model, such as shadcn-vue's <Input />. Bind field when the control is a native element. field binds value, which a v-model component ignores, so initialValues never render.
::::tabs{default-value="component"}
:::tabs-list
::tabs-trigger{value="component"}
Component
::
::tabs-trigger{value="native"}
Native
:::::
::tabs-content{value="component"}
`Input` Component
<template>
<VeeField v-slot="{ componentField, errors }" name="title">
<Field :data-invalid="!!errors.length">
<FieldLabel for="title">
Bug Title
</FieldLabel>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>::
::tabs-content{value="native"}
native `input` element
<template>
<VeeField v-slot="{ field, errors }" name="title">
<Field :data-invalid="!!errors.length">
<FieldLabel for="title">
Bug Title
</FieldLabel>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template> ::
::::
Form
Create a form schema
We'll start by defining the shape of our form using a Zod schema
::callout
icon: true
Note: This example uses zod v3 for schema validation, but you can replace it with any other Standard Schema validation library supported by VeeValidate.
::
<script setup lang="ts">
import * as z from 'zod'
const formSchema = z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
})
</script>Setup the form
Next, we'll use the useForm composable from VeeValidate to create our form instance. We'll also add the Zod schema for validation.
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import * as z from 'zod'
const formSchema = z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
})
const { handleSubmit } = useForm({
validationSchema: toTypedSchema(formSchema),
initialValues: {
title: '',
},
})
const onSubmit = handleSubmit((values) => {
// Do something with the form values.
console.log(values)
})
</script>
<template>
<form @submit="onSubmit">
</form>
</template>Build the form
We can now build the form using VeeValidate's <Field /> component with scoped slots and shadcn-vue <Field /> components.
::component-source{name="VeeValidateDemo" title="Form.vue"}
::
Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the onSubmit function will be called with the validated form data. If the form data is invalid, VeeValidate will display the errors next to each field.
Validation
Client-side Validation
VeeValidate validates your form data using the Zod schema. Define a schema and pass it to the validationSchema option of the useForm composable.
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm, Field as VeeField } from 'vee-validate'
import * as z from 'zod'
const formSchema = z.object({
title: z.string(),
})
const { handleSubmit } = useForm({
validationSchema: toTypedSchema(formSchema),
initialValues: {
title: '',
},
})
</script>Validation Modes
VeeValidate supports different validation strategies through the Field component props.
<VeeField
v-slot="{ componentField, errors }"
name="title"
:validate-on-input="true"
>
</VeeField>| Prop | Description |
|---|---|
validateOnInput |
Validation triggers on input event. |
validateOnChange |
Validation triggers on change event. |
validateOnBlur |
Validation triggers on blur event. |
validateOnMount |
Validation triggers when component is mounted. |
Displaying Errors
Display errors next to the field using <FieldError />. For styling and accessibility:
- Add the
:data-invalidprop to the shadcn-vue<Field />component. - Add the
:aria-invalidprop to the form control such as<Input />,<SelectTrigger />,<Checkbox />, etc.
<template>
<VeeField v-slot="{ componentField, errors }" name="email">
<Field :data-invalid="!!errors.length">
<FieldLabel for="email">
Email
</FieldLabel>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>Working with Different Field Types
Input
- For input fields, use
v-bind="componentField"to bind VeeValidate's field object to the input. - To show errors, add the
:aria-invalidprop to the<Input />component and the:data-invalidprop to the shadcn-vue<Field />component.
::component-preview
name: VeeValidateInputDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
For simple text inputs, use VeeValidate's Field component with scoped slots.
<template>
<VeeField v-slot="{ componentField, errors }" name="name">
<Field :data-invalid="!!errors.length">
<FieldLabel for="name">
Name
</FieldLabel>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>Textarea
- For textarea fields, use
v-bind="componentField"to bind VeeValidate's field object to the textarea. - To show errors, add the
:aria-invalidprop to the<Textarea />component and the:data-invalidprop to the shadcn-vue<Field />component.
::component-preview
name: VeeValidateTextareaDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
For textarea fields, use VeeValidate's Field component with scoped slots.
<template>
<VeeField v-slot="{ componentField, errors }" name="about">
<Field :data-invalid="!!errors.length">
<FieldLabel for="about">
More about you
</FieldLabel>
<Textarea
id="about"
v-bind="componentField"
placeholder="I'm a software engineer..."
class="min-h-[120px]"
:aria-invalid="!!errors.length"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize your experience.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</template>Select
- For select components, use
v-bind="componentField"on<Select />. It bindsmodelValue,update:modelValueandnamein one go. - To show errors, add the
:aria-invalidprop to the<SelectTrigger />component and the:data-invalidprop to the shadcn-vue<Field />component.
::component-preview
name: VeeValidateSelectDemo class: sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px] chromeLessOnMobile: true
::
<template>
<VeeField v-slot="{ componentField, errors }" name="language">
<Field orientation="responsive" :data-invalid="!!errors.length">
<FieldContent>
<FieldLabel for="language">
Spoken Language
</FieldLabel>
<FieldDescription>For best results, select the language you speak.</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
<Select v-bind="componentField">
<SelectTrigger
id="language"
class="min-w-[120px]"
:aria-invalid="!!errors.length"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">
Auto
</SelectItem>
<SelectItem value="en">
English
</SelectItem>
</SelectContent>
</Select>
</Field>
</VeeField>
</template>Checkbox
- For a single boolean checkbox, use
v-bind="componentField"and settype="checkbox"on VeeValidate's<Field />. - For checkbox arrays, one field holds the whole array, so bind the
valueandhandleChangeslot props by hand instead. - To show errors, add the
:aria-invalidprop to the<Checkbox />component and the:data-invalidprop to the shadcn-vue<Field />component. - Remember to add
data-slot="checkbox-group"to the<FieldGroup />component for proper styling and spacing.
::component-preview
name: VeeValidateCheckboxDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
<template>
<VeeField v-slot="{ componentField, errors }" name="responses" type="checkbox">
<FieldSet :data-invalid="!!errors.length">
<FieldLegend variant="label">
Responses
</FieldLegend>
<FieldDescription>Get notified for requests that take time.</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal">
<Checkbox
id="responses"
v-bind="componentField"
:aria-invalid="!!errors.length"
/>
<FieldLabel for="responses" class="font-normal">
Push notifications
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
</template>For an array field, read the current selection from value and write the next one with handleChange.
<template>
<VeeField v-slot="{ value, handleChange, errors }" name="tasks">
<FieldSet :data-invalid="!!errors.length">
<FieldLegend variant="label">
Tasks
</FieldLegend>
<FieldDescription>Get notified when tasks you've created have updates.</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field
v-for="task in tasks"
:key="task.id"
orientation="horizontal"
:data-invalid="!!errors.length"
>
<Checkbox
:id="`task-${task.id}`"
:model-value="value?.includes(task.id)"
:aria-invalid="!!errors.length"
@update:model-value="(checked: boolean | 'indeterminate') => {
handleChange(checked
? [...(value || []), task.id]
: (value || []).filter((id: string) => id !== task.id))
}"
/>
<FieldLabel :for="`task-${task.id}`" class="font-normal">
{{ task.label }}
</FieldLabel>
</Field>
</FieldGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
</template>Radio Group
- For radio groups, use
v-bind="componentField"on<RadioGroup />. Each<RadioGroupItem />only needs its ownvalue. - To show errors, add the
:aria-invalidprop to the<RadioGroup />component and the:data-invalidprop to the shadcn-vue<Field />component.
::component-preview
name: VeeValidateRadioGroupDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
<template>
<VeeField v-slot="{ componentField, errors }" name="plan">
<FieldSet :data-invalid="!!errors.length">
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
v-bind="componentField"
:aria-invalid="!!errors.length"
>
<FieldLabel v-for="planOption in plans" :key="planOption.id" :for="`plan-${planOption.id}`">
<Field orientation="horizontal" :data-invalid="!!errors.length">
<FieldContent>
<FieldTitle>{{ planOption.title }}</FieldTitle>
<FieldDescription>{{ planOption.description }}</FieldDescription>
</FieldContent>
<RadioGroupItem
:id="`plan-${planOption.id}`"
:value="planOption.id"
/>
</Field>
</FieldLabel>
</RadioGroup>
<FieldError v-if="errors.length" :errors="errors" />
</FieldSet>
</VeeField>
</template>Switch
- For switches, use
v-bind="componentField"and settype="checkbox"on VeeValidate's<Field />so the value is treated as a boolean. - To show errors, add the
:aria-invalidprop to the<Switch />component and the:data-invalidprop to the shadcn-vue<Field />component.
::component-preview
name: VeeValidateSwitchDemo class: sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px] chromeLessOnMobile: true
::
<template>
<VeeField v-slot="{ componentField, errors }" name="twoFactor" type="checkbox">
<Field orientation="horizontal" :data-invalid="!!errors.length">
<FieldContent>
<FieldLabel for="two-factor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
<Switch
id="two-factor"
v-bind="componentField"
:aria-invalid="!!errors.length"
/>
</Field>
</VeeField>
</template>Complex Forms
Here is an example of a more complex form with multiple fields and validation.
::component-preview
name: VeeValidateComplexDemo class: sm:[&_.preview]:h-[1300px] sm:[&_pre]:!h-[1300px] chromeLessOnMobile: true
::
Resetting the Form
Use the resetForm function returned by useForm to reset the form to its initial values.
<script setup lang="ts">
const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
// ...
})
</script>
<template>
Reset
</template>Array Fields
VeeValidate provides a FieldArray component for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
::component-preview
name: VeeValidateArrayDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
Using FieldArray
Use the FieldArray component to manage array fields. It provides fields, push, and remove methods through its slot props.
<script setup lang="ts">
import { FieldArray as VeeFieldArray } from 'vee-validate'
</script>
<template>
<VeeFieldArray v-slot="{ fields, push, remove }" name="emails">
</VeeFieldArray>
</template>Array Field Structure
Wrap your array fields in a <FieldSet /> with a <FieldLegend /> and <FieldDescription />.
<template>
<FieldSet class="gap-4">
<FieldLegend variant="label">
Email Addresses
</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup class="gap-4">
</FieldGroup>
</FieldSet>
</template>Field Pattern for Array Items
Map over the fields array and create fields for each item. Make sure to use field.key as the key.
<template>
<VeeFieldArray v-slot="{ fields, push, remove }" name="emails">
<VeeField
v-for="(field, index) in fields"
:key="field.key"
v-slot="{ componentField: controllerField, errors }"
:name="`emails[${index}].address`"
>
<Field orientation="horizontal" :data-invalid="!!errors.length">
<FieldContent class="flex-1">
<FieldError v-if="errors.length" :errors="errors" />
</FieldContent>
</Field>
</VeeField>
</VeeFieldArray>
</template>Adding Items
Use the push method to add new items to the array.
<template>
= 5"
@click="push({ address: '' })"
>
Add Email Address
</template>Removing Items
Use the remove method to remove items from the array. Add the remove button conditionally.
<template>
1" align="inline-end">
<XIcon />
</template>Array Validation
Use Zod's array method to validate array fields.
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email('Enter a valid email address.'),
}),
)
.min(1, 'Add at least one email address.')
.max(5, 'You can add up to 5 email addresses.'),
})Apps V4 Content Docs Forms 02.Tanstack Form (`apps/v4/content/docs/forms/02.tanstack-form.md`)
title: TanStack Form description: Build forms in Vue using TanStack Form and Zod. links: doc: https://tanstack.com/form
This guide explores how to build forms using TanStack Form. You'll learn to create forms with <Field /> components, implement schema validation with Zod, handle errors, and ensure accessibility.
Demo
We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
::callout
icon: true
Note: For the purpose of this demo, we have intentionally disabled browser validation to show how schema validation and form errors work in TanStack Form. It is recommended to add basic browser validation in your production code.
::
::component-preview
name: TanStackFormDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
Approach
This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the <Field /> components, which give you complete flexibility over the markup and styling.
- Uses TanStack Form's
useFormcomposable for form state management. form.Fieldcomponents with render prop pattern for controlled inputs.<Field />components for building accessible forms.- Client-side validation using Zod.
- Real-time validation feedback.
Anatomy
Here's a basic example of a form using TanStack Form with the <Field /> component.
<template>
<form
@submit.prevent="form.handleSubmit"
>
<FieldGroup>
<form.Field
name="title"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="field.name">Bug Title</FieldLabel>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</FieldGroup>
Submit
</form>
</template>Form
Create a form schema
We'll start by defining the shape of our form using a Zod schema.
Note: This example uses zod v3 for schema validation. TanStack Form integrates seamlessly with Zod and other Standard Schema validation libraries through its validators API.
<script setup lang="ts">
import { z } from 'zod'
const formSchema = z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
})
</script>Setup the form
Use the useForm composable from TanStack Form to create your form instance with Zod validation.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { toast } from 'vue-sonner'
import { z } from 'zod'
const formSchema = z.object({
// ...
})
const form = useForm({
defaultValues: {
title: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success('Form submitted successfully')
},
})
function isInvalid(field) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</script>
<template>
<form @submit.prevent="form.handleSubmit">
</form>
</template>We are using onSubmit to validate the form data here. TanStack Form supports other validation modes, which you can read about in the documentation.
Build the form
We can now build the form using the form.Field component from TanStack Form and the Field components.
::component-source
name: TanStackFormDemo title: Form.vue
::
Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the onSubmit function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.
Validation
Client-side Validation
TanStack Form validates your form data using the Zod schema. Validation happens in real-time as the user types.
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const formSchema = z.object({
// ...
})
const form = useForm({
defaultValues: {
title: '',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
</script>Validation Modes
TanStack Form supports different validation strategies through the validators option:
| Mode | Description |
|---|---|
onChange |
Validation triggers on every change. |
onBlur |
Validation triggers on blur. |
onSubmit |
Validation triggers on submit. |
<script setup lang="ts">
const form = useForm({
defaultValues: {
title: '',
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})
</script>Displaying Errors
Display errors next to the field using FieldError. For styling and accessibility:
- Add the
:data-invalidprop to theFieldcomponent. - Add the
:aria-invalidprop to the form control such asInput,SelectTrigger,Checkbox, etc.
<script setup lang="ts">
function isInvalid(field) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</script>
<template>
<form.Field
name="email"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="field.name">Email</FieldLabel>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</template>Working with Different Field Types
Input
For input fields, use field.state.value and field.handleChange on the Input component.
To show errors, add the :aria-invalid prop to the Input component and the :data-invalid prop to the Field component.
::component-preview
name: TanStackFormInput class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
<template>
<form.Field
name="username"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="`form-tanstack-input-username`">Username</FieldLabel>
<FieldDescription>
This is your public display name. Must be between 3 and 10 characters.
Must only contain letters, numbers, and underscores.
</FieldDescription>
<FieldError v-if="isInvalid(field)" :errors="field.state.meta.errors" />
</Field>
</form.Field>
</template>Textarea
For textarea fields, use field.state.value and field.handleChange on the Textarea component.
To show errors, add the :aria-invalid prop to the Textarea component and the :data-invalid prop to the Field component.
::component-preview
name: TanStackFormTextarea class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true
::
<template>
<form.Field
name="about"
#default="{ field }"
>
<Field :data-invalid="isInvalid(field)">
<FieldLabel :for="`form-tanstack-textarea-about`">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-