# @vuetify/v0 - Complete Documentation > Headless Vue 3 UI primitives and composables for building modern applications and design systems. --- # Browser Support URL: https://0.vuetifyjs.com/introduction/browser-support # Browser Support Vuetify0 targets modern evergreen browsers. Some features require newer browser versions or degrade gracefully when unavailable. ## Baseline Support Vuetify0 requires Vue 3.5+, which targets browsers with native [ES2016 support](https://caniuse.com/es6): | Browser | Minimum Version | |---------|----------------:| | Chrome | 52+ | | Edge | 79+ | | Firefox | 52+ | | Safari | 10.1+ | | Opera | 39+ | > [!WARNING] > Internet Explorer is not supported and never will be. ## Feature Compatibility Some Vuetify0 features use modern browser APIs that have varying levels of support. The library includes feature detection and graceful degradation where possible. ### Cutting-Edge Features These features require the latest browser versions and may not work in all browsers: | Feature | | | | | Fallback | |---------|---:|---:|---:|---:|----------| | [CSS Anchor Positioning](https://caniuse.com/css-anchor-positioning) | 125+ | 147+ | 26+ | 125+ | Properties ignored | | [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) | 114+ | 125+ | 17+ | 114+ | Optional chaining | | [Scrollend Event](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event) | 114+ | 109+ | 26.2+ | 114+ | `createVirtual` `scrollend()` aliases `scroll`; no automatic event fallback | | [Temporal](https://caniuse.com/temporal) | 144+ | 139+ | —[^safari-temporal] | 144+ | `@js-temporal/polyfill` | [^safari-temporal]: Temporal is not yet in stable Safari (available in Safari Technology Preview). `useDate`'s `V0DateAdapter` automatically uses the [@js-temporal/polyfill](https://www.npmjs.com/package/@js-temporal/polyfill) peer when native Temporal is absent. The API reached TC39 Stage 4 (ES2026) in 2026. ### Well-Supported Features These features have broad browser support with proper feature detection: | Feature | | | | | Fallback | |---------|---:|---:|---:|---:|----------| | [Native Dialog](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal) | 37+ | 98+ | 15.4+ | 79+ | Optional chaining | | [ResizeObserver](https://caniuse.com/resizeobserver) | 64+ | 69+ | 13.1+ | 79+ | Feature detection | | [IntersectionObserver](https://caniuse.com/intersectionobserver) | 51+ | 55+ | 12.1+ | 79+ | Feature detection | | [MutationObserver](https://caniuse.com/mutationobserver) | 26+ | 14+ | 6+ | 12+ | Feature detection | | [matchMedia](https://caniuse.com/matchmedia) | 10+ | 6+ | 5.1+ | 12+ | Feature detection | | [Pointer Events](https://caniuse.com/pointer) | 55+ | 59+ | 13.1+ | 79+ | Assumed available | ### SSR & Hydration Safety All composables use the `IN_BROWSER` constant and `useHydration` composable to ensure safe server-side rendering: ```ts import { IN_BROWSER, useHydration } from '@vuetify/v0' const { isHydrated } = useHydration() // Browser APIs are only accessed after hydration if (isHydrated.value && IN_BROWSER) { // Safe to use browser APIs } ``` ## Feature Detection Constants Vuetify0 exposes feature detection constants for conditional logic: ```ts import { IN_BROWSER, SUPPORTS_TOUCH, SUPPORTS_MATCH_MEDIA, SUPPORTS_OBSERVER, SUPPORTS_INTERSECTION_OBSERVER, SUPPORTS_MUTATION_OBSERVER, } from '@vuetify/v0' ``` ## Documentation Site The documentation site you're viewing uses additional modern features for the best experience. For optimal documentation browsing: | Browser | Recommended Version | |---------|--------------------:| | Chrome | 123+ | | Edge | 123+ | | Firefox | 120+ | | Safari | 17.5+ | | Opera | 109+ | ### Docs-Specific Features The documentation site uses these modern CSS and browser features: - **`color-mix()`** — Theme color blending and glass effects - **`light-dark()`** — Native CSS theme switching - **`inert` attribute** — Modal focus management - **CSS Nesting** — Organized stylesheets - **Backdrop Filter** — Glass morphism effects - **Service Workers** — Offline support (PWA) > [!TIP] > If you experience visual issues or missing features while browsing the docs, try updating to the latest version of your browser. ## Known Limitations ### CSS Anchor Positioning The `Popover` component uses CSS Anchor Positioning for automatic placement relative to trigger elements. Support landed in all major engines between mid-2024 and early 2026, so only recent versions have it: - **Chrome 125+**: Full support - **Edge 125+**: Full support - **Firefox 147+**: Full support - **Safari 26+**: Full support When anchor positioning is unavailable, the CSS properties are simply ignored. Pass the shipped [FloatingUIPopoverAdapter](/composables/system/use-popover#adapters) (`@vuetify/v0/popover/adapters/floating-ui`) if you need to support older browser versions. ### Popover API The native Popover API provides built-in light dismiss behavior and top-layer rendering. Support is growing but not universal: - **Chrome 114+**: Full support - **Edge 114+**: Full support - **Firefox 125+**: Full support - **Safari 17+**: Full support - **Opera 100+**: Full support The v0 Popover component uses optional chaining (`element.showPopover?.()`) to gracefully handle missing API support. ### Native Dialog The `` element with `showModal()` is well-supported in modern browsers but older Safari versions lack support: - **Safari 15.4+**: Full support - **Safari < 15.4**: No support, no fallback Consider a dialog polyfill if you need to support older Safari versions. ## Polyfills Vuetify0 does not include polyfills to keep bundle sizes small. If you need to support older browsers, consider adding polyfills for: - **ResizeObserver**: [resize-observer-polyfill](https://www.npmjs.com/package/resize-observer-polyfill) · [caniuse](https://caniuse.com/resizeobserver) - **IntersectionObserver**: [intersection-observer](https://www.npmjs.com/package/intersection-observer) · [caniuse](https://caniuse.com/intersectionobserver) - **Dialog**: [dialog-polyfill](https://www.npmjs.com/package/dialog-polyfill) · [caniuse](https://caniuse.com/dialog) - **Temporal**: [@js-temporal/polyfill](https://www.npmjs.com/package/@js-temporal/polyfill) · [caniuse](https://caniuse.com/temporal) — optional peer used by `useDate`'s `V0DateAdapter` when native Temporal is unavailable ## Testing Your Browser You can check your browser's feature support using these resources: - [Can I Use](https://caniuse.com) — Browser compatibility tables - [MDN Web Docs](https://developer.mozilla.org) — Feature documentation and support info - [Baseline](https://web.dev/baseline) — Web platform feature availability --- # Code of Conduct URL: https://0.vuetifyjs.com/introduction/code-of-conduct # Code of Conduct We are committed to providing a welcoming and inspiring community for all. This Code of Conduct outlines our expectations for participant behavior in every space we operate — and the consequences for unacceptable behavior. ## Our Pledge We pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards **Positive behaviors include:** - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes - Focusing on what is best for the overall community **Unacceptable behaviors include:** - Sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting/derogatory comments, and personal attacks - Public or private harassment - Publishing others' private information without permission - Other conduct inappropriate in a professional setting ## Where This Applies This Code of Conduct applies within all community spaces operated by the Vuetify team: - The [Vuetify Discord server](https://discord.gg/vuetify) — support channels, showcase, and general chat - The [vuetifyjs GitHub organization](https://github.com/vuetifyjs) — issues, discussions, pull requests, and code review - Official Vuetify social media accounts - Official events, online or offline It also applies when you are officially representing the community in public spaces — for example, posting via an official social media account or acting as an appointed representative at an event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior in any of these spaces may be reported by contacting the project team at [hello@vuetifyjs.com](mailto:hello@vuetifyjs.com). On Discord, you can also reach out directly to any member of the moderation team. The project team will review and investigate all complaints, responding in a way appropriate to the circumstances. The team is obligated to maintain confidentiality regarding the reporter. Enforcement actions apply across all community spaces — behavior on Discord can result in consequences on GitHub, and vice versa. Consequences follow the Contributor Covenant enforcement ladder: | Action | When | Consequence | |--------|------|-------------| | **Correction** | Unprofessional or unwelcome behavior | Private written warning | | **Warning** | A violation through a single incident or series of actions | Warning with a no-interaction period | | **Temporary Ban** | Serious or sustained violation | Time-limited ban from all community spaces | | **Permanent Ban** | Pattern of violations, harassment, or aggression | Permanent ban from all community spaces | ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. View the full [CODE_OF_CONDUCT.md](https://github.com/vuetifyjs/0/blob/master/CODE_OF_CONDUCT.md) on GitHub. > [!DISCORD] --- # Contributing to Vuetify0 URL: https://0.vuetifyjs.com/introduction/contributing # Contributing Thank you for your interest in contributing to Vuetify0! This guide will help you get started. ## Getting Started Before contributing, please: 1. Read the [Getting Started](/introduction/getting-started) guide to understand the project 2. Review existing [issues](https://github.com/vuetifyjs/0/issues) 3. Join our [Discord community](https://discord.gg/vuetify) for questions ## Reporting Issues ### Bug Reports When reporting bugs, please include: - A clear, descriptive title - Steps to reproduce the issue - Expected vs actual behavior - Browser and OS information - A minimal reproduction (preferably a [Playground](/playground) link or a repo) ### Feature Requests For new features: - Check if it's already been requested in [issues](https://github.com/vuetifyjs/0/issues) - Explain the use case and why it would benefit others - Consider if it fits the [headless/composable philosophy](https://github.com/vuetifyjs/0/blob/master/packages/0/PHILOSOPHY.md) of Vuetify0 ## Local Development ### Prerequisites - Node 26+ (matches .nvmrc) - pnpm 11.16.0 - Git ### Setup ```bash # Clone the repository git clone https://github.com/vuetifyjs/0.git cd 0 # Install dependencies pnpm install # Start the dev environment pnpm dev # Start the docs site pnpm dev:docs ``` ### Project Structure ```text ├── packages/ │ ├── 0/ # @vuetify/v0 - main package │ │ └── src/ │ │ ├── components/ # Vue components │ │ ├── composables/ # Composable functions │ │ ├── utilities/ # Helper functions │ │ └── types/ # TypeScript types │ ├── play/ # @vuetify/play - playground hash protocol │ ├── emerald/ # @paper/emerald - design system │ ├── bulma/ # @paper/bulma - Bulma compat design system │ ├── genesis/ # @paper/genesis - design system │ └── paper/ # @vuetify/paper - styling primitives (dormant, not published) ├── apps/ │ ├── docs/ # Documentation site │ └── playground/ # Browser-based code editor └── dev/ # Development environment ``` ### Useful Commands ```bash # Development pnpm dev # Start dev environment pnpm dev:docs # Start docs site # Testing pnpm test # Run tests in watch mode pnpm test:run # Run tests once # Type checking pnpm typecheck # Check all packages # Linting pnpm lint # Lint codebase pnpm lint:fix # Auto-fix lint issues # Building pnpm build # Build packages ``` ## Pull Requests ### Branch Model `@vuetify/v0` uses three long-lived branches. Open your PR against the base that matches the **semver impact** of your change: | Base | Use it for | Release | |------|-----------|---------| | `master` | Bug fixes, docs, chores, refactors, tests | Patch (or no version bump) | | `dev` | New features that add public API (a component, composable, prop, or option) | Minor | | `next` | Breaking changes (anything with a `BREAKING CHANGE:` footer) | Major | Only `master` publishes to npm. Work on `dev` and `next` merges into `master` at the next minor or major release, and that merge is what ships it. That join is a **merge commit**, not squash or rebase — squashing rewrites the `dev` SHAs and the next cut treats the same history as new. If you're unsure which base fits, open against `master` — a maintainer will retarget it. A `feat` PR that adds public API **must** target `dev`. If it lands on `master` with a `minor` changeset, the next Version Packages merge *is* that minor, even if larger features are still queued on `dev`. The version number is not reserved for a milestone. > [!TIP] > A `feat` that only touches the docs site, playground, or other tooling (not `packages/*` source) ships no package version, so it targets `master` — prefer a `docs`/`chore` prefix for those. ### Before Submitting 1. Create a new branch from the right base for your change (see [Branch Model](#branch-model)): `master` for fixes, `dev` for features, `next` for breaking changes 2. Make your changes 3. Write tests for new functionality 4. Run `pnpm lint:fix` to fix formatting 5. Run `pnpm typecheck` to check types 6. Run `pnpm test:run` to verify tests pass 7. Run `pnpm repo:check` to catch unused files and dependency issues 8. Run `pnpm changeset` if you changed `packages/*` source (see [Changesets](#changesets)) The pre-push hook runs lint, typecheck, tests, and repo checks automatically — the steps above keep it green. ### Changesets Releases are managed with [Changesets](https://github.com/changesets/changesets). If your PR changes published source under `packages/*`, add a changeset so the change lands in the next release's version bump and changelog: ```bash pnpm changeset ``` Pick the affected package(s), a bump type (`patch`/`minor`/`major`), and a short summary. The command generates a markdown file that you commit alongside your code: ```md --- "@vuetify/v0": patch --- fix(createSelection): reject disabled items in multiple-mode apply ``` The entire changeset body — everything after the frontmatter — is rendered verbatim into the changelog and the GitHub release notes. It is release-note copy, not a commit message. So the rule is not "how long" but "what belongs": - **First line** — the conventional-commit summary: `type(Scope): what changed (#PR)`, describing the change from the consumer's side, not the diff's. It stands alone as the whole changeset when it already answers both questions a consumer asks of release notes — *does this affect me, and must I act?* Many routine fixes need nothing more. A title that instead restates the diff is a commit message wearing changeset frontmatter — that is the failure to avoid, body or not. - **Body (optional)** — add one when the title can't carry the whole consumer-visible consequence: a behavior delta, a performance change worth quantifying (state the magnitude), a breaking change or migration step, or new public options / escape hatches. Length is earned by consumer impact — a genuinely rich behavior change may run to a paragraph; a routine, self-evident fix stays one line. - **Never the mechanism.** How you implemented it — internal composables touched, private fields, refactors mirrored from a sibling — belongs in the PR description and the commit body, not the changelog. A consumer reads release notes to decide whether to upgrade and whether they must act, nothing more. `@vuetify/v0` versions and publishes independently; `@vuetify/paper` is `private` and unpublished, so it takes no changeset. The `@paper/*` design systems version separately. Docs-only, chore, refactor, or CI PRs don't need one. A bot comments on every PR to remind you. The changeset is how your change reaches a release. On every push to `master`, automation gathers all pending `.changeset/*.md` files into a "Version Packages" PR that applies the version bumps and writes the changelog entries. When a maintainer merges that PR, the packages are built, published to npm, and the GitHub releases are created. A `packages/*` change merged without a changeset still ships in the code — but with no version bump and no changelog entry. Never edit `package.json` versions by hand — release automation owns every bump. If you're unsure which bump type fits, pick your best guess; maintainers adjust it during release review. ### PR Guidelines - Keep PRs focused - one feature or fix per PR - Title must be Conventional Commits: `type(scope): subject` (scope optional). Allowed types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`. Squash merge uses the title as the commit — Semantic PR is required, and a non-matching title cannot merge - Write a clear description - Reference any related issues - Be responsive to feedback ### Branch Naming Use descriptive branch names; the prefix should match the base branch you target (see [Branch Model](#branch-model)): - `fix/issue-description` - Bug fixes → base `master` - `feat/feature-name` - New features → base `dev` - `docs/what-changed` - Documentation updates → base `master` - `refactor/what-changed` - Code refactoring → base `master` Breaking changes target `next` regardless of prefix. ## Commit Messages Follow the [Conventional Commits](https://www.conventionalcommits.org/) format: ```text type(scope): subject ``` ### Types - `feat` - New feature - `fix` - Bug fix - `docs` - Documentation changes - `refactor` - Code refactoring - `test` - Adding or updating tests - `chore` - Maintenance tasks `feat` and `fix` are reserved for changes to `packages/*` source — they drive changelogs and version bumps. Everything else (docs, apps, tooling, CI) uses `docs`, `chore`, `refactor`, or `test`. ### Examples ```bash feat(createSelection): add toggle method fix(ExpansionPanel): correct aria-expanded state docs(getting-started): update installation instructions refactor(createRegistry): simplify reindex logic test(useForm): add validation edge cases ``` ### Guidelines - Use imperative mood ("add" not "added") - Keep the subject under 60 characters - Don't end with a period - Reference issues when applicable: `fix(useForm): validation error (#123)` ## Code Style The sections below are a summary. The design contract behind them — axioms, return-shape conventions, reactivity rules — lives in [PHILOSOPHY.md](https://github.com/vuetifyjs/0/blob/master/packages/0/PHILOSOPHY.md), with detailed per-scope playbooks in [.claude/rules](https://github.com/vuetifyjs/0/tree/master/.claude/rules). ### General - Use TypeScript for all new code - Follow existing patterns in the codebase - Prefer `function` declarations over `const` arrow functions - Use single-word variable names when clear - Add JSDoc comments for public APIs ### Composables - Place in `packages/0/src/composables/` - Each composable in its own directory with `index.ts` - Include `@module` JSDoc block at the top - Colocate tests as `index.test.ts` - Export both standalone and context-creation functions ### Components - Follow the compound component pattern (Root/Item) - Extend `AtomProps` for polymorphic components - Use `useProxyModel` for v-model binding - Include proper ARIA attributes ## Testing - Write tests for new composables and components - Focus on behavior, not implementation details - Test edge cases and error conditions - Use `describe` blocks to organize related tests ```ts describe('createSelection', () => { it('should select an item', () => { // ... }) it('should respect mandatory option', () => { // ... }) }) ``` ## Skillz Feedback [Vuetify0 Skillz](/skillz) is our interactive tutorial system currently in beta. We're actively developing new content and improving the learning experience. ### How to Give Feedback - **Content issues**: Typos, unclear instructions, or incorrect examples - **Technical problems**: Bugs in the interactive editor or validation - **Suggestions**: New skill ideas or improvements to existing ones When reporting issues, please include: - The skill name and step number - What you expected vs what happened - Browser and OS information > [!DISCORD] Thank you for contributing! --- # Vuetify0 FAQ URL: https://0.vuetifyjs.com/introduction/frequently-asked # Frequently Asked Common questions and answers about Vuetify0. Have a question that isn't answered here? Try [Ask AI](/guide/essentials/using-the-docs#ask-ai) for instant answers, join our [Discord community](https://discord.gg/vuetify), or open an [issue on GitHub](https://github.com/vuetifyjs/0/issues). ::: faq ??? What is Vuetify0? Vuetify0 is a collection of headless UI primitives and composables for Vue 3. It provides unstyled, logic-focused building blocks that handle accessibility, keyboard navigation, and state management while giving you complete control over styling. The library includes composables covering everything from selection patterns to form validation, plus ready-to-use headless components like Dialog, Tabs, and ExpansionPanel. Get started instantly with `pnpm create vuetify0`. ??? How is Vuetify0 different from Vuetify? Vuetify is a full-featured Material Design component framework with opinionated styling, theming, and a complete design system. Vuetify0 is headless - components have no built-in styles. You bring your own CSS using Tailwind, UnoCSS, or plain CSS. Think of Vuetify0 as the foundation layer that Vuetify and other component libraries could be built on top of. ??? Do I need Vuetify to use Vuetify0? No. Vuetify0 is a standalone package with no dependency on Vuetify. You can use it in any Vue 3 project. ??? What styling approach should I use? Vuetify0 is style-agnostic - use whatever CSS approach you prefer. We recommend [UnoCSS](https://unocss.dev) for its speed and flexibility, but Tailwind CSS v4, plain CSS, CSS Modules, and CSS-in-JS solutions all work well. The theme plugin exposes CSS custom properties (`--v0-primary`, `--v0-surface`, etc.) that you can map to your framework's color system. All documentation examples use Tailwind-compatible utility classes. ??? Why use composables instead of components? Components are great for common UI patterns, but composables give you more flexibility. You can build custom components, combine behaviors, control the entire DOM structure, and import only the logic you need for smaller bundles. ??? Is Vuetify0 accessible? Yes. Components and composables implement WAI-ARIA patterns including proper ARIA attributes, roles, and states. Keyboard navigation is built-in - arrow keys for selection, Enter/Space for activation, Escape for dismissal. Focus management handles trapping focus in modals, restoring focus on close, and roving tabindex for composite widgets. The headless approach means you control the visual styling, but the accessibility logic is built-in. ??? Does Vuetify0 support SSR? Yes. All composables and components are SSR-safe and work with Nuxt, Vite SSR, and other server-rendering solutions. Use the `IN_BROWSER` constant to guard browser-only code, and the `useHydration` composable for hydration-aware rendering that prevents mismatches. Observer composables (`useResizeObserver`, `useIntersectionObserver`, etc.) automatically skip initialization on the server. ??? What browsers are supported? Vuetify0 targets modern evergreen browsers: Chrome 52+, Firefox 52+, Safari 10.1+, and Edge 79+. No IE11 support. Some cutting-edge features like CSS Anchor Positioning (Chrome/Edge 125+) and the native Popover API (Chrome 114+, Firefox 125+, Safari 17+) have more limited support but degrade gracefully. See [Browser Support](/introduction/browser-support) for detailed compatibility tables. ??? Can I use Vuetify0 with Nuxt? Yes. Create a Nuxt plugin that registers Vuetify0 plugins, add `@vuetify/v0` to `build.transpile` in your Nuxt config, and you're ready to go. Components and composables can be imported directly or configured for auto-imports. See [Getting Started](/introduction/getting-started#nuxt) for setup instructions and the [Nuxt Guide](/guide/integration/nuxt) for advanced configuration. ??? What version of Vue is required? Vue 3.5.0 or higher. Vuetify0 uses modern Vue features like `useId()` and improved reactivity that require Vue 3.5+. Node 22+ is recommended for application development. Contributing to Vuetify0 requires Node 26+. ??? Is Vuetify0 tree-shakeable? Yes. Import only what you need and unused code is eliminated during bundling. Components and composables are individually exportable (`@vuetify/v0/components`, `@vuetify/v0/composables`), and the library has no side effects that prevent tree-shaking. ??? Does Vuetify0 support TypeScript? Yes. The library is written in TypeScript with full type definitions. Generic components preserve type inference, composables return properly typed refs and functions, and the plugin system is fully typed. No `@types` packages needed. ??? What are plugins in Vuetify0? Plugins provide app-wide functionality that composables can access via Vue's provide/inject. Core plugins include `createThemePlugin` (theming with CSS variables), `createLocalePlugin` (i18n), `createStoragePlugin` (persistent state), and `createLoggerPlugin` (debug logging). Additional plugins cover breakpoints, hydration, and permissions. Each plugin is optional - install only what you need. ??? How do I get started quickly? The fastest way is with the CLI: `pnpm create vuetify0`. This scaffolds a complete project with UnoCSS, theming, and example components pre-configured. For existing projects, install `@vuetify/v0` and start importing components and composables directly. See [Getting Started](/introduction/getting-started) for full instructions. ??? How do I contribute? See the [Contributing](/introduction/contributing) page for guidelines. The project is open source at [github.com/vuetifyjs/0](https://github.com/vuetifyjs/0). ??? Where can I get help? [GitHub Issues](https://github.com/vuetifyjs/0/issues) for bug reports and feature requests, or the [Vuetify Discord](https://discord.gg/vuetify) community for real-time chat and questions. ::: ::: sponsor ::: --- # Getting Started URL: https://0.vuetifyjs.com/introduction/getting-started # Get started with Vuetify0 Vuetify0 provides headless UI primitives and composables for Vue 3. Components are unstyled and logic-focused, giving you complete control over styling while handling accessibility, keyboard navigation, and state management. ## Quick Start The fastest way to start a new Vuetify0 project is with the CLI. ### Vuetify Create Standalone scaffolding tool for Vuetify0 projects using [Vuetify Create](https://github.com/vuetifyjs/cli/tree/master/packages/create0). ::: code-group no-filename ```bash pnpm pnpm create vuetify0 ``` ```bash npm npm create vuetify0 ``` ```bash yarn yarn create vuetify0 ``` ```bash bun bun create vuetify0 ``` ::: ### Vuetify CLI Full-featured CLI with additional presets and options using [Vuetify CLI](/guide/tooling/vuetify-cli). ::: code-group no-filename ```bash pnpm pnpm dlx @vuetify/cli init --type vuetify0 ``` ```bash npm npx @vuetify/cli init --type vuetify0 ``` ```bash yarn yarn dlx @vuetify/cli init --type vuetify0 ``` ```bash bun bunx @vuetify/cli init --type vuetify0 ``` ::: Both options scaffold a complete project with UnoCSS, theming, and example components pre-configured — the same shape as [DevKey](/guide/integration/devkey), the reference Vuetify0 starter project. > [!TIP] > Use the Skill Filter to narrow down navigation to match your experience level: ## Manual Setup To add Vuetify0 to an existing project, follow the steps below. ### Create Vue Project ::: code-group no-filename ```bash pnpm pnpm create vue@latest ``` ```bash npm npm create vue@latest ``` ```bash yarn yarn create vue ``` ```bash bun bun create vue@latest ``` ::: ### Installation Install `@vuetify/v0` with your preferred package manager: ::: code-group no-filename ```bash pnpm pnpm add @vuetify/v0 ``` ```bash npm npm install @vuetify/v0 ``` ```bash yarn yarn add @vuetify/v0 ``` ```bash bun bun add @vuetify/v0 ``` ::: Create a plugin file to configure v0: ```ts src/plugins/vuetify0.ts collapse import { createThemePlugin } from '@vuetify/v0' import type { App } from 'vue' export default function vuetify0 (app: App) { app.use( createThemePlugin({ default: 'light', themes: { light: { dark: false, colors: { primary: '#3b82f6', surface: '#ffffff', 'on-primary': '#ffffff', 'on-surface': '#212121', }, }, }, }), ) } ``` Register the plugin in your app entry: ```ts src/main.ts import { createApp } from 'vue' import App from './App.vue' import vuetify0 from './plugins/vuetify0' const app = createApp(App) vuetify0(app) app.mount('#app') ``` > [!NOTE] > For additional plugins, theming options, and advanced configuration, see the [Guide](/guide). ## Seed Working Examples Vuetify0 is headless: installing `@vuetify/v0` and importing a component gives you logic, accessibility, and state management — but no rendered UI. To start with a working, styled example you can edit and ship, use `vuetify add` to pull from the [docs registry](https://0.vuetifyjs.com/registry/index.json): ::: code-group no-filename ```bash pnpm # Interactive picker pnpm dlx @vuetify/cli add # Seed a Dialog example pnpm dlx @vuetify/cli add dialog # Seed a composable plugin pnpm dlx @vuetify/cli add use-theme ``` ```bash npm npx @vuetify/cli add npx @vuetify/cli add dialog npx @vuetify/cli add use-theme ``` ```bash yarn yarn dlx @vuetify/cli add yarn dlx @vuetify/cli add dialog yarn dlx @vuetify/cli add use-theme ``` ```bash bun bunx @vuetify/cli add bunx @vuetify/cli add dialog bunx @vuetify/cli add use-theme ``` ::: The CLI writes example files into your project, installs any missing dependencies, and records the install in `vuetify.json` so you can diff or refresh later. > [!TIP] > See the [Vuetify CLI guide](/guide/tooling/vuetify-cli) for the full command list, registry options, and `vuetify.json` tracking. ## Requirements - Vue 3.5.0 or higher - Node 22+ recommended for app tooling. Developing Vuetify0 itself requires Node 26+ (see [Contributing](/introduction/contributing)). ## Usage Import and use components directly - no plugin installation required: ```vue QuickStart.vue playground no-filename collapse ``` Components are completely unstyled. Add your own classes using Tailwind, UnoCSS, or plain CSS. ## Styling Vuetify0 is style-agnostic. Choose your preferred CSS framework and map theme colors to v0's CSS variables. ### UnoCSS [UnoCSS](https://unocss.dev) is our recommended choice for its speed and flexibility. #### 1. Install ::: code-group no-filename ```bash pnpm pnpm add -D unocss @unocss/preset-wind ``` ```bash npm npm install -D unocss @unocss/preset-wind ``` ```bash yarn yarn add -D unocss @unocss/preset-wind ``` ```bash bun bun add -D unocss @unocss/preset-wind ``` ::: #### 2. Configure ```ts uno.config.ts import { defineConfig, presetWind } from 'unocss' export default defineConfig({ presets: [presetWind()], theme: { colors: { primary: 'var(--v0-primary)', surface: 'var(--v0-surface)', 'on-primary': 'var(--v0-on-primary)', 'on-surface': 'var(--v0-on-surface)', // Add more theme colors as needed }, }, }) ``` #### 3. Add Vite Plugin ```ts vite.config.ts import UnoCSS from 'unocss/vite' export default defineConfig({ plugins: [ vue(), UnoCSS(), ], }) ``` #### 4. Import Styles ```ts src/main.ts import 'virtual:uno.css' ``` Now use utility classes in your components: ```vue ``` ### Tailwind CSS v4 [Tailwind v4](https://tailwindcss.com) uses CSS-first configuration with native cascade layers. #### 1. Install ::: code-group no-filename ```bash pnpm pnpm add -D tailwindcss @tailwindcss/vite ``` ```bash npm npm install -D tailwindcss @tailwindcss/vite ``` ```bash yarn yarn add -D tailwindcss @tailwindcss/vite ``` ```bash bun bun add -D tailwindcss @tailwindcss/vite ``` ::: #### 2. Add Vite Plugin ```ts vite.config.ts import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [ vue(), tailwindcss(), ], }) ``` #### 3. Create Stylesheet ```css src/styles/main.css @import "tailwindcss"; @theme { --color-primary: var(--v0-primary); --color-surface: var(--v0-surface); --color-on-primary: var(--v0-on-primary); --color-on-surface: var(--v0-on-surface); } ``` #### 4. Import Styles ```ts src/main.ts import './styles/main.css' ``` Now use utility classes in your components: ```vue ``` ### CSS Modules Vue's built-in [CSS Modules](https://vuejs.org/api/sfc-css-features#css-modules) require zero configuration. ```vue Button.vue no-filename ``` Type-safe access via `useCssModule()`: ```vue ``` > [!TIP] > For dark mode, custom themes, and design tokens, see the [Theming Guide](/guide/features/theming). ## Nuxt Vuetify0 works with Nuxt via a standard plugin. ### 1. Create Plugin ```ts plugins/vuetify0.ts collapse import { createThemePlugin } from '@vuetify/v0' export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use( createThemePlugin({ default: 'light', themes: { light: { dark: false, colors: { primary: '#3b82f6', surface: '#ffffff', 'on-primary': '#ffffff', 'on-surface': '#212121', }, }, dark: { dark: true, colors: { primary: '#60a5fa', surface: '#1e1e1e', 'on-primary': '#1a1a1a', 'on-surface': '#e0e0e0', }, }, }, }), ) }) ``` ### 2. Configure Nuxt ```ts nuxt.config.ts export default defineNuxtConfig({ build: { transpile: ['@vuetify/v0'], }, }) ``` > [!TIP] > This plugin uses the default theme adapter, which emits no CSS during SSR. The [Nuxt guide](/guide/integration/nuxt) replaces `plugins/vuetify0.ts` with one plugin that renders the theme on the server, stores it in a cookie, and sets up hydration and breakpoints. Use that file on its own. ## Exposed Exports The following export paths exist for the Vuetify0 framework: | Name | Description | | ---- | ----------- | | `@vuetify/v0` | Main entry point exposing all components, composables, and utilities. | | `@vuetify/v0/components` | Components only. | | `@vuetify/v0/composables` | Composables only. | | `@vuetify/v0/utilities` | Utilities only. | | `@vuetify/v0/constants` | Constants only (not included in main entry). | | `@vuetify/v0/locale/messages/en` | Optional canonical English aria-string map for component accessible names. Never imported by the runtime. | ```ts // Everything import { ExpansionPanel, createSelection } from '@vuetify/v0' // Components only import { ExpansionPanel, Single, Group } from '@vuetify/v0/components' // Composables only import { createSelection, useTheme, createForm } from '@vuetify/v0/composables' // Utilities only import { isObject, isString } from '@vuetify/v0/utilities' // Constants only import { IN_BROWSER } from '@vuetify/v0/constants' ``` ## Agents Click the portrait to copy John's identity into [Grok Bot](https://x.ai/bot). Setup prompts live on [Agents](/guide/tooling/agents). ## Next Steps Now that Vuetify0 is installed, choose your path: | Goal | Start Here | | - | - | | Understand the architecture | [Components](/guide/fundamentals/components) → [Composables](/guide/fundamentals/composables) → [Core](/guide/fundamentals/core) | | Build production UIs now | [Theming](/guide/features/theming) → [Accessibility](/guide/features/accessibility) | | Build a component library | [Building Frameworks](/guide/fundamentals/building-frameworks) | | Seed styled examples | [Vuetify CLI](/guide/tooling/vuetify-cli) (`vuetify add`) | | Set up an AI agent | [Agents](/guide/tooling/agents) → [AI Tools](/guide/tooling/ai-tools) | | Explore interactively | [Playground](/playground) | > [!TIP] > Use `Cmd+/` on any documentation page to ask AI questions about Vuetify0. > [!TOUR] using-search ## Support Vuetify0 Vuetify0 is built and maintained in the open. If it's useful to you or your team, sponsoring funds the work and keeps it moving — and need a hand shipping? The [Services](/services) page covers direct support plans and fixed-scope project builds. ::: sponsor ::: > [!DISCORD] --- # License URL: https://0.vuetifyjs.com/introduction/license # License Vuetify0 is released under the [MIT License](https://opensource.org/license/MIT), one of the most permissive and widely-used open source licenses. ## What This Means The MIT License allows you to: - **Use** — Use Vuetify0 in personal and commercial projects - **Modify** — Modify the source code to fit your needs - **Distribute** — Distribute original or modified versions - **Sublicense** — Include in proprietary software **The only requirement** is that you include the original copyright notice and license text in any copy of the software or substantial portion of it. ## Full License Text ```text LICENSE.md collapse The MIT License (MIT) Copyright (c) 2016-now Vuetify, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` View the [LICENSE.md](https://github.com/vuetifyjs/0/blob/master/packages/0/README.md) on GitHub. > [!DISCORD] --- # Security Disclosure URL: https://0.vuetifyjs.com/introduction/security # Security Disclosure The Vuetify team takes security seriously. We appreciate your efforts to responsibly disclose vulnerabilities and will make every effort to acknowledge your contributions. ## Reporting a Vulnerability To report a security issue, email [security@vuetifyjs.com](mailto:security@vuetifyjs.com?subject=SECURITY) and include the word **"SECURITY"** in the subject line. **Please include:** - Description of the vulnerability - Steps to reproduce - Potential impact - Any suggested fixes (optional) ## What to Expect 1. **Initial Response** — We will acknowledge receipt within 48 hours 2. **Investigation** — We will investigate and keep you informed of progress 3. **Resolution** — We will prepare and release fixes as quickly as possible 4. **Credit** — We will credit you in the release notes (unless you prefer anonymity) ## Disclosure Policy When we receive a security report, we will: - Confirm the problem and determine affected versions - Audit code to find any similar issues - Prepare fixes for all maintained releases - Release fixes to npm as quickly as possible Internally, security incidents are handled according to a formal Incident Response Plan that defines severity classification, response timelines, and escalation procedures. ## Threat Model `@vuetify/v0` is a **client-side UI composables library**. It processes no secrets, manages no authentication, and communicates with no external services. This threat model uses the [STRIDE framework](https://en.wikipedia.org/wiki/STRIDE_(security)) to identify and mitigate threats across the project lifecycle. ### Assets | Asset | Impact if Compromised | |-------|----------------------| | npm package (`@vuetify/v0`) | Supply chain attack on all downstream consumers | | GitHub repository | Tampered source leads to tampered package | | CI/CD secrets (npm token, deploy tokens) | Unauthorized publish or deployment | | Documentation site | Defacement, phishing, malicious examples | | Consumer app security | Apps trust v0 to not introduce XSS or injection | ### Trust Boundaries ```mermaid "Trust Boundaries" flowchart LR Contributor -->|PR| GitHub GitHub -->|tag| CI CI -->|publish| npm npm -->|install| ConsumerApp[Consumer app] ConsumerApp -->|render| EndUser[End user] GitHub -->|push| CIDocs[CI] CIDocs -->|webhook| Deploy Deploy -->|serve| DocsSite[Docs site] ``` ### In Scope | Category | Threats | |----------|---------| | Supply chain | Compromised npm publish credentials, malicious dependency updates, build pipeline injection, typosquatting, mutable CI action references | | Consumer-facing | CSS injection via theme values, XSS through unsanitized slot/prop content, storage data exposure, CSP implications | | CI/CD | Fork PR code execution, preview package abuse, deploy webhook replay, secret leakage | | Contributor | Review bypass, social engineering via issues/PRs | | Infrastructure | Docs site compromise via deploy pipeline, availability | ### Out of Scope | Threat | Reason | |--------|--------| | XSS from user content | Consumer's responsibility to sanitize before passing to v0 | | Authentication / authorization | Vuetify0 has no auth layer | | Server-side code execution | Vuetify0 has no dynamic code execution; SSR rendering is stateless and read-only | | Issues in consumer applications | v0 controls only what it exports | ### Supply Chain Hardening These measures protect the integrity of `@vuetify/v0` from source to consumer: - **Lockfile committed** — `pnpm-lock.yaml` is version-controlled, ensuring reproducible installs - **Dependency cooldown** — pnpm's `minimumReleaseAge` (set in `pnpm-workspace.yaml`) enforces a release-age waiting period before a newly published dependency version can be installed, giving time for a compromised release to be detected and pulled. First-party Vuetify packages and the dev/build toolchain (linters, the bundler, CI preview tooling) are exempt via `minimumReleaseAgeExclude`, as they are either published by us or never ship in a consumer's runtime bundle - **Pinned CI actions** — Vuetify-owned GitHub Actions are pinned to commit SHAs, not mutable branch refs, preventing silent changes via force-push - **Scoped package** — published under the `@vuetify` npm org, reducing typosquatting risk - **No lifecycle scripts** — no `postinstall` or `preinstall` scripts that could execute arbitrary code at install time > [!IMPORTANT] > The current dependency release-age cooldown is **14 days** — a newly published version of any non-exempt dependency must be at least 14 days old before it can be installed. ### Security Properties These properties are verified in the codebase: - **No network requests** — Vuetify0 makes no HTTP calls; the Knock notification adapter is opt-in only - **No dynamic code evaluation** — no runtime code generation or arbitrary script execution - **Prototype pollution protection** — `mergeDeep` blocks `__proto__`, `constructor`, and `prototype` keys - **CSS injection protection** — Theme adapters validate theme names and color keys against a safe identifier pattern (`[a-zA-Z0-9_-]`), and reject color values containing dangerous CSS patterns. The browser adapter uses `adoptedStyleSheets` (no DOM parsing); SSR adapters use `innerHTML` on ` ``` ## Theme API ```ts const theme = useTheme() // Read current theme theme.selectedId.value // 'light' | 'dark' theme.selectedItem.value // Current theme ticket theme.isDark.value // boolean // Switch themes theme.cycle() // Cycle through themes theme.select('dark') // Select specific theme // Access theme values theme.colors.value // Map of all theme colors theme.colors.value[theme.selectedId.value] // Current theme's colors ``` ## CSS Variables v0 injects variables with the `--v0-` prefix: | Variable | Purpose | | - | - | | `--v0-primary` | Primary brand color | | `--v0-secondary` | Secondary color | | `--v0-background` | Page background | | `--v0-surface` | Card/component background | | `--v0-error` | Error state color | | `--v0-on-primary` | Text on primary background | | `--v0-on-surface` | Text on surface background | ### Using Variables ```css .btn-primary { background: var(--v0-primary); color: var(--v0-on-primary); } .card { background: var(--v0-surface); border: 1px solid var(--v0-divider); } ``` ### Tailwind CSS v4 Integration Map v0 variables to Tailwind theme colors in your CSS: ```css app.css @import "tailwindcss"; @theme { --color-primary: var(--v0-primary); --color-secondary: var(--v0-secondary); --color-background: var(--v0-background); --color-surface: var(--v0-surface); --color-error: var(--v0-error); --color-on-primary: var(--v0-on-primary); --color-on-surface: var(--v0-on-surface); } ``` Now use standard utilities: ```html
Card
``` Theme changes update automatically—the utilities reference CSS variables, not static values. ### UnoCSS Integration Map v0 variables to UnoCSS theme colors: ```ts uno.config.ts collapse import { defineConfig, presetUno } from 'unocss' export default defineConfig({ presets: [presetUno()], theme: { colors: { primary: 'var(--v0-primary)', secondary: 'var(--v0-secondary)', background: 'var(--v0-background)', surface: 'var(--v0-surface)', error: 'var(--v0-error)', 'on-primary': 'var(--v0-on-primary)', 'on-surface': 'var(--v0-on-surface)', }, }, }) ``` ## Dark Mode ### Manual Toggle ```vue playground ``` ### localStorage Persistence Use `useStorage` to persist theme selection across page loads. The key is installing the storage plugin first, then reading the stored preference during theme plugin setup: > [!TIP] Built-in persistence > `createThemePlugin` ships `persist`/`restore` wiring out of the box — pass `persist: true` and skip the manual `useStorage` + `watch` code below: > > ```ts no-filename > app.use(createStoragePlugin()) > app.use( > createThemePlugin({ > default: 'light', > themes: { light: { dark: false, colors: { /* ... */ } }, dark: { dark: true, colors: { /* ... */ } } }, > persist: true, > }), > ) > ``` > > `createStoragePlugin()` still has to be installed first — the theme plugin reads/writes through it. Reach for the manual approach below when persistence needs custom logic the built-in path doesn't cover — a cookie for SSR or a backend call. To follow the OS until the user picks a theme, pass `system` (see [Follow the operating system](/composables/plugins/use-theme#follow-the-operating-system)) instead of resolving `matchMedia` into `default`. ```ts main.ts collapse import { createApp } from 'vue' import { createStoragePlugin, createThemePlugin } from '@vuetify/v0' import App from './App.vue' const app = createApp(App) app.use(createStoragePlugin()) app.use( createThemePlugin({ default: 'light', system: { light: 'light', dark: 'dark' }, persist: true, themes: { light: { dark: false, colors: { /* ... */ } }, dark: { dark: true, colors: { /* ... */ } }, }, }), ) ``` `system` requires both ids to already be registered. A system-applied theme is not persisted; `select` is. `theme.reset()` follows the OS again. > [!TIP] > For SSR apps, use cookies instead of localStorage—see [Nuxt Theme Persistence](/guide/integration/nuxt#theme-persistence). ## Design Tokens with createTokens For fine-grained token management beyond colors: ```ts import { createTokens } from '@vuetify/v0' const tokens = createTokens() // Register tokens tokens.register({ id: 'spacing-sm', value: '0.5rem' }) tokens.register({ id: 'spacing-md', value: '1rem' }) tokens.register({ id: 'radius-sm', value: '4px' }) // Aliases use {path} syntax in the value tokens.register({ id: 'gap', value: '{spacing-md}' }) // Lookup resolves aliases tokens.resolve('gap') // '1rem' ``` ### Token Categories ```ts // Typography tokens.register({ id: 'font-body', value: 'Inter, sans-serif' }) tokens.register({ id: 'font-size-sm', value: '0.875rem' }) // Spacing tokens.register({ id: 'space-1', value: '0.25rem' }) tokens.register({ id: 'space-2', value: '0.5rem' }) // Shadows tokens.register({ id: 'shadow-sm', value: '0 1px 2px rgba(0,0,0,0.1)' }) ``` ## Scoped Themes Override theme for a subtree using the trinity pattern: ```ts import { createThemeContext, createTheme } from '@vuetify/v0' // Create a custom theme context with options const [useTheme, provideTheme] = createThemeContext({ default: 'custom', themes: { custom: { dark: false, colors: { primary: '#E91E63', // Pink instead of blue secondary: '#9C27B0', }, }, }, }) // Provide to descendants provideTheme() ``` ## Best Practices ### 1. Semantic Color Names ```ts // Good - semantic themes: { light: { colors: { primary: '#1976D2', error: '#B00020' } }, } // Avoid - literal themes: { light: { colors: { blue: '#1976D2', red: '#B00020' } }, } ``` ### 2. On-* Contrast Colors Always define contrast colors for accessibility: ```ts themes: { light: { colors: { primary: '#1976D2', 'on-primary': '#FFFFFF', // White text on primary surface: '#FFFFFF', 'on-surface': '#212121', // Dark text on surface }, }, } ``` ### 3. CSS Fallbacks ```css .card { /* Fallback for missing variable */ background: var(--v0-surface, #ffffff); } ``` --- # Types URL: https://0.vuetifyjs.com/guide/features/types # Types Shared TypeScript types used across the library and available for your own code. ```ts import type { ID } from '@vuetify/v0' import type { Activation, DeepPartial, Extensible, MaybeArray } from '@vuetify/v0/types' ``` ## ID Identifier type used throughout the registry system. All tickets, items, and registrable entities use this for their `id` property. ```ts type ID = string | number ``` ```ts const id: ID = 'item-1' // string const id: ID = 42 // number ``` ## Activation Keyboard activation mode for navigable components. ```ts type Activation = 'automatic' | 'manual' ``` - **automatic** — selection follows focus. Arrow keys select immediately. This is the WAI-ARIA standard for radio groups. - **manual** — arrow keys move focus only. Enter or Space is required to select. Use for toolbars or when deliberate selection is preferred. ```vue ``` ## DeepPartial Recursively makes all properties of `T` optional. Used by `mergeDeep` to type partial source objects. ```ts type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; } : T ``` ```ts type User = { name: string; address: { city: string } } type PartialUser = DeepPartial // { name?: string; address?: { city?: string } } ``` ## Extensible Preserves string literal autocomplete while allowing arbitrary strings. TypeScript normally collapses `'a' | 'b' | string` into just `string`, losing IDE autocomplete. This type uses the `string & {}` trick to prevent that collapse. ```ts type Extensible = T | (string & {}) ``` Use for extensible APIs where you want autocomplete for known values but still accept custom strings — event names, theme tokens, CSS classes, etc. ```ts type Color = 'red' | 'blue' | 'green' // WITHOUT Extensible — autocomplete lost type BadColor = Color | string // collapses to just `string` // WITH Extensible — autocomplete preserved type GoodColor = Extensible function setColor(c: GoodColor) {} setColor('red') // autocomplete suggests 'red' | 'blue' | 'green' setColor('purple') // also OK — custom value accepted ``` ## MaybeArray Union type that accepts either a single value or an array. Use for APIs that accept both forms. ```ts type MaybeArray = T | T[] ``` ```ts function process(input: MaybeArray) { ... } process('single') // OK process(['a', 'b', 'c']) // OK ``` ## DOMElement Valid element types for Vue's `h()` render function. Includes HTML tag names, component definitions, and functional components. Used by the `Atom` component for polymorphic rendering. ```ts type DOMElement = Parameters[0] ``` ## UnknownObject Object with string keys and unknown values for generic record handling. Requires type narrowing at the use site, which catches more bugs at compile time than `Record`. ```ts type UnknownObject = Record ``` ## V0ErrorDetails Discriminated union of structured details attached to every v0-thrown error. Each arm pairs a stable `code` with the domain context for that code — consumed by the `V0Error` constructor and narrowed by `isV0Error(err, code)` (both from `@vuetify/v0`, see [Utilities](/guide/features/utilities#errors)). ```ts type V0ErrorDetails = | { code: 'V0_CONTEXT_MISSING', key: string | symbol } | { code: 'V0_PLUGIN_MISSING', plugin: string } | { code: 'V0_PALETTE_INVALID_SEED', palette: 'material' | 'leonardo' | 'ant', seed: string } | { code: 'V0_PALETTE_UNKNOWN_VARIANT', palette: 'material', variant: string } | { code: 'V0_ADAPTER_INSTANCE_MISSING', adapter: string } | { code: 'V0_THEME_INVALID_PREFIX', prefix: string } ``` ## V0ErrorCode Union of every error code thrown by v0. Convenience alias for the discriminant field of `V0ErrorDetails`. ```ts type V0ErrorCode = V0ErrorDetails['code'] ``` ```ts import { isV0Error, V0Error } from '@vuetify/v0' try { throw new V0Error('Context not found.', { code: 'V0_CONTEXT_MISSING', key: 'v0:theme' }) } catch (err) { if (isV0Error(err, 'V0_CONTEXT_MISSING')) { console.log(err.key) // typed as string | symbol, not string | symbol | undefined } } ``` --- # Utilities Guide URL: https://0.vuetifyjs.com/guide/features/utilities # Utilities Standalone helpers for common UI patterns. These composables don't depend on context or plugins—use them anywhere. ## Overview | Utility | Purpose | | - | - | | [createFilter](/composables/data/create-filter) | Filter arrays with search queries | | [createPagination](/composables/data/create-pagination) | Page navigation state | | [createVirtual](/composables/data/create-virtual) | Virtual scrolling for large lists | | [createOverflow](/composables/semantic/create-overflow) | Compute visible item capacity | > [!TIP] > These utilities are standalone—they don't require plugins or context. Use them anywhere, including outside Vue components. ## createFilter Filter arrays based on search queries: ```ts import { createFilter } from '@vuetify/v0' const items = ref(['Apple', 'Banana', 'Cherry']) const query = ref('') const filter = createFilter() const { items: filtered } = filter.apply(query, items) query.value = 'an' filtered.value // ['Banana'] ``` ### With Object Keys ```ts const users = ref([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' }, ]) const filter = createFilter({ keys: ['name', 'email'], }) const query = ref('alice') const { items: filtered } = filter.apply(query, users) filtered.value // [{ name: 'Alice', ... }] ``` ### Filter Modes ```ts const filter = createFilter({ mode: 'intersection', // 'some' | 'every' | 'union' | 'intersection' keys: ['name', 'tags'], }) ``` ## createPagination Pagination state management: ```ts import { createPagination } from '@vuetify/v0' const pagination = createPagination({ size: 100, itemsPerPage: 10, }) pagination.page.value // 1 pagination.pages // 10 pagination.isFirst.value // true pagination.isLast.value // false pagination.next() // Go to page 2 pagination.prev() // Go to page 1 pagination.select(5) // Go to page 5 pagination.first() // Go to page 1 pagination.last() // Go to page 10 ``` ### With Reactive Size ```ts const items = ref([...]) const pagination = createPagination({ size: () => items.value.length, itemsPerPage: 20, }) ``` ### Page Items ```ts pagination.items.value // [{ type: 'page', value: 1 }, { type: 'page', value: 2 }, ...] ``` ## createVirtual Virtual scrolling for large datasets: ```ts import { createVirtual } from '@vuetify/v0' const items = ref(Array.from({ length: 10000 }, (_, i) => `Item ${i}`)) // createVirtual takes items as first arg, options as second const virtual = createVirtual(items, { itemHeight: 40, }) ``` ```vue playground VirtualList.vue ``` ### Variable Height ```ts const virtual = createVirtual(items, { height: 400, // Container height (or use element ref) }) ``` ## createOverflow Compute how many items fit in a container: ```ts import { createOverflow } from '@vuetify/v0' import { useTemplateRef } from 'vue' const containerRef = useTemplateRef('container') const overflow = createOverflow({ container: containerRef, itemWidth: 100, gap: 8, }) overflow.capacity.value // Number of items that fit overflow.isOverflowing.value // Boolean: items exceed capacity ``` ### Use Case: Responsive Chips ```vue playground ResponsiveChips.vue ``` ## Transformers Value transformation utilities: ### toArray Normalize any value to an array: ```ts import { toArray } from '@vuetify/v0' toArray('single') // ['single'] toArray(['array']) // ['array'] toArray(null) // [] toArray(undefined) // [] ``` ### toReactive Convert ref objects to reactive proxies: ```ts import { toReactive } from '@vuetify/v0' const configRef = ref({ debug: false }) // Unwraps the ref and returns a reactive object const config = toReactive(configRef) config.debug // Reactive access ``` ## Color Color manipulation utilities for hex and RGB formats. ### hexToRgb / rgbToHex Convert between hex strings and `RGB` objects: ```ts import { hexToRgb, rgbToHex } from '@vuetify/v0' hexToRgb('#1976d2') // { r: 25, g: 118, b: 210 } hexToRgb('#fff') // { r: 255, g: 255, b: 255 } rgbToHex({ r: 25, g: 118, b: 210 }) // '#1976d2' ``` The `RGB` interface is available for typing: ```ts import type { RGB } from '@vuetify/v0' const color: RGB = { r: 255, g: 0, b: 0 } ``` ### APCA Contrast [APCA](https://github.com/Myndex/SAPC-APCA) (Advanced Perceptual Contrast Algorithm) for accessible color pairings. Returns a signed contrast value — higher magnitude means more contrast: ```ts import { apca, foreground, hexToRgb } from '@vuetify/v0' // Raw APCA contrast between two colors const text = hexToRgb('#1a1a1a') const bg = hexToRgb('#ffffff') apca(text, bg) // ~-106 (high contrast, dark on light) // Pick black or white text for a background foreground('#1976d2') // '#ffffff' (white reads better on this blue) foreground('#ffd600') // '#000000' (black reads better on yellow) ``` > [!TIP] > `foreground` is ideal for computing on-color text in design tokens. It uses APCA to select the higher-contrast option. ## Errors Structured error class and type guard for errors thrown by v0 internals. ### V0Error / isV0Error ```ts import { isV0Error, V0Error } from '@vuetify/v0' try { useContext('v0:missing') } catch (err) { if (isV0Error(err, 'V0_CONTEXT_MISSING')) { console.log(err.code, err.key) // 'V0_CONTEXT_MISSING', 'v0:missing' } } ``` `V0Error` carries a stable `code` discriminant (see [V0ErrorCode](/guide/features/types#v0errorcode)) instead of a message-only `Error`, so callers can branch on failure kind without parsing strings. `isV0Error(err, code?)` narrows `unknown` to `V0Error`, and — when `code` is passed — further narrows to the matching error-detail shape, so per-code fields like `key` or `plugin` become required instead of optional. ## Best Practices ### Combine Utilities ```ts // Filter + Paginate const query = ref('') const filter = createFilter() const { items: filtered } = filter.apply(query, items) const pagination = createPagination({ size: () => filtered.value.length, itemsPerPage: 10, }) const displayedItems = computed(() => { const start = pagination.pageStart.value const end = pagination.pageStop.value return filtered.value.slice(start, end) }) ``` ### Virtual + Filter ```ts const query = ref('') const filter = createFilter() const { items: filtered } = filter.apply(query, items) const virtual = createVirtual(filtered, { itemHeight: 40, }) ``` ### Filter + Pagination + Virtual For large datasets that need all three utilities working together: ```ts collapse import { computed, shallowRef } from 'vue' import { createFilter, createPagination, createVirtual, Pagination } from '@vuetify/v0' // Source data const items = shallowRef(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}`, }))) // 1. Filter first const query = shallowRef('') const filter = createFilter({ keys: ['name'] }) const { items: filtered } = filter.apply(query, items) // 2. Paginate the filtered results — `page` is shared with Pagination.Root below const page = shallowRef(1) const pagination = createPagination({ page, size: () => filtered.value.length, itemsPerPage: 100, }) // 3. Get current page slice const pageItems = computed(() => { const start = pagination.pageStart.value const end = pagination.pageStop.value return filtered.value.slice(start, end) }) // 4. Virtual scroll the current page const virtual = createVirtual(pageItems, { itemHeight: 40 }) const { element, items: virtualItems, offset, size } = virtual ``` ```vue ``` ```mermaid "Data Pipeline" flowchart LR A[All Items] --> B[Filtered] B --> C[Paginated] C --> D[Virtualized] ``` Each layer is reactive—changing the search query refilters, which updates pagination, which updates the virtual list. ## Type Guards Type-narrowing functions that replace raw `typeof` / `===` checks. All are tree-shakeable. ```ts import { isString, isNullOrUndefined, isObject } from '@vuetify/v0' if (isString(value)) { // value is string } if (!isNullOrUndefined(x)) { // x is defined } ``` | Guard | Narrows to | | - | - | | `isFunction(x)` | `Function` | | `isString(x)` | `string` | | `isNumber(x)` | `number` (includes NaN) | | `isBoolean(x)` | `boolean` | | `isObject(x)` | `Record` (excludes null and arrays)[^isobject-any] | | `isArray(x)` | the array member of `x`[^isarray-preserves] | | `isElement(x)` | `Element` | | `isNull(x)` | `null` | | `isUndefined(x)` | `undefined` | | `isNullOrUndefined(x)` | `null \| undefined` | | `isPrimitive(x)` | `string \| number \| boolean` | | `isSymbol(x)` | `symbol` | | `isNaN(x)` | `number` (only the NaN value)[^isnan-vs-global] | | `isThenable(x)` | `{ then: Function }`[^isthenable-duck-typed] | [^isobject-any]: The value type is `any` rather than `unknown` so that narrowing preserves what you already knew. TypeScript never grants an interface an implicit index signature, so a predicate of `Record` would collapse an interface-typed value's known property types and would fail to remove object members in the `else` branch. `any` here widens only the *index* type — the union member itself survives narrowing intact. [^isarray-preserves]: Narrowing preserves the element type, so a `string[] | string` narrows to `string[]`, not `unknown[]`. Tuples keep their arity and `readonly` arrays stay `readonly`. When nothing more specific is known (e.g. `x: unknown`), it falls back to `unknown[]`. [^isnan-vs-global]: `isNaN` here uses `Number.isNaN()` internally — it does not coerce strings like the global `isNaN()`. `isNaN("foo")` returns `false`, not `true`. [^isthenable-duck-typed]: Duck-typed — matches native `Promise` instances and any other object exposing a callable `.then`, not just `instanceof Promise`. Use it to guard async-vs-sync return values; use `instanceof Promise` directly when only native promises should pass. > [!TIP] > Prefer these over raw comparisons. ## Helpers General-purpose utilities for numbers, arrays, and objects. ### clamp Clamp a number between a minimum and maximum: ```ts import { clamp } from '@vuetify/v0' clamp(15, 0, 10) // 10 clamp(-5, 0, 10) // 0 clamp(5, 0, 10) // 5 clamp(0.5) // 0.5 (defaults: min=0, max=1) ``` ### range Create an array of sequential numbers: ```ts import { range } from '@vuetify/v0' range(5) // [0, 1, 2, 3, 4] range(3, 1) // [1, 2, 3] range(5, 10) // [10, 11, 12, 13, 14] range(0) // [] ``` ### findMatchRanges Locate a query inside a string and get back `[start, end]` pairs, optionally folding accents before matching. This is what backs the `ignoreAccents` option of [toHighlight](/composables/transformers/to-highlight): ```ts import { findMatchRanges } from '@vuetify/v0' findMatchRanges('Zürich', 'zurich', { ignoreCase: true, ignoreAccents: true }) // [[0, 6]] findMatchRanges('Zürich', 'zurich', { ignoreCase: true }) // [] ``` `ignoreAccents` is directional. `'target'` folds only the text, so a plain query reaches accented entries; `'query'` folds only the search term, so a pasted `café` reaches plain `cafe`; `true` folds both sides. ```ts findMatchRanges('Łódź', 'Lo', { ignoreAccents: 'target' }) // [[0, 2]] findMatchRanges('cafe', 'café', { ignoreAccents: 'query' }) // [[0, 4]] ``` Folding strips Combining Diacritical Marks (U+0300–036F) after NFD, then common letters NFD leaves alone (`ł`, `ø`, `đ`, `ß`, `æ`, `œ`, …). When the text is folded or case-converted, ranges are mapped back onto the source — `ß` → `ss` still spans one source character, and `İ` still spans one. Pass `matchAll: true` for every occurrence instead of the first. ### getActiveElement Resolve the deepest focused node. Open shadow roots are walked so focus/keyboard logic sees the real control, not the host; light DOM matches `document.activeElement`. Closed roots are not traversable. Returns `null` under SSR. ```ts import { getActiveElement } from '@vuetify/v0' const active = getActiveElement() ``` ### pxToNumber Parse a CSS pixel length into a number. Built for reading `getComputedStyle` output, where a length that does not apply resolves to `''` or `'auto'` rather than to a number: ```ts import { pxToNumber } from '@vuetify/v0' const style = getComputedStyle(el) pxToNumber(style.marginLeft) // 16 pxToNumber(style.marginRight) // 0 (resolved '0px') pxToNumber(style.width) // 0 ('auto' does not parse) pxToNumber(undefined) // 0 ``` The second argument is the value returned when the length does not parse — it defaults to `0`: ```ts // A parsed 0 and an unparseable length are different answers pxToNumber('0px', rect.width) // 0 pxToNumber('auto', rect.width) // rect.width ``` That distinction is the reason to reach for this over `Number.parseFloat(value) || 0`, which collapses both cases onto `0` and so is only correct when the fallback is itself `0`. Only the leading number is read, matching `Number.parseFloat`, so any unit suffix is ignored. Values `getComputedStyle` never returns — percentages, `calc()` expressions, multi-value shorthands — are out of scope. ### mergeDeep Deep-merge objects without mutating inputs. Arrays are replaced, not concatenated: ```ts import { mergeDeep } from '@vuetify/v0' mergeDeep({ a: { b: 1 } }, { a: { c: 2 } }) // { a: { b: 1, c: 2 } } mergeDeep({}, { a: 1 }, { b: 2 }) // { a: 1, b: 2 } mergeDeep({ arr: [1, 2] }, { arr: [3] }) // { arr: [3] } ``` ### useId SSR-safe unique ID generation. Uses Vue's `useId()` inside components, falls back to a counter outside: ```ts import { useId } from '@vuetify/v0' // In component setup const id = useId() // 'v:0' (Vue's format) // Outside component const id = useId() // 'v0-0', 'v0-1', ... ``` --- # Benchmarks URL: https://0.vuetifyjs.com/guide/fundamentals/benchmarks # Benchmarks v0 maintains performance benchmarks for all core composables. This page explains what gets benchmarked, how to interpret metrics, and what the performance tiers mean. ## Why Benchmark Headless UI libraries must be fast—they're foundational infrastructure. v0 benchmarks exist to: 1. **Catch regressions** — CI fails if performance drops 2. **Guide optimization** — Data-driven decisions, not guesses 3. **Set expectations** — Users know what to expect at scale 4. **Validate minimal reactivity** — Prove the tradeoffs are worth it ## What Gets Benchmarked ### Core Composables | Composable | Why It's Benchmarked | | - | - | | `createRegistry` | Foundation for all collections—performance here affects everything | | `createModel` | Value store underlying all selection—selection benchmarks depend on it | | `createSelection` | Base for all selection patterns—select, toggle, mandatory, batch | | `createNested` | Hierarchical trees with cascade—tree traversal scales with depth | | `createTokens` | Design tokens can grow large—alias resolution must scale | | `createFilter` | Search/filter on large datasets must remain responsive | | `createVirtual` | Virtual scrolling is performance-critical by definition | | `createDataTable` | Composed orchestrator—measures sorting, filtering, and pagination together | | `createDataGrid` | Composed on top of createDataTable—measures column layout, cell editing, row ordering, and row spanning overhead | | `createSortable` | Ordered-list primitive—move, swap, and reorder operations must scale to large lists | | `useDate` | Date operations are frequent in UIs | | `useProxyRegistry` | Reactive proxy for templates—shows reactivity overhead vs raw registry | ### Operation Categories Each benchmark file covers multiple operation types: | Category | Fixture Type | What It Measures | | - | - | - | | Initialization | Fresh | Setup/creation cost | | Lookup operations | Shared | Single item access (O(1) expected) | | Mutation operations | Fresh | Updates and modifications | | Batch operations | Fresh | Bulk actions (onboard, offboard) | | Computed access | Shared | Cached/derived value reads | | Seek operations | Shared | Directional search | **Shared fixtures** reuse the same data structure across iterations—safe for read-only operations. **Fresh fixtures** create new data per iteration—required for mutations to get accurate measurements. ## Performance Tiers Tiers grade **cost**, not raw throughput. Raw ops/s mostly measures how much work a benchmark was given: an operation over 10,000 items at 17 ops/s is 5.9µs per item, which is better engineering than a single-item call at 200,000 ops/s and 5µs per item. Grading on ops/s would rank the second one higher. Which budget applies depends on whether the benchmark has a workload to amortize. **Collection operations** — anything measured over N items — are graded on two axes, and the worse of the two wins: | Tier | Per-item cost | Single-operation latency | |------|---------------|--------------------------| | Blazing | < 1µs per item | < 16.7ms — one frame at 60fps | | Fast | < 10µs | < 33.4ms — two frames | | Good | < 100µs | < 100ms — the perceptibility threshold | | Slow | ≥ 100µs | ≥ 100ms | The second axis exists so honest per-item cost cannot hide a slow operation: a 10,000-row sort taking 160ms has respectable per-item cost, but a user waits a tenth of a second for it, so it does not get to badge "fast". **One-shot operations** — constructors and single utility calls, where there is no workload to spread — are graded on call latency alone. Grading them per-item would be a category error, since the item count is one: | Tier | Per call | |------|----------| | Blazing | < 10µs | | Fast | < 100µs | | Good | < 16.7ms — within one frame | | Slow | ≥ 16.7ms | The latency thresholds are the only ones anchored to something outside the library: 16.7ms is a frame at 60fps, and 100ms is the long-standing threshold below which an interface feels instantaneous. The per-item and per-call budgets are engineering conventions. **A feature's badge is its worst group, and a group's badge is its worst benchmark.** Averaging would let one flattering microbenchmark mask a genuinely slow path, which is the opposite of what these badges are for. > [!NOTE] > Numbers are measured on a single fixed reference machine so they stay comparable between releases, and are published as measured with no host correction applied. That machine is deliberately not the fastest available — a benchmark host should err pessimistic. Absolute figures will differ on your hardware; the tiers and the trends are the parts that carry meaning. ### Reading Results ```bash ✓ createRegistry/index.bench.ts lookup operations ✓ Get by id (1,000 items) 1,234,567 ops/s ✓ Get by id (10,000 items) 1,198,432 ops/s ``` - **ops/s** — Operations per second (higher is better) - **Consistent across sizes** — O(1) complexity confirmed - **10x data, ~same speed** — Good scaling behavior ## Dataset Sizes Benchmarks test multiple sizes to reveal complexity: | Size | Items | Purpose | | - | - | - | | Medium | 1,000 | Baseline measurement | | Large | 10,000 | Reveals O(n) vs O(1) | | Small | 100 | Optional edge case | | Stress | 100,000 | Optional stress test | If a 10,000-item benchmark is 10x slower than 1,000-item, the operation is O(n). If it's roughly the same speed, it's O(1). ## Running Benchmarks ```bash # Run all benchmarks pnpm test:bench # Run specific file pnpm vitest bench packages/0/src/composables/createRegistry/index.bench.ts ``` Numbers from your own machine are useful for spotting a regression you just introduced, and are not comparable to the published figures — different CPU, different results. The artifacts behind this site are regenerated only on the project's reference machine, so please don't commit locally generated ones; CI rejects them. ## Interpreting for Your Use Case ::: faq single ??? Is this fast enough for my app? Most Vuetify0 composables handle 10,000+ items at interactive speeds (>60fps). For typical UIs: - **<100 items** — Instant, no optimization needed - **100-1,000 items** — Smooth, standard usage - **1,000-10,000 items** — Consider virtual scrolling - **10,000+ items** — Use `createVirtual`, paginate, or filter ??? Should I use events or polling? - **Events** — Real-time updates, notifications, debugging - **Polling (`values()`)** — Periodic snapshots, non-critical freshness - **`useProxyRegistry`** — Template-bound lists that must stay current Events add minimal overhead when enabled. Benchmarks show raw operation cost; event emission adds ~1-5% overhead. ??? Why minimal reactivity instead of full reactivity? Vue's reactivity system is powerful but not free. Each reactive wrapper adds: - Memory for dependency tracking - CPU cycles for change detection - Potential for unnecessary re-renders By keeping reactivity minimal, Vuetify0 composables stay predictable—you know exactly what triggers updates. When you need reactivity, opt in explicitly with `useProxyRegistry`. ??? How do I compare raw vs reactive performance? Compare `createRegistry` benchmarks with `useProxyRegistry` to see the reactivity overhead. It's worth it when needed, but shouldn't be the default. ::: ## Explorer Browse all benchmark results. Select a composable to filter, or expand groups to compare individual operations. ## Contributing Benchmarks New composables should include benchmarks if they: - Manage collections (registries, arrays, maps) - Perform search/filter operations - Have user-perceived latency (loading, transitions) - Are called frequently (every render, every keystroke) See [createRegistry benchmarks](https://github.com/vuetifyjs/0/blob/master/packages/0/src/composables/createRegistry/index.bench.ts) for the canonical example. --- # Building Frameworks URL: https://0.vuetifyjs.com/guide/fundamentals/building-frameworks # Building Frameworks When building a component framework, you're often reimplementing the same patterns: selection state, keyboard navigation, form validation, and focus management. Vuetify0 provides these behaviors as headless primitives so you can focus on what makes your framework unique—its design language. > [!NOTE] Prerequisites > This guide builds on concepts from [Core](/guide/fundamentals/core), [Components](/guide/fundamentals/components), and [Composables](/guide/fundamentals/composables). Read those first if you're new to v0, or check the [Glossary](/guide/essentials/glossary) for a quick term lookup. ## Getting Started ### New Project Use `vuetify0 create` to scaffold a new project with Vuetify0 pre-configured: ::: code-group no-filename ```bash pnpm pnpm create vuetify0 ``` ```bash npm npm create vuetify0 ``` ```bash yarn yarn create vuetify0 ``` ```bash bun bun create vuetify0 ``` ::: ### Existing Project Add v0 to an existing Vue project: ::: code-group no-filename ```bash pnpm pnpm add @vuetify/v0 ``` ```bash npm npm install @vuetify/v0 ``` ```bash yarn yarn add @vuetify/v0 ``` ```bash bun bun add @vuetify/v0 ``` ::: Vuetify0 uses [subpath exports](https://nodejs.org/api/packages.html#subpath-exports) for tree-shaking: ```ts // Import everything import { createSingle, Atom } from '@vuetify/v0' // Or import specific modules import { createSingle } from '@vuetify/v0/composables' import { Atom } from '@vuetify/v0/components' import type { ID } from '@vuetify/v0/types' import { isObject } from '@vuetify/v0/utilities' import { IN_BROWSER } from '@vuetify/v0/constants' ``` ## Two Integration Patterns This guide covers two approaches: | Pattern | When to Use | | - | - | | **Pattern A: Behavior-Focused** | Need complex state (selection, navigation, validation) with full rendering control | | **Pattern B: Component Wrappers** | Building styled components on top of Vuetify0's headless primitives | > [!TIP] > v0's composables are completely headless—they manage state and behavior without any DOM assumptions. This makes them ideal for building design systems that need complete control over markup and styling. ## Core Concepts ### Direct vs Context APIs Vuetify0 composables offer two API surfaces. The direct API creates standalone instances—perfect for component-local state. The context API uses Vue's [provide/inject](https://vuejs.org/guide/components/provide-inject.html) for sharing state across component trees. ```ts // Direct instance (component-local) const tabs = createSingle({ mandatory: true }) // Context trinity (for dependency injection) const [useTabs, provideTabs, defaultTabs] = createSingleContext() ``` > [!TIP] > Start with the direct API. Only reach for contexts when you need to share state between parent and child components that can't communicate via props. ### The Ticket System When you register items with a Vuetify0 registry, you get back "tickets"—plain objects that contain reactive properties and bound methods. Properties like `isSelected` are Vue refs that update automatically when selection state changes. ```ts tabs.register({ id: 'home', value: 'Home' }) const ticket = tabs.get('home') ticket.id // 'home' (static) ticket.value // 'Home' (static) ticket.index // 0 (static, position in registry) ticket.isSelected // Ref - reactive! ticket.isSelected.value // true/false - access the value ticket.toggle() // Toggle selection ticket.select() // Select this item ``` ### Template Iteration Registry internals aren't directly exposed for template iteration. `useProxyRegistry` wraps a registry and provides reactive arrays that update when items change: ```vue playground ``` > [!TIP] > The `events: true` option is required. `useProxyRegistry` listens for registry events to invalidate its cache. ## Pattern A: Behavior-Focused Use v0's composables directly when you need complex state management—selection, navigation, validation—but want full control over rendering. ::: gn-example /guide/building-frameworks/pattern-a/tabs-basic ### Behavior-Focused Tabs Custom tabs using `createSingle` and `useProxyRegistry` with ARIA attributes and accessible keyboard navigation. ::: ### Adding Keyboard Navigation Vuetify0 composables handle state; you add the interaction layer: ::: gn-example /guide/building-frameworks/pattern-a/tabs-keyboard ### Keyboard Navigation Arrow key, Home, and End key support added to the tabs above. ::: ### Multi-Selection with Groups Use `createGroup` for checkbox-style multi-selection with tri-state support: ::: gn-example /guide/building-frameworks/pattern-a/accordion ### Multi-Select Accordion Accordion with expand/collapse-all using `createGroup` and tri-state support. ::: > [!ASKAI] How does building a Tabs UI manually with createSingle and useProxyRegistry compare to Vuetify0's built-in Tabs component? ## Pattern B: Component Wrappers Wrap Vuetify0's headless components with your design system's styling. Vuetify0 handles behavior, accessibility, and keyboard navigation—you control the visual presentation. ### Polymorphic Elements The `Atom` component provides polymorphic rendering via the `as` prop—render as any HTML element while keeping your component's API consistent: ::: gn-example /guide/building-frameworks/pattern-b/button ### Polymorphic Button An Atom-based button with BEM class names and style/size/color variants. ::: > [!TIP] > The example uses [BEM naming](https://getbem.com/)—a convention for organizing CSS in component libraries. Blocks (`.my-button`), elements (`__icon`), and modifiers (`--filled`, `--primary`) create predictable, collision-free class names. ### Styling Headless Components Wrap v0's compound components with custom CSS. The components expose data attributes like `data-state` for styling different states: ::: gn-example /guide/building-frameworks/pattern-b/card ### Styled ExpansionPanel ExpansionPanel wrapped with custom CSS targeting data attributes and slot props for visual state. ::: ### Form Components v0's form components handle focus, keyboard interaction, and ARIA attributes. Apply your styles via classes: ::: gn-example /guide/building-frameworks/pattern-b/input ### Styled Checkbox Checkbox wrapped with label and description, styled via data attributes for checked and disabled states. ::: ## Plugin Architecture v0's plugins follow Vue's [plugin pattern](https://vuejs.org/guide/reusability/plugins.html) with additional structure for namespaced context provision. ::: gn-example /guide/building-frameworks/plugins/setup ### Plugin Access Reading and toggling theme and breakpoint state anywhere in the component tree via `useTheme` and `useBreakpoints`. ::: > [!TIP] > Vuetify0 plugins are designed to be order-independent. Each plugin gracefully handles missing dependencies by providing sensible fallbacks. ### Creating Custom Plugins Use `createPluginContext` to build your own plugins without boilerplate. It returns the standard `[createContext, createPlugin, useContext]` triple: ```ts import { createPluginContext } from '@vuetify/v0' interface ThemeOptions { namespace?: string primary?: string } interface ThemeContext { primary: string setPrimary: (color: string) => void } function createThemeFeature (options: Omit): ThemeContext { const primary = shallowRef(options.primary ?? '#1976d2') return { primary, setPrimary: (color) => { primary.value = color } } } export const [createMyThemeContext, createMyThemePlugin, useMyTheme] = createPluginContext( 'my-ui:theme', options => createThemeFeature(options), ) // In main.ts app.use(createMyThemePlugin({ primary: '#e91e63' })) // In any component const { primary, setPrimary } = useMyTheme() ``` See [createPlugin](/composables/foundation/create-plugin) for the full API including `persist`/`restore` lifecycle hooks for saving and rehydrating plugin state. ## SSR Safety Vuetify0 is designed for universal rendering. Use the provided constants and composables to guard browser-only code: ```ts import { useHydration, useWindowEventListener } from '@vuetify/v0' // SSR-safe event listener (no-op on server, auto-cleanup) useWindowEventListener('resize', handler) // Reactive hydration state for conditional rendering const { isHydrated } = useHydration() // In templates: v-if="isHydrated" ``` The `isHydrated` shallowRef is `false` during SSR and becomes `true` after the root component mounts. This prevents hydration mismatches when rendering browser-dependent content. ::: gn-example /guide/building-frameworks/ssr/hydration-guard ### SSR Hydration Guard Using `useHydration()` and `IN_BROWSER` to safely defer browser-only rendering until after hydration. ::: ## TypeScript Patterns Vuetify0 uses generics extensively. When extending composables, provide your custom ticket and context types: ::: gn-example /guide/building-frameworks/typescript/type-extension ### Type Extension Extending `SelectionTicket` and `SelectionContext` with custom properties for type-safe file selection. ::: Vue's [shallowReactive](https://vuejs.org/api/reactivity-advanced.html#shallowreactive) and [computed](https://vuejs.org/api/reactivity-core.html#computed) are used internally—understanding these helps when debugging reactivity issues. ## Complete Example: @example/my-ui To see everything come together, we've included a complete example library that demonstrates the patterns in this guide. ::: gn-example /guide/building-frameworks/my-ui/demo ### Complete Design System MyButton, MyTabs, and MyAccordion assembled from Vuetify0 primitives — all patterns from this guide working together. ::: The example package includes: ``` my-ui/ ├── package.json # Peer deps on @vuetify/v0 and vue ├── vite.config.ts # Library build config ├── src/ │ ├── index.ts # Public exports │ ├── plugin.ts # Vue plugin with Vuetify0 setup │ └── components/ │ ├── MyButton.vue # Atom wrapper (polymorphic) │ ├── MyTabs.vue # createSingle + keyboard nav │ └── MyAccordion.vue # ExpansionPanel wrapper ``` View the full source in the [examples directory](https://github.com/vuetifyjs/0/tree/master/apps/docs/src/examples/guide/building-frameworks/my-ui). --- # Components Guide URL: https://0.vuetifyjs.com/guide/fundamentals/components # Components Vuetify0 components are Vue wrappers around composables. Composables hold logic, components provide Vue integration via slots, props, and emits. ## Component Philosophy - **Headless**: Zero styling - you own all CSS - **Slot-driven**: All customization through scoped slots - **Accessible**: ARIA attributes via `attrs` object - **Composable-backed**: Use components or composables directly > [!TIP] > Always spread the `attrs` object onto your elements. It contains ARIA attributes and data attributes for accessibility and styling. ## Component Categories | Category | Purpose | Examples | | - | - | - | | **Primitives** | Base building blocks | [Atom](/components/primitives/atom), [Presence](/components/primitives/presence) — [all primitives](/components#primitives) | | **Providers** | Pure state management, no DOM | [Selection](/components/providers/selection), [Theme](/components/providers/theme) — [all providers](/components#providers) | | **Actions** | Interactive controls that trigger behavior | [Button](/components/actions/button), [Toggle](/components/actions/toggle) — [all actions](/components#actions) | | **Forms** | Form controls with accessibility | [Checkbox](/components/forms/checkbox), [Select](/components/forms/select), [Form](/components/forms/form) — [all forms](/components#forms) | | **Semantic** | Meaningful HTML defaults | [Avatar](/components/semantic/avatar), [Pagination](/components/semantic/pagination) — [all semantic](/components#semantic) | | **Disclosure** | Show/hide patterns | [Dialog](/components/disclosure/dialog), [AlertDialog](/components/disclosure/alert-dialog), [Tabs](/components/disclosure/tabs) — [all disclosure](/components#disclosure) | Full inventory with descriptions: [Components](/components). ## Atom: The Foundation The `Atom` component is a polymorphic base element supporting any HTML tag: ```vue ``` ### Rendering Modes | Mode | Usage | Output | | - | - | - | | Element | `as="button"` | `