Version: 1.0.0
Status: Stable
Last updated: 2026-08-06
Maintainers: Volt UI contributors
Post-v1 design proposal (2026-09-07):
specs/v2/README.mdcontains the current audit and a bounded evolution plan. This document describes the v1 architecture; the new proposal does not imply an implemented or released v2.
Volt UI is an Angular component library inspired by shadcn/ui. It ships ready-to-copy components that consumers own and customize in their own codebases, combined with a published theme/CSS package (@voltui/components) for the design tokens and runtime utilities.
Volt UI is an independent Angular implementation under the @voltui scope. It is not affiliated with, derived from, or maintained by PrimeVue Volt UI.
The project exists because the Angular ecosystem lacks a shadcn/ui equivalent: existing solutions such as Spartan/ng are mostly headless directives, while Volt UI provides higher-level, opinionated but customizable components with a built-in theme system.
- For Angular: first-class support for Angular 21+ (zoneless, standalone components, signals).
- Copy-paste ownership: components are copied into the consumer project via a CLI, so teams can customize markup, styles, and behavior.
- Primitive-based accessibility: accessibility and interaction logic come from ng-primitives, the Angular equivalent of Radix UI.
- Tailwind-first styling: styles are built with Tailwind CSS v4 and
class-variance-authority(CVA), making variants fast to author and consistent. - Theme engine: multiple color and style presets, switchable at runtime, backed by CSS custom properties.
Volt UI has two consumption paths:
- CLI / copy-paste workflow (recommended): install
@voltui/cli, runvolt init, thenvolt add <component>. Source files are copied into the consumer'ssrc/app/ui/folder, renamed fromvolt-*/Volt*toui-*/Ui*, and wired to local imports. - npm package workflow: install
@voltui/componentsto reuse the theme CSS, theme provider, and a small set of utilities directly. Full package component consumption is advanced and secondary to source ownership.
The library is not a traditional drop-in component package for every component; the source-code ownership model is the primary design choice.
- Provide a shadcn/ui-like experience for Angular developers.
- Keep components accessible by default through ng-primitives.
- Make components easy to customize because the source lives in the consumer project.
- Support multiple visual presets (colors + styles) without changing component code.
- Offer a first-class CLI for scaffolding and adding components.
- Maintain a docs site with live demos, source snippets, and usage examples.
- Reach v1 with a stable public API, solid test coverage, and hardened overlay/form components.
- Be a locked, black-box UI kit (consumers are expected to edit the copied code).
- Support Angular versions older than v21.
- Maintain a Tailwind v3 configuration (
tailwind.config.jsis intentionally absent). - Provide every possible component variant out of the box; variants are templates consumers can extend.
- Re-implement low-level accessibility primitives from scratch (ng-primitives handles this).
┌─────────────────────────────────────────────────────────────┐
│ Docs App (volt-ui) │
│ AnalogJS + Vite + SSR + Tailwind v4 │
│ Pages: src/app/pages/(components-docs)/docs/components/ │
└─────────────────────────────────────────────────────────────┘
│
│ imports via `volt` alias
▼
┌─────────────────────────────────────────────────────────────┐
│ Library Source (projects/volt) │
│ Components Layouts Themes Theme provider │
│ public-api.ts │
└─────────────────────────────────────────────────────────────┘
│
│ build / copy
▼
┌─────────────────────────────────────────────────────────────┐
│ CLI (@voltui/cli) │
│ init / add / list / clear-cache │
│ Transforms volt-* → ui-* and copies source locally │
└─────────────────────────────────────────────────────────────┘
/
├── projects/volt/ # Publishable Angular library
│ ├── src/lib/components/<name>/ # Component source folders
│ ├── src/lib/layouts/<name>/ # Layout components (e.g. sidebar)
│ ├── src/themes/ # Theme presets and core tokens
│ ├── src/public-api.ts # Public exports
│ └── package.json # @voltui/components manifest
├── src/app/ # AnalogJS docs app
│ ├── pages/(components-docs)/docs/components/<name>.page.ts
│ └── lib/snippets/ # Source and usage snippets
├── cli/ # @voltui/cli package
│ ├── bin/volt # CLI entry
│ ├── lib/core.js # Copy/transform logic
│ └── generate-manifest.js # public/manifest.json generator
├── public/manifest.json # Component manifest
├── e2e/ # Playwright tests
├── package.json # Workspace root
└── vite.config.ts # Vite + AnalogJS config
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Framework | Angular | ^21.2.2 | UI framework, zoneless change detection, standalone components, signals |
| Docs / SSR | AnalogJS | ^2.6.1 | File-based routing, SSR, Nitro preset for Cloudflare Pages |
| Build tool | Vite | ^7.3.1 | Dev server and production builds |
| Styling | Tailwind CSS | ^4.3.0 | CSS-only configuration, @theme inline |
| Primitives | ng-primitives | 0.110.2 | Accessible behaviors (button, dialog, select, tabs, etc.) |
| Variants | class-variance-authority | ^0.7.1 | Type-safe component variants |
| Class merging | clsx + tailwind-merge | ^2.1.1 / ^3.5.0 | Conditional and deduplicated class strings |
| Forms | @angular/forms | ^21.2.2 | ControlValueAccessor integration |
| Positioning | @floating-ui/dom | ^1.6.0 | Overlay positioning |
| Icons | lumen-icons | ^0.1.0 | Icon set |
| Testing (unit) | Vitest + @testing-library/angular | ^4.1.8 / ^19.2.1 | Component unit tests |
| Testing (e2e) | Playwright | ^1.60.0 | Browser and consumer fixture tests |
| Package manager | pnpm | 10.30.1 | Workspace dependency management |
| Deploy | Wrangler / Cloudflare Pages | ^4.72.0 | Docs app hosting |
Every library component follows these rules:
- Standalone component — no NgModules.
- OnPush change detection everywhere.
- Signals API —
input(),output(),model(),computed()for derived class strings. - Boolean inputs must use
booleanAttribute:readonly disabled = input<boolean, unknown>(false, { transform: booleanAttribute });
- Number inputs should use
numberAttributewhen relevant (e.g. slider). - CVA for variants — each variant surface is defined with
class-variance-authorityand exported as<name>Variants. - ng-primitives integration via direct template directives (
ngpButton,ngpInput) orhostDirectives(accordion, tabs, slider, etc.). - Form integration — input, checkbox, switch, toggle, select, slider, textarea implement
ControlValueAccessorwithNG_VALUE_ACCESSOR. - Class merging utility — components merge base, variant, and user classes with
clsx+tailwind-merge.
projects/volt/src/lib/components/button/
├── index.ts # Public barrel export
├── button.ts # Main component + CVA variants
└── button.spec.ts # Unit tests (required for v1)
| Scope | Selector | Class | File names |
|---|---|---|---|
| Library | volt-* |
Volt* |
button.ts, index.ts |
| Consumer (after CLI) | ui-* |
Ui* |
button.ts, index.ts |
Form components must:
- Implement
ControlValueAccessor. - Accept
disabledstate and propagate it to ng-primitives directives. - Emit changes via the Angular forms API.
- Remain compatible with reactive and template-driven forms.
Components use Tailwind utilities (bg-primary, rounded-md, shadow-sm) instead of arbitrary var() values. Theme tokens are CSS custom properties mapped to Tailwind theme keys via @theme inline.
- Source variables (
--volt-*) — defined inprojects/volt/src/themes/core.cssand preset files. Example:--volt-shadow-sm. - Tailwind theme mapping — maps semantic keys to source variables. Example:
--shadow-sm: var(--volt-shadow-sm);. - Semantic variables —
--background,--foreground,--primary,--secondary,--destructive,--success,--warning,--error,--info, etc.
Color palettes:
volt(default) — blue-purpleember— warm orange-redsage— greendusk— purpleglacier— cool blue
Style presets:
sharp(default) — moderate border radiussoft— larger radiusbrutal— no radius, heavy bordersghost— minimal, transparentretro— classic aesthetic
Import the bundled theme CSS:
@import '@voltui/components/themes.css';Provide the theme at bootstrap:
import { provideVoltTheme } from '@voltui/components';
bootstrapApplication(AppComponent, {
providers: [provideVoltTheme({ color: 'volt', style: 'sharp', dark: false })],
});Or apply dynamically:
import { applyVoltTheme } from '@voltui/components';
applyVoltTheme({ color: 'ember', style: 'soft', dark: true });The provider sets data-color, data-style, and the .dark class on the document element.
The CLI package is @voltui/cli located in cli/.
| Command | Description |
|---|---|
volt init [target-dir] |
Scaffolds the local ui/ folder with an index.ts. |
volt add <component> [target-dir] [--install] |
Copies a component and its local dependencies, transforms naming, and optionally installs runtime deps. |
volt add <component> [target-dir] [--dry-run] |
Prints the files that would be copied without writing. |
volt add <component> [target-dir] [--force] |
Allows overwriting existing copied files. Default behavior refuses overwrite. |
volt list |
Lists components from public/manifest.json. |
volt clear-cache |
Clears ~/.volt-ui/cache. |
- Reads source from
projects/volt/src/lib(local copy workflow). - Replaces
volt-selectors withui-. - Replaces
Volt*/volt*identifiers withUi*/ui*. - Rewrites
from 'volt'imports to local./index. - Updates the target
index.tsbarrel. - Refuses to overwrite existing files unless
--forceis passed. - Installs runtime dependencies if
--installis passed:ng-primitives,class-variance-authority,clsx,tailwind-merge. - Supports
--dry-runto preview the file plan without creating directories, writing files, or installing packages.
public/manifest.json is generated by cli/generate-manifest.js and describes:
- Available components and layouts.
- Files per component.
- Version.
- Transitive dependencies detected automatically from source imports (
'volt'alias and relative imports across component/layout folders).
Run pnpm manifest to regenerate it after component changes.
- Dependency mapping relies on explicit cross-component imports in source files; components that are only used together in demos but not imported in source must still be added manually if needed.
- Adding multiple components is supported, but dependency mapping still comes from the manifest and explicit source imports.
The docs app is an AnalogJS application using file-based routing.
- Component docs live at
src/app/pages/(components-docs)/docs/components/<name>.page.ts. - Getting-started and other content pages live under
src/app/pages/(getting-started)/docs/.
A typical component demo page:
- Imports the library component via the
voltalias. - Uses a
CodePanelcomponent to display source and usage snippets. - Imports raw source from
src/app/lib/snippets/index.ts. - Imports usage examples from
src/app/lib/snippets/usage.ts.
src/app/lib/snippets/index.ts— exports raw component source files (*.ts?raw) for the "Copy code" feature.src/app/lib/snippets/usage.ts— exports usage examples for every public component.
When adding or editing a component, both snippet files must be updated.
- Runner: Vitest.
- Setup:
test-setup.tsconfigures zonelessTestBed, jest-dom matchers, and common DOM mocks (matchMedia,ResizeObserver,getAnimations). - Library tests: live next to components in
projects/volt/src/lib/components/**/*.spec.ts. - App tests: live in
src/app/**/*.spec.ts. - Pattern: prefer
@testing-library/angularrender()+screen+userEventover manualTestBedwiring.
- Tool: Playwright.
- Main config:
playwright.config.ts— tests the docs app in Chromium and Firefox. - Consumer config:
playwright.consumer.config.ts— tests the built library inside a fixture Angular app. - Coverage: smoke tests on demo pages and overlay behavior (select, popover, dropdown, tooltip, dialog, drawer).
cli/tests/core.spec.jscoverstransformContent, manifest loading,initProject,copyComponent, dependency copying, overwrite protection,--force, and--dry-run.
pnpm lint # ESLint for .ts and .html
pnpm typecheck # tsc --noEmit
pnpm test:run # Vitest unit tests
pnpm build:lib # ng-packagr library build
pnpm test:e2e:ci # Build + Playwright smoke tests- ESLint:
eslint.config.jsuses@eslint/js,typescript-eslint,angular-eslint,prettier.- Valid component prefixes:
app,volt,ui. cli/is ignored by the root config (CLI has its own package).
- Valid component prefixes:
- Prettier: formats TS, HTML, CSS, SCSS, JSON, MD.
- Husky: pre-commit runs
lint-staged; commit-msg hook is present but currently disabled.
- CI (
.github/workflows/ci.yml): lint, typecheck, unit tests, library build, CLI pack, Playwright install, e2e smoke + consumer tests. - Deploy (
.github/workflows/deploy-cloudflare-pages.yml): lint, typecheck, unit tests, lib + app build, deploy to Cloudflare Pages.
The library currently exposes 41 component groups plus one layout from projects/volt/src/public-api.ts:
- Basic: button, badge, input, search, autofill, textarea, card
- Form: checkbox, radio, switch, toggle, form-field, select, slider, range-slider, input-otp, file-upload, combobox, date-picker, listbox
- Overlays: tooltip, dialog, drawer, popover, dropdown-menu, toast
- Navigation: navigation-menu, tabs, accordion, breadcrumbs, pagination, toolbar, sidebar (layout)
- Data / Feedback: avatar, separator, progress, meter, toggle-group, skeleton, table, resizable
- Theming: theme provider and utilities
The public API is frozen as of 1.0.0 — see §12.0 for the post-v1 stability policy.
Volt UI reached 1.0.0 on 2026-08-06. The road there (form/overlay hardening, full test
coverage, CLI robustness, documentation completeness, theme polish, bundle/perf audits,
API freeze) is recorded in CHANGELOG.md release-by-release — see the
[0.5.0] through [1.0.0] entries — rather than duplicated here as a roadmap.
- Breaking changes only happen in a major version bump. The public API frozen at
1.0.0(inventoried inspecs/api-freeze-0.9.md) will not change incompatibly in any1.xrelease. - New features land in minor releases (
1.1.0,1.2.0, ...): new components, new optional inputs/outputs, new CVA variants that don't change existing defaults. - Fixes land in patch releases (
1.0.1,1.0.2, ...): bug fixes, accessibility corrections, and internal refactors that don't change the public contract. betacomponents may still gain forms/keyboard/accessibility hardening in minor or patch releases without that counting as a breaking change, as long as the existing public API keeps working. Moving a component frombetatostableis a documentation change, not an API change, and never requires a major bump on its own.- A component is considered
stableonly when its public API is documented, source-copy output is usable, forms/keyboard behavior is tested where applicable, and known accessibility caveats are documented. SeeCOMPONENT_STATUS.mdfor the current label per component. - User-facing detail (semver promise, Angular-major support policy) lives in the docs "Versioning & stability" page; this section is the source-of-truth for contributors.
When modifying this codebase:
- Read
AGENTS.mdfor the latest conventions and commands. - Components live in
projects/volt/src/lib/components/<name>/. - Update
projects/volt/src/public-api.tsfor new public exports. - Add/update the demo page in
src/app/pages/(components-docs)/docs/components/<name>.page.ts. - Add/update snippet exports in
src/app/lib/snippets/index.tsandsrc/app/lib/snippets/usage.ts. - Add real unit tests using
@testing-library/angularorTestBed. - Run
pnpm typecheck,pnpm lint,pnpm test:run, andpnpm build:libbefore committing. - Regenerate the CLI manifest with
pnpm manifestwhen component files change. - Keep this SDD in sync if architecture, scope, or conventions change.
| Term | Meaning |
|---|---|
| CVA | class-variance-authority — type-safe class variant generator. |
| CLI | @voltui/cli — command-line tool for copying components. |
| ng-primitives | Headless, accessible Angular primitives used as the behavioral layer. |
| OnPush | ChangeDetectionStrategy.OnPush — used in every component. |
| Preset | A pre-defined color or style theme (e.g. volt, sharp). |
| SDD | This Software Design Document. |
| shadcn/ui | React UI library that popularized the copy-paste component model. |
| Zoneless | Angular change detection without zone.js, relying on signals. |