shadcn-vue Documentation

TypeScript accessible & component-library repository by @unovue. Model Context Protocol (MCP) server & tools with 11k stars.

RAW Doc

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:

bash
# npm
npm install

# pnpm
pnpm install

# yarn
yarn install

# bun
bun install

Development Server

Start the development server on http://localhost:3000:

bash
# npm
npm run dev

# pnpm
pnpm dev

# yarn
yarn dev

# bun
bun run dev

Production

Build the application for production:

bash
# npm
npm run build

# pnpm
pnpm build

# yarn
yarn build

# bun
bun run build

Locally preview production build:

bash
# npm
npm run preview

# pnpm
pnpm preview

# yarn
yarn preview

# bun
bun run preview

Check 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.

bash
npx shadcn-vue create

The 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

bash
# 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 figtree

init

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.

bash
npx shadcn-vue init

Options

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.

bash
npx shadcn-vue add [component]

Example

bash
npx shadcn-vue add alert-dialog

You can also run the command without any arguments to view a list of all available components:

bash
npx shadcn-vue add

Visual 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

npm version
npm downloads
License
Nuxt

Shadcn Vue module for Nuxt.

Features

  • β›° Auto-import correct and relevant components
  • more to come...

Quick Setup

  1. Add shadcn-nuxt dependency to your project
bash
# Using pnpm
pnpm add -D shadcn-nuxt

# Using yarn
yarn add --dev shadcn-nuxt

# Using npm
npm install --save-dev shadcn-nuxt
  1. Add shadcn-nuxt to the modules section of nuxt.config.ts
js
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:

ts
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

bash
# 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 release

Overview (`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.

Getting Started Set up and build your own registry Examples Registry item examples and configurations registry.json Schema specification for registry.json registry-item.json Schema specification for registry items FAQ Frequently asked questions

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.

json showlinenumbers title="registry.json"
{
  "$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:

vue showlinenumbers title="registry/new-york/helloworld/helloworld.vue"
<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.

txt
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.

ts showlinenumbers
// 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.

json showlinenumbers title="registry.json" {6-17}
{
  "$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.

bash
npm install shadcn-vue@latest

Add a build script

Add a registry:build script to your package.json file.

json showlinenumbers title="package.json"
{
  "scripts": {
    "registry:build": "shadcn-vue build"
  }
}

Run the build script

Run the build script to generate the registry JSON files.

bash
npm run registry:build

Note: 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.

bash
npm run dev

Your 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.

**Note:** Make sure to encrypt and expire tokens.

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 using new-york as an example. It can be anything you want as long as it's nested under the registry directory.
  • The following properties are required for the block definition: name, description, type and files.
  • 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 the name@version format eg. zod@^3.20.0.
  • Imports should always use the @/registry path. eg. import { HelloWorld } from "@/registry/new-york/hello-world/hello-world"
  • Ideally, place your files within a registry item in components, hooks, lib directories.

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.

bash
npx shadcn-vue@latest add http://localhost:3000/r/hello-world.json

Install 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:

bash
npx shadcn-vue@latest add owner/repo/hello-world

The 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:

bash
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#1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b

Branches win over tags when a name is ambiguous, and an annotated tag resolves to
the commit it points at.

Listing what a repository publishes

bash
npx shadcn-vue@latest search owner/repo

Only 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/login installs the item
    named forms/login from the root registry.json. It is not a path to a
    nested registry.json.
  • The repository must be public. raw.githubusercontent.com does not serve
    private repositories. Use a hosted registry with auth instead.
  • Git is required. The CLI shells out to git ls-remote to resolve the ref,
    which is what lets it find the default branch instead of guessing main.
  • registryDependencies are resolved the usual way. A full URL is fetched
    as-is; a bare name such as button resolves against the default shadcn-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:

json title="components.json"
{
  "registries": {
    "@acme": "https://acme.com/r/{name}.json"
  }
}
bash
npx shadcn-vue@latest add @acme/hello-world

Apps 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.

json:line-numbers title="registry.json"
{
  "$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:

vue:line-numbers title="registry/new-york/helloworld/helloworld.vue"
<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.

txt
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.

ts:line-numbers
// 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.

json:line-numbers title="registry.json" {6-17}
{
  "$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.

bash
npm install shadcn-vue@latest

Add a build script

Add a registry:build script to your package.json file.

json:line-numbers title="package.json"
{
  "scripts": {
    "registry:build": "shadcn-vue build"
  }
}

Run the build script

Run the build script to generate the registry JSON files.

bash
npm run registry:build

Note: 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.

bash
npm run dev

Your 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.

**Note:** Make sure to encrypt and expire tokens.

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 using new-york as an example. It can be anything you want as long as it's nested under the registry directory.
  • The following properties are required for the block definition: name, description, type and files.
  • 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 the name@version format eg. zod@^3.20.0.
  • Imports should always use the @/registry path. eg. import { HelloWorld } from "@/registry/new-york/hello-world/hello-world"
  • Ideally, place your files within a registry item in components, hooks, lib directories.

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.

bash
npx shadcn-vue@latest add http://localhost:3000/r/hello-world.json

Deprecated 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's
base-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:

  1. Build time. scripts/lib/build-styles.ts (buildStyles()) reads the
    file, builds a token→utilities map (createStyleMap), and transforms every
    file in registry/bases/reka/ui/** via vue-metamorph, replacing cn-*
    tokens with their expansions. Output β†’ apps/v4/styles/reka-{name}/ui/**
    (a fully-overwritten build artifact). buildStylesRegistry() then publishes
    apps/v4/public/r/styles/reka-{name}/*.json for CLI installs.
    Both loop over the STYLES array and run inside pnpm registry:build.
  2. Runtime. assets/css/main.css @imports each style-{name}.css under
    layer(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 via
    tailwind-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.css defines 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
    *-logical direction 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, so rhea is 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:

ts
{
  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:

ts
{
  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:

css
@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 other reka-* style outputs.

Out of scope (explicit)

  • Default style unchanged. DEFAULT_CONFIG.style stays nova. rhea is
    additive/selectable.
  • Homepage cards repoint (@/styles/reka-nova/* β†’ @/styles/reka-rhea/*)
    is an optional follow-up, not part of this task. Can be done after rhea
    is verified if the landing page should visually match upstream shadcn/ui.
  • Full token reconciliation of drift-named edge components β€” deferred.

Verification plan

  1. buildStyles() runs clean; grep generated styles/reka-rhea/ui/** for
    leftover unexpanded cn-* tokens (expect only allowlisted ones, matching
    other styles).
  2. eslint the generated output (the build already runs eslint --fix).
  3. Boot dev server; render a .style-rhea-wrapped preview (or temporarily
    point a card demo at @/styles/reka-rhea/*) and visually confirm parity
    with React base-rhea.
  4. Confirm Rhea appears in the style switcher (PresetPicker) and selecting it
    renders correctly.
  5. Confirm public/r/styles/reka-rhea/*.json generated for CLI installs.

Success criteria

  • styles/reka-rhea/ui/** generated, compiles, no stray cn-*.
  • Rhea selectable in the customizer and renders faithfully to base-rhea.
  • Registry JSON published.
  • No regression to existing styles (shared base + pipeline untouched except the
    additive STYLES/PRESETS/main.css/style-rhea.css entries).

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 in
nuxt.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.ts swap the --font-sans / --font-heading CSS vars
    to a family string, but nothing loads the actual @font-face for the ~25 fonts
    in lib/fonts.ts when 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, no access-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/list and /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" in apps/v4/assets/css/main.css (currently
    commented out) and keep the @theme font vars as-is.
  • Add a plain Bunny stylesheet link to app.head.link in
    apps/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
    dynamic import('unifont') β†’
    createUnifont([providers.bunny()]). Created lazily on first loadFont call.
  • loadFont(family: string): Promise<void>:
    • No-op on the server (import.meta.client guard) and for empty/inherit values.
    • 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-face CSS from the returned FontFaceData[] (family, weight,
      style, font-display: swap, src list from RemoteFontSource/LocalFontSource,
      unicode-range).
    • Inject a single <style data-font="<family>"> element into document.head.
    • Wrap in try/catch β†’ on failure console.warn and resolve anyway
      (non-blocking; the CSS var is still set and the family falls back through the
      existing CSS font stack).

3. Wire into the design-system provider

In apps/v4/composables/useDesignSystemProvider.ts, the existing watcher applies
selectedFont / 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:

text
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 :root

Because 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 in
main.css. While commented, var(--font-sans) has no default value, so every
font-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 in
main.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 the main.css @theme inline block unchanged β€” its
    var(--font-*) mapping is what lets both the CSS default and the runtime JS
    override (documentElement.style.setProperty('--font-sans', …)) flow into the
    font-sans / font-heading / font-mono utilities.
  • Family-name match: the @font-face family that useFontLoader injects must
    exactly equal the primary family in the string useDesignSystemProvider writes
    to --font-sans (e.g. lib/fonts.ts fontFamily: "'Inter', sans-serif" β†’
    injected @font-face { font-family: 'Inter' }). Bunny returns the canonical
    Google family name, which matches lib/fonts.ts names, so this holds β€” but the
    builder must not rename the family.
  • The eager Bunny <link> (Part 1) provides the actual @font-face for the
    Geist / Geist Mono families that the --font-sans / --font-mono defaults
    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 in nuxt.config.ts.
  • Remove "@nuxt/fonts" from apps/v4/package.json dependencies.

Data flow

text
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: loadFont is a no-op on the server; injection happens client-only.

Out of scope

  • apps/v4/registry/fonts.ts β€” CLI-facing registry for consumers of the
    shadcn-vue CLI; separate concern, untouched.
  • The in-progress theme-color edits currently in the working tree
    (main.css oklch 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.css re-imported, inspect a font-sans
    element β†’ computed font-family resolves to "Geist", … (not empty / Arial-only)
    with no customizer JS run. Confirms @theme inline β†’ var(--font-sans) β†’
    fonts.css @theme default 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-tree not in
    the loaded JS.
  • Customizer: open the customizer, pick Inter β†’ Network shows a
    fonts.bunny.net request; 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.
text
---

### 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.

vue
<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:

ts
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.

Project Structure

The GitHub repository consists of the several folders. here's a quick view.

  1. packages -
    Contains source code for supporting nuxt as a module and the cli to add new components.

  2. apps/www -
    The main folder that holds the source code for the website and every shadcn/vue component. This folder contains important sub-folders and is a subproject with its own package.json.

  3. .vitepress -
    Contains the configuration and source code for vitepress and the shadcn/vue website.

  4. src -
    Hosts the main source code for every shadcn/vue component or demo and their documentation on the website.

  5. __registry__ -
    Holds the registry file generated by scripts/build-registry.ts to serve components for the cli. This folder's content is auto-generated and should not be edited manually.

  6. scripts -
    Contains various helper scripts, such as build-registry.ts, which automatically generates the __registry__ folder.

  7. content -
    This folder holds all the documentation for the /docs route. Each component has one .md file documenting the installation and usage of the component.

  8. examples -
    Holds all examples not part of /docs, like the main page.

  9. 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:

  1. Default
  2. 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:

  1. You make the changes for every style.
  2. You update the documentation.
  3. You run pnpm build:registry to update the registry.

Development

Start by cloning the repository:

bash
git clone [email protected]:unovue/shadcn-vue.git

Install dependencies

bash
pnpm install

Run 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

  1. To run the shadcn-vue.com website:
bash
pnpm dev
  1. To run the shadcn-vue cli package:
bash
pnpm dev:cli

Documentation

The documentation for this project is located in the www workspace. You can run the documentation locally by running the following command:

bash
pnpm dev

Documentation 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.

bash
pnpm test

Please 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 convention
category(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
    features

  • fix: 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 feature

  • docs: 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 dependencies

  • test: 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
    categories

    e.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.

vue
<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.

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.

vue
<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

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

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.

  1. 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.
  2. If there are changes, you should review thoese changes and try to apply them on shadcn/vue codebase and create a PR, remember to update the latestSyncCommitTag in 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 @theme directive and @theme inline option.
  • All components are updated for Tailwind v4.
  • Every primitive now has a data-slot attribute for styling.
  • We've fixed and cleaned up the style of the components.
  • We're deprecating the toast component in favor of sonner.
  • Buttons now use the default cursor.
  • We're deprecating the default style. New projects will use new-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

Vite

Nuxt

Nuxt

Astro

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@next codemod 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.

css showlinenumbers
@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:

  1. Move :root and .dark out of the @layer base.
  2. Wrap the color values in hsl()
  3. Add the inline option to @theme i.e @theme inline
  4. Remove the hsl() wrappers from @theme
css showlinenumbers
: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:

diff
- w-4 h-4
+ size-4

4. Install and Update your dependencies

bash
pnpm i tw-animate-css
pnpm up reka-ui @lucide/vue tailwind-merge clsx --latest

Apps 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.

How do I pull upstream updates in an Open Code approach?

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

Vite

Nuxt

Nuxt

Astro

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:

bash
npx shadcn-vue@latest init

See the CLI section for more information.

$schema

You can see the JSON Schema for components.json here.

json title="components.json"
{
  "$schema": "https://shadcn-vue.com/schema.json"
}

style

The style for your components. This cannot be changed after initialization.

json title="components.json"
{
  "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.

json title="components.json"
{
  "tailwind": {
    "config": "tailwind.config.js" | "tailwind.config.ts"
  }
}

tailwind.css

Path to the CSS file that imports Tailwind CSS into your project.

json title="components.json"
{
  "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.

json title="components.json"
{
  "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.

json title="components.json"
{
  "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.

json title="components.json"
{
  "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.

json title="components.json"
{
  "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.

json title="components.json"
{
  "aliases": {
    "utils": "@/lib/utils"
  }
}

aliases.components

Import alias for your components.

json title="components.json"
{
  "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.

json title="components.json"
{
  "aliases": {
    "ui": "@/app/ui"
  }
}

aliases.lib

Import alias for lib functions such as cn.

json title="components.json"
{
  "aliases": {
    "lib": "@/lib"
  }
}

aliases.composables

Import alias for composables such as useMediaQuery or useToast.

json title="components.json"
{
  "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.

vue /bg-background/ /text-foreground/

To use CSS variables for theming, set tailwind.cssVariables to true in your components.json file. This is the default.

json {8} title="components.json" showlinenumbers
{
  "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:

css
--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).

vue
Hello

Theme 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.

css title="assets/css/tailwind.css" showlinenumbers
@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-lg is the base value.
  • Smaller radii scale down from --radius.
  • Larger radii scale up from --radius.
  • Changing --radius updates 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.

css title="assets/css/tailwind.css" showlinenumbers
: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.

vue /bg-warning/ /text-warning-foreground/

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

text
/* 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.

bash
npx shadcn-vue@latest init --no-css-variables

This sets tailwind.cssVariables to false in your components.json file.

vue /bg-zinc-950/ /text-zinc-50/ /dark:bg-white/ /dark:text-zinc-950/

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

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.

bash
npx shadcn-vue@latest init

Options

text
/* Detailed source-code truncated for AI context efficiency. */

The create command is an alias for init:

bash
npx shadcn-vue@latest create

add

Use the add command to add components and dependencies to your project.

bash
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

bash
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 command

apply

Use the apply command to apply a preset to an existing project.

bash
npx shadcn-vue@latest apply --preset nova

Options

bash
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 command

view

Use the view command to view items from the registry before installing them.

bash
npx shadcn-vue@latest view [item]

You can view multiple items at once:

bash
npx shadcn-vue@latest view button card dialog

Or view items from namespaced registries:

bash
npx shadcn-vue@latest view @acme/auth @v0/dashboard

Options

bash
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 command

Use the search command to search for items from registries.

bash
npx shadcn-vue@latest search [registry]

You can search with a query:

bash
npx shadcn-vue@latest search @shadcn-vue -q "button"

Or search multiple registries at once:

bash
npx shadcn-vue@latest search @shadcn-vue @v0 @acme

The list command is an alias for search:

bash
npx shadcn-vue@latest list @acme

Options

bash
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 command

build

Use the build command to generate the registry JSON files.

bash
npx shadcn-vue@latest build

This command reads the registry.json file and generates the registry JSON files in the public/r directory.

Options

bash
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 command

To customize the output directory, use the --output option.

bash
npx shadcn-vue@latest build --output ./public/registry

docs

Use the docs command to fetch documentation and API references for components.

bash
npx shadcn-vue@latest docs [component]

Options

bash
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 command

info

Use the info command to get information about your project.

bash
npx shadcn-vue@latest info

Options

bash
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 command

migrate

Use the migrate command to run migrations on your project.

bash
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

bash
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 command

migrate rtl

The rtl migration transforms your components to support RTL (right-to-left) languages.

bash
npx shadcn-vue@latest migrate rtl

This will:

  1. Update components.json to set rtl: true
  2. Transform physical CSS properties to logical equivalents (e.g., ml-4 β†’ ms-4, text-left β†’ text-start)
  3. 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:

bash
# 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.

json {9} title="components.json" showlinenumbers
{
  "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:

json {4} title="jsconfig.json" showlinenumbers
{
  "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:

bash
npx shadcn-vue@latest create --rtl

This will create a components.json file with the rtl: true flag.

json title="components.json" showlinenumbers {4}
{
  "$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:

html title="index.html" showlinenumbers {2}
<!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:

vue title="app.vue" showlinenumbers
<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:

vue title="app.vue" showlinenumbers {2,6,10}
<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:

bash
npm install @fontsource-variable/noto-sans-arabic

Import the font in your css file:

css title="src/assets/index.css" showlinenumbers {3,6}
@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.

bash
npx shadcn-vue@latest add card

How 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-* and right-* are converted to logical equivalents like start-* and end-*.
  • 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.

vue showlinenumbers
Open
  
    Content
vue showlinenumbers
<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

bash
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.

bash
# 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.

vue showlinenumbers
<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.

  • 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


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

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.

css
.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.

bash
npx shadcn-vue@latest add questionnaire

See 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.

vue
<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:

bash
npx shadcn-vue@latest add drawer

We 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.

ts
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. 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:

vue
<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.

vue
<script setup lang="ts">
import { Kbd, KbdGroup } from '@/components/ui/kbd'
</script>

<template>
  <Kbd>Ctrl</Kbd>
</template>

Use KbdGroup to group keyboard keys together.

vue showlinenumbers
<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:

vue showlinenumbers
<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.

vue showlinenumbers
<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]"

::

vue showlinenumbers
<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.

vue showlinenumbers
<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:

vue showlinenumbers
<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.

vue showlinenumbers
<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:

vue showlinenumbers
<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:

vue showlinenumbers
<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:

vue showlinenumbers
<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.
::

  1. First up, when you init into a new app, we update your existing Tailwind files instead of overriding.

  2. 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.

  3. You can also install remote components using url. npx shadcn-vue add https://acme.com/registry/navbar.json.

  4. 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.

  5. And a few more updates like better error handling and monorepo support.

You can try the new cli today.

bash
npx shadcn-vue@latest init Sidebar01 Login01

Apps 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.

View the legacy docs

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.

json title="components.json" showlinenumbers
{
  "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
::

text
::tabs-trigger{value="opencode"}
Opencode
::

:::

::tabs-content{value="claude"}
Run the following command in your project:

bash
npx shadcn-vue@latest mcp init --client claude

Restart 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:

bash
npx shadcn-vue@latest mcp init --client cursor

Open 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:

bash
npx shadcn-vue@latest mcp init --client vscode

Open .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:

bash
npx shadcn-vue@latest mcp init --client codex

Then, add the following configuration to ~/.codex/config.toml:

json title=".mcp.json" showlinenumbers
[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:

bash
npx shadcn-vue@latest mcp init --client opencode

Restart 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.

  1. Registry Connection - MCP connects to configured registries (shadcn/ui, private registries, third-party sources)
  2. Natural Language - You describe what you need in plain English
  3. AI Processing - The assistant translates your request into registry commands
  4. 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 @namespace syntax

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:

json title=".mcp.json" showlinenumbers
{
  "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:

json title=".cursor/mcp.json" showlinenumbers
{
  "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:

json title=".vscode/mcp.json" showlinenumbers
{
  "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:

toml title="~/.codex/config.toml" showlinenumbers
[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:

json title="opencode.json" showlinenumbers
{
    "$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:

json title="components.json" showlinenumbers
{
  "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:

bash title=".env.local"
REGISTRY_TOKEN=your_token_here
API_KEY=your_api_key_here

For 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:

  • 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:

  1. Check Configuration - Verify the MCP server is properly configured and enabled in your MCP client
  2. Restart MCP Client - Restart your MCP client after configuration changes
  3. Verify Installation - Ensure shadcn-vue is installed in your project
  4. Check Network - Confirm you can access the configured registries

Registry Access Issues

If components aren't loading from registries:

  1. Check components.json - Verify registry URLs are correct
  2. Test Authentication - Ensure environment variables are set for private registries
  3. Verify Registry - Confirm the registry is online and accessible
  4. Check Namespace - Ensure namespace syntax is correct (@namespace/component)

Installation Failures

If components fail to install:

  1. Check Project Setup - Ensure you have a valid components.json file
  2. Verify Paths - Confirm the target directories exist
  3. Check Permissions - Ensure write permissions for component directories
  4. Review Dependencies - Check that required dependencies are installed

No Tools or Prompts

If you see the No tools or prompts message, try the following:

  1. Clear the npx cache - Run npx clear-npx-cache
  2. Re-enable the MCP server - Try to re-enable the MCP server in your MCP client
  3. Check Logs - In Cursor, you can see the logs under View -> Output and select MCP: project-* in the dropdown.

Learn More


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

bash
npx skills add unovue/shadcn-vue

This 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

  1. Project detection β€” The skill activates when it finds a components.json file in your project.
  2. Context injection β€” It runs shadcn-vue info --json to read your project configuration and injects the result into the assistant's context.
  3. Pattern enforcement β€” The assistant follows shadcn-vue composition rules: using FieldGroup for forms, ToggleGroup for option sets, semantic colors, and correct base-specific APIs.
  4. 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.

css
.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;
}

Build your typeset


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:

css
@import "tailwindcss";
@import "./typeset.css";

Then wrap your rendered markdown with typeset and your preset class:

vue
<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:

css
.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-size sets the base text size. 1em follows the surrounding layout. On smaller screens, Typeset bumps it up a little.
  • --typeset-leading sets the space between lines.
  • --typeset-flow sets 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:

css
.typeset-chat {
  --typeset-flow: 1em;
  --typeset-leading: 1.6;
}

.typeset-docs {
  --typeset-size: 15px;
  --typeset-flow: 1.5em;
}
vue
<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:

vue
<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.

css
/* 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:

css
.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:

css
.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:

vue
<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:

vue
<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:

vue
<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 :empty are left out of layout rules because their matches can change as content is added.
  • Spacing flows in one direction, using margin-block-start only. 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`)


::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"}

bash
npx shadcn-vue@latest add accordion

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from '@/components/ui/accordion'
</script>

<template>
  <Accordion type="single" collapsible>
    <AccordionItem value="item-1">
      <AccordionTrigger>Is it accessible?</AccordionTrigger>
      <AccordionContent>
        Yes. It adheres to the WAI-ARIA design pattern.
      </AccordionContent>
    </AccordionItem>
  </Accordion>
</template>

Apps V4 Content Docs Components Alert Dialog (`apps/v4/content/docs/components/alert-dialog.md`)


::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"}

bash
npx shadcn-vue@latest add alert-dialog

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
</script>

<template>
  <AlertDialog>
    <AlertDialogTrigger>Open</AlertDialogTrigger>
    <AlertDialogContent>
      <AlertDialogHeader>
        <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
        <AlertDialogDescription>
          This action cannot be undone. This will permanently delete your account
          and remove your data from our servers.
        </AlertDialogDescription>
      </AlertDialogHeader>
      <AlertDialogFooter>
        <AlertDialogCancel>Cancel</AlertDialogCancel>
        <AlertDialogAction>Continue</AlertDialogAction>
      </AlertDialogFooter>
    </AlertDialogContent>
  </AlertDialog>
</template>

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"}

bash
npx shadcn-vue@latest add alert

::

::::tabs-content{value="manual"}
:::steps
::step
Copy and paste the GitHub source code into your project.
::

text
::step
Update the import paths to match your project setup.
::

:::
::::

:::::

Usage

vue showlinenumbers
<script setup lang="ts">
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
</script>

<template>
  <Alert>
    <AlertTitle>Heads up!</AlertTitle>
    <AlertDescription>
      You can add components and dependencies to your app using the cli.
    </AlertDescription>
  </Alert>
</template>

Apps V4 Content Docs Components Aspect Ratio (`apps/v4/content/docs/components/aspect-ratio.md`)


::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"}

bash
npx shadcn-vue@latest add aspect-ratio

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<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"}

bash
npx shadcn-vue@latest add attachment

::

::::tabs-content{value="manual"}
:::steps
::step
Copy and paste the GitHub source code into your project.
::

text
::step
Update the import paths to match your project setup.
::

:::
::::

:::::

Usage

vue showlinenumbers
<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:

text
Attachment
β”œβ”€β”€ AttachmentMedia
β”œβ”€β”€ AttachmentContent
β”‚   β”œβ”€β”€ AttachmentTitle
β”‚   └── AttachmentDescription
β”œβ”€β”€ AttachmentActions
β”‚   └── AttachmentAction
└── AttachmentTrigger

Use AttachmentGroup to lay out multiple attachments in a scrollable row:

text
AttachmentGroup
β”œβ”€β”€ Attachment
└── Attachment

Features

  • Icon and image media through AttachmentMedia
  • Upload states: idle, uploading, processing, error, and done with built-in styling and a shimmer while in progress
  • Three sizes and horizontal or vertical orientation
  • A full-card AttachmentTrigger that opens a link or dialog while the actions stay independently clickable
  • Scrollable, snapping AttachmentGroup with an edge fade
  • Customizable styling through the Vue class attribute 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

::

vue showlinenumbers
<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.

vue showlinenumbers
<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.

vue showlinenumbers
<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`)


::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"}

bash
npx shadcn-vue@latest add avatar

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
</script>

<template>
  <Avatar>
    <AvatarImage src="https://github.com/shadcn.png" />
    <AvatarFallback>CN</AvatarFallback>
  </Avatar>
</template>

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"}

bash
npx shadcn-vue@latest add badge

::

::::tabs-content{value="manual"}
:::steps
::step
Copy and paste the GitHub source code into your project.
::

text
::step
Update the import paths to match your project setup.
::

:::
::::

:::::

Usage

vue showlinenumbers
<script setup lang="ts">
import { Badge } from '@/components/ui/badge'
</script>

<template>
  
    Badge
  
</template>

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.

vue showlinenumbers
<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`)


::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"}

bash
npx shadcn-vue@latest add breadcrumb

::

::::tabs-content{value="manual"}
:::steps
::step
Copy and paste the GitHub source code into your project.
::

text
::step
Update the import paths to match your project setup.
::

:::
::::

:::::

Usage

vue showlinenumbers
<script setup lang="ts">
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
</script>

<template>
  
    
      
        
          Home
        
      
      
      
        
          Components
        
      
      
      
        Breadcrumb
      
    
  
</template>

Examples

Custom separator

Use a custom component as children for <BreadcrumbSeparator /> to create a custom separator.

::component-preview

name: BreadcrumbCustomSeparatorDemo

::

vue showlinenumbers {2,12-14}
<script setup lang="ts">
import { SlashIcon } from '@lucide/vue'
// ...
</script>

<template>
  
    
      
        
          Home
        
      
      
        <SlashIcon />
      
      
        
          Components
        
      
    
  
</template>

You can compose <BreadcrumbItem /> with a <DropdownMenu /> to create a dropdown in the breadcrumb.

::component-preview

name: BreadcrumbDropdownDemo

::

vue showlinenumbers {2-7,13-22}
<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

::

vue showlinenumbers {2,11}
<script setup lang="ts">
import { BreadcrumbEllipsis } from "@/components/ui/breadcrumb"
//...
</script>

<template>
  
    
      
      
        
      
    
    
  
</template>

To use a custom link component from your routing library, you can use the as-child prop on <BreadcrumbLink />.

::component-preview

name: BreadcrumbLinkDemo

::

vue showlinenumbers {2,10-12}
<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"}

bash
npx shadcn-vue@latest add bubble

::

::::tabs-content{value="manual"}
:::steps
::step
Copy and paste the GitHub source code into your project.
::

text
::step
Update the import paths to match your project setup.
::

:::
::::

:::::

Usage

vue showlinenumbers
<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:

text
Bubble
β”œβ”€β”€ BubbleContent
└── BubbleReactions

Use BubbleGroup to group consecutive bubbles from the same sender:

text
BubbleGroup
β”œβ”€β”€ Bubble
β”‚   └── BubbleContent
└── Bubble
    └── BubbleContent

Features

  • 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 as or as-child for link and button bubbles
  • Customizable styling through the class prop 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.

text
BubbleGroup
β”œβ”€β”€ Bubble
β”‚   └── BubbleContent
└── Bubble
    └── BubbleContent

::component-preview

name: BubbleGroupDemo previewClass: h-auto theme-blue

::

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

::

vue showlinenumbers
<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.

vue showlinenumbers
πŸ‘
  πŸ”₯
  +8

When reactions are interactive, render buttons instead and give icon-only buttons an aria-label.

vue showlinenumbers

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.

vue showlinenumbers
I forgot my password

Meaning 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`)


::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"}

bash
npx shadcn-vue@latest add button-group

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue
<script setup lang="ts">
import {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from '@/components/ui/button-group'
</script>

<template>
  
    Button 1
    Button 2
  
</template>

Accessibility

  • The ButtonGroup component has the role attribute set to group.
  • Use Tab to navigate between the buttons in the group.
  • Use aria-label or aria-labelledby to label the button group.
vue
<template>
  
    Button 1
    Button 2
  
</template>

ButtonGroup vs ToggleGroup

  • Use the ButtonGroup component when you want to group buttons that perform an action.
  • Use the ToggleGroup component 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

::

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"
vue
<template>
  
    Button 1
    Button 2
  
</template>

Nest multiple button groups to create complex layouts with spacing. See the nested example for more details.

vue
<template>
  
    
    
  
</template>

ButtonGroupSeparator

The ButtonGroupSeparator component visually divides buttons within a group.

Prop Type Default
orientation "horizontal" | "vertical" vertical
vue
<template>
  
    Button 1
    
    Button 2
  
</template>

ButtonGroupText

Use this component to display text within a button group.

Prop Type Default
as-child boolean false
vue
<template>
  
    Text
    Button
  
</template>

Use the as-child prop to render a custom component as the text, for example a label.

vue
<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`)


::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"}

bash
npx shadcn-vue@latest add button

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<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.

css showlinenumbers title="tailwind.css"
@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

::

::component-preview

::

Icon

::component-preview

name: ButtonIcon class: mb-4

::

vue showlinenumbers
<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.

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.

vue showlinenumbers
<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`)


::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"}

bash
npx shadcn-vue@latest add calendar

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar'
</script>

<template>
  <Calendar />
</template>

Calendar Systems (Persian / Hijri / Jalali for example)

@internationalized/date 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

vue
<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

vue showlinenumbers
<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.

ts
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`)


::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"}

bash
npx shadcn-vue@latest add card

::

::::tabs-content{value="manual"}
:::steps
::step
Copy and paste the GitHub source code into your project.
::

text
::step
Update the import paths to match your project setup.
::

:::
::::

:::::

Usage

vue showlinenumbers
<script setup lang="ts">
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card'
</script>

<template>
  <Card>
    <CardHeader>
      <CardTitle>Card Title</CardTitle>
      <CardDescription>Card Description</CardDescription>
    </CardHeader>
    <CardContent>
      Card Content
    </CardContent>
    <CardFooter>
      Card Footer
    </CardFooter>
  </Card>
</template>


::component-preview

::

Installation

:::::code-tabs

:::tabs-list

::tabs-trigger{value="cli"}
CLI
::

::tabs-trigger{value="manual"}
Manual
::

:::

::tabs-content{value="cli"}

bash
npx shadcn-vue@latest add carousel

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from '@/components/ui/carousel'
</script>

<template>
  <Carousel>
    <CarouselContent>
      <CarouselItem>...</CarouselItem>
      <CarouselItem>...</CarouselItem>
      <CarouselItem>...</CarouselItem>
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
</template>

Examples

Sizes

To set the size of the items, you can use the basis utility class on the <CarouselItem />.

::component-preview

::

vue showlinenumbers {5-7}
// 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>
vue showlinenumbers {5-7}
// 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

::

vue showlinenumbers /-ml-4/ /pl-4/
<template>
  <Carousel>
    <CarouselContent class="-ml-4">
      <CarouselItem class="pl-4">
        ...
      </CarouselItem>
      <CarouselItem class="pl-4">
        ...
      </CarouselItem>
      <CarouselItem class="pl-4">
        ...
      </CarouselItem>
    </CarouselContent>
  </Carousel>
</template>
vue showlinenumbers /-ml-2/ /pl-2/ /md:-ml-4/ /md:pl-4/
<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

::

vue
<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.

vue showlinenumbers {3-6}
<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

::

Method 2

You can access it through setting a template ref on the <Carousel /> component.

vue showlinenumbers {2,5,10}
<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

vue showlinenumbers {5,7-9,25}
<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.

vue showlinenumbers {2}
<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.

bash
npm install embla-carousel-autoplay
vue showlinenumbers {2,8-10}
<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

::

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.

Browse the Charts Library.

Component

We use Unovis under the hood.

We designed the chart component with composition in mind. You build your charts using Unovis components and only bring in custom components, such as ChartTooltip, when and where you need it.

vue showlinenumbers
<script setup lang="ts">
import { VisGroupedBar, VisXYContainer } from '@unovis/vue'
import { ChartContainer, ChartTooltipContent } from '@/components/ui/chart'
</script>

<template>
  <ChartContainer :config="chartConfig">
    <VisXYContainer :data="data">
      <VisGroupedBar :x="(d) => d.month" :y="(d) => d.value" />
      <ChartTooltip :template="componentToString(chartConfig, ChartTooltipContent)" />
    </VisXYContainer>
  </ChartContainer>
</template>

We do not wrap Unovis. This means you're not locked into an abstraction. When a new Unovis version is released, you can follow the official upgrade path to upgrade your charts.

The components are yours.

Installation

:::::code-tabs

:::tabs-list

::tabs-trigger{value="cli"}
CLI
::

::tabs-trigger{value="manual"}
Manual
::

:::

::tabs-content{value="cli"}

bash
npx shadcn-vue@latest add chart

::

::::tabs-content{value="manual"}

:::steps
::step
Install the following dependencies
::

text
```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

text
/* 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.

ts showlinenumbers
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.

ts showlinenumbers
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
::

vue showlinenumbers
import { VisAxis, VisGroupedBar, VisXYContainer } from '@unovis/vue'

::step
Add the VisAxis components to your chart
::

vue showlinenumbers
<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
::

ts
import { ChartTooltip, ChartTooltipContent, componentToString } from '@/components/ui/chart'

::step
Add the components to your chart
::

vue showlinenumbers
<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.
::

ts
import { ChartLegendContent } from '@/components/ui/chart'

::step
Add the components to your chart.
::

vue showlinenumbers {4}
<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.

vue showlinenumbers
<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
::

css showlinenumbers
@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
::

ts showlinenumbers {4,8}
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.

ts showlinenumbers
const chartConfig = {
  desktop: {
    label: 'Desktop',
    color: '#2563eb',
  },
} satisfies ChartConfig

Using Colors

To use the theme colors in your chart, reference the colors using the format var(--color-KEY).

Components

vue showlinenumbers
<VisGroupedBar
  :x="(d) => d.month"
  :y="(d) => d.desktop"
  color="var(--color-desktop)"
/>

Chart Data

ts showlinenumbers
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.

ts showlinenumbers
import { ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
vue showlinenumbers
<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.

ts showlinenumbers
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
vue showlinenumbers
<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.

ts
import { ChartLegendContent } from '@/components/ui/chart'
vue
<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.

tsx showlinenumbers /browser/
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
vue
<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`)


::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"}

bash
npx shadcn-vue@latest add checkbox

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<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.

vue showlinenumbers
<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`)


::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"}

bash
npx shadcn-vue@latest add collapsible

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from '@/components/ui/collapsible'
</script>

<template>
  <Collapsible>
    <CollapsibleTrigger>Can I use this in my project?</CollapsibleTrigger>
    <CollapsibleContent>
      Yes. Free to use for personal and commercial projects. No attribution
      required.
    </CollapsibleContent>
  </Collapsible>
</template>

Apps V4 Content Docs Components Combobox (`apps/v4/content/docs/components/combobox.md`)


::component-preview

name: ComboboxDemo description: A combobox with a list of frameworks.

::

A combobox can be built in two ways:

  • Using the Combobox components, built on top of the Reka UI Combobox.
  • Composing the Popover and the Command components as a listbox.

Installation

Combobox

:::::code-tabs

:::tabs-list

::tabs-trigger{value="cli"}
CLI
::

::tabs-trigger{value="manual"}
Manual
::

:::

::tabs-content{value="cli"}

bash
npx shadcn-vue@latest add combobox

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

text
/* Detailed source-code truncated for AI context efficiency. */

Popover and Command

text
/* 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`)


::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"}

bash
npx shadcn-vue@latest add command

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from '@/components/ui/command'
</script>

<template>
  <Command>
    <CommandInput placeholder="Type a command or search..." />
    <CommandList>
      <CommandEmpty>No results found.</CommandEmpty>
      <CommandGroup heading="Suggestions">
        <CommandItem>Calendar</CommandItem>
        <CommandItem>Search Emoji</CommandItem>
        <CommandItem>Calculator</CommandItem>
      </CommandGroup>
      <CommandSeparator />
      <CommandGroup heading="Settings">
        <CommandItem>Profile</CommandItem>
        <CommandItem>Billing</CommandItem>
        <CommandItem>Settings</CommandItem>
      </CommandGroup>
    </CommandList>
  </Command>
</template>

Apps V4 Content Docs Components Context Menu (`apps/v4/content/docs/components/context-menu.md`)


::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"}

bash
npx shadcn-vue@latest add context-menu

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<script setup lang="ts">
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from '@/components/ui/context-menu'
</script>

<template>
  <ContextMenu>
    <ContextMenuTrigger>Right click</ContextMenuTrigger>
    <ContextMenuContent>
      <ContextMenuItem>Profile</ContextMenuItem>
      <ContextMenuItem>Billing</ContextMenuItem>
      <ContextMenuItem>Team</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem>Subscription</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</template>

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:

Installation

  1. Add the <Table /> component to your project:
bash
npx shadcn-vue@latest add table
  1. Add the @tanstack/vue-table dependency. This guide uses TanStack Table v9:
bash
npm install @tanstack/vue-table

Prerequisites

We are going to build a table to show recent payments. Here's what our data looks like:

ts showlinenumbers
interface Payment {
  id: string
  amount: number
  status: 'pending' | 'processing' | 'success' | 'failed'
  email: string
}

export const payments: Payment[] = [
  {
    id: '728ed52f',
    amount: 100,
    status: 'pending',
    email: '[email protected]',
  },
  {
    id: '489e1d42',
    amount: 125,
    status: 'processing',
    email: '[email protected]',
  },
  // ...
]

Project Structure

Start by creating the following file structure:

ansi
components
    └── payments
          β”œβ”€β”€ columns.ts
          β”œβ”€β”€ features.ts
          β”œβ”€β”€ data-table.vue
          β”œβ”€β”€ data-table-dropdown.vue
└── app.vue

I'm using a Nuxt example here but this works for any other Vue framework.

  • columns.ts It will contain our column definitions.
  • features.ts It will contain the shared features object that tells TanStack Table which behavior to enable.
  • data-table.vue It will contain our <DataTable /> component.
  • data-table-dropdown.vue It will contain our <DropdownAction /> component.
  • app.vue This is where we'll fetch data and render our table.

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.

ts showlinenumbers
// 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.

ts showlinenumbers
// 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.

text
/* 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.

vue
<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:

ts
// 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

vue
<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.

ts
// 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.

vue
<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().

vue
<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.

ts showlinenumbers {2-3,8-13}
// 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

vue
<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

vue
<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

ts showlinenumbers {2,5-19}
// 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:

ts showlinenumbers {2-3,5-7,13-18}
// 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.

vue showlinenumbers {8-11}
<template>
  
    
        
</template>

Apps V4 Content Docs Components Tabs (`apps/v4/content/docs/components/tabs.md`)


::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"}

bash
npx shadcn-vue@latest add tabs

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<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`)


::component-preview

name: TagsInputDemo

::

Installation

:::::code-tabs

:::tabs-list

::tabs-trigger{value="cli"}
CLI
::

::tabs-trigger{value="manual"}
Manual
::

:::

::tabs-content{value="cli"}

bash
npx shadcn-vue@latest add tags-input

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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"}

bash
npx shadcn-vue@latest add textarea

::

::::tabs-content{value="manual"}
:::steps
::step
Copy and paste the GitHub source code into your project.
::

text
::step
Update the import paths to match your project setup.
::

:::
::::

:::::

Usage

vue showlinenumbers
<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`)


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`)


::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"}

bash
npx shadcn-vue@latest add toggle-group

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<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`)


::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"}

bash
npx shadcn-vue@latest add toggle

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<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.

vue showlinenumbers
<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`)


::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"}

bash
npx shadcn-vue@latest add tooltip

::

::::tabs-content{value="manual"}
:::steps
::step
Install the following dependencies:
::

text
```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

vue showlinenumbers
<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

bash
npm install @vueuse/core

Optional, to include icons for theme button.

bash
npm install -D @iconify/vue @iconify-json/radix-icons

Add 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.

vue title="components/modetoggle.vue"
<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

bash
npm install -D @nuxtjs/color-mode

Then, add @nuxtjs/color-mode to the modules section of your nuxt.config.ts

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.

bash
npm install -D @iconify/vue @iconify-json/radix-icons
vue title="components/modetoggle.vue"
<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

bash
npm install @vueuse/core

Optional, to include icons for theme button.

bash
npm install -D @iconify/vue @iconify-json/radix-icons

Add 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.

vue title="components/modetoggle.vue"
<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

astro title="src/pages/index.astro"
---
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

bash
npm install @vueuse/core

Optional, to include icons for theme button.

bash
npm install -D @iconify/vue @iconify-json/radix-icons

Add 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.

vue title="components/modetoggle.vue"
<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.

astro title="src/pages/index.astro"
---
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`)


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 useForm composable 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

text
::tabs-trigger{value="component"}
Component
::

::tabs-trigger{value="native"}
Native
::

:::

::tabs-content{value="component"}

`Input` Component

vue showlinenumbers {2,9}
<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

vue showlinenumbers {2,9}
<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.
::

vue showlinenumbers title="form.vue"
<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.

vue showlinenumbers title="form.vue" {17-23}
<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.

vue showlinenumbers title="exampleform.vue" {6-9,12}
<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.

vue showlinenumbers title="form.vue" {4}
<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-invalid prop to the shadcn-vue <Field /> component.
  • Add the :aria-invalid prop to the form control such as <Input />, <SelectTrigger />, <Checkbox />, etc.
vue showlinenumbers title="form.vue" {2,9,11}
<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-invalid prop to the <Input /> component and the :data-invalid prop 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.

vue showlinenumbers title="form.vue" {3,9,11}
<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-invalid prop to the <Textarea /> component and the :data-invalid prop 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.

vue showlinenumbers title="form.vue" {3,9,12}
<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 binds modelValue, update:modelValue and name in one go.
  • To show errors, add the :aria-invalid prop to the <SelectTrigger /> component and the :data-invalid prop to the shadcn-vue <Field /> component.

::component-preview

name: VeeValidateSelectDemo class: sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px] chromeLessOnMobile: true

::

vue showlinenumbers title="form.vue" {2,11,15}
<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 set type="checkbox" on VeeValidate's <Field />.
  • For checkbox arrays, one field holds the whole array, so bind the value and handleChange slot props by hand instead.
  • To show errors, add the :aria-invalid prop to the <Checkbox /> component and the :data-invalid prop 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

::

vue showlinenumbers title="form.vue" {2,12}
<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.

vue showlinenumbers title="form.vue" {2,17,19-23}
<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 own value.
  • To show errors, add the :aria-invalid prop to the <RadioGroup /> component and the :data-invalid prop to the shadcn-vue <Field /> component.

::component-preview

name: VeeValidateRadioGroupDemo class: sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px] chromeLessOnMobile: true

::

vue showlinenumbers title="form.vue" {2,8-11}
<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 set type="checkbox" on VeeValidate's <Field /> so the value is treated as a boolean.
  • To show errors, add the :aria-invalid prop to the <Switch /> component and the :data-invalid prop to the shadcn-vue <Field /> component.

::component-preview

name: VeeValidateSwitchDemo class: sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px] chromeLessOnMobile: true

::

vue showlinenumbers title="form.vue" {2,15}
<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.

vue showlinenumbers
<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.

vue showlinenumbers title="form.vue" {6-8}
<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 />.

vue showlinenumbers title="form.vue"
<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.

vue showlinenumbers title="form.vue"
<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.

vue showlinenumbers title="form.vue"
<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.

vue showlinenumbers title="form.vue"
<template>
   1" align="inline-end">
    
      <XIcon />
    
  
</template>

Array Validation

Use Zod's array method to validate array fields.

ts showlinenumbers title="form.vue"
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`)


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 useForm composable for form state management.
  • form.Field components 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.

vue showlinenumbers {10-26}
<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.

vue showlinenumbers
<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.

vue showlinenumbers
<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.

vue showlinenumbers
<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.
vue showlinenumbers {7-11}
<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-invalid prop to the Field component.
  • Add the :aria-invalid prop to the form control such as Input, SelectTrigger, Checkbox, etc.
vue showlinenumbers
<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

::

vue showlinenumbers {6, 11-14, 22}
<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

::

vue showlinenumbers {6,13-16,24}
<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-