@quartz-headless/primitives
v0.6.0
Published
Headless, accessible Angular 21 UI primitives built on Quartz Core — dialog, tooltip, toast, navigation, layout, stepper, selection and controls.
Maintainers
Readme
🪨 Quartz Headless
Unstyled, accessible Angular 21 UI primitives.
You own the styles — Quartz owns the behaviour.
🌐 Live docs & demos · 📦 Core · 📦 Primitives · 🐛 Report a bug
Quartz is the behaviour layer for Angular design systems — overlay positioning, focus traps, drag & drop, keyboard navigation and reactive state — with zero CSS opinions. It is Radix UI / Headless UI for Angular, combined with the shadcn/ui "copy the source into your project" distribution model.
Quartz ships as two packages: @quartz-headless/core (low-level infrastructure — think
Angular CDK) and @quartz-headless/primitives (accessible UI patterns built on Core).
// Behaviour in, styling yours.
import { OverlayTriggerDirective } from '@quartz-headless/core';
@Component({
imports: [OverlayTriggerDirective],
template: `
<button qzOverlayTrigger [overlayTemplate]="menu" placement="bottom-start">Open</button>
<ng-template #menu><div class="your-styles">…</div></ng-template>
`,
})
export class Demo {}✨ Why Quartz
| | |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| 🎨 No visual theme | No theme or visual design opinions. Structural portal/layout styles only where behaviour requires them; use data-qz-* hooks for your UI. |
| ⚡ Zoneless | Built for provideZonelessChangeDetection() — signals all the way down. |
| ♿ Accessible | WAI-ARIA roles, focus management and full keyboard support baked in. |
| 🌳 Tree-shakeable | Standalone directives & services with no import-time side effects. |
| 🖥️ SSR-safe | Guards DOM access so it runs cleanly under Angular server rendering. |
| 📦 Two ways to ship | Install the npm package or copy the raw source with the CLI — your call. |
🚀 Install
Option A — npm packages
npm install @quartz-headless/core @quartz-headless/primitives
# or just the core package if you're building your own patterns on top of it:
npm install @quartz-headless/core// app.config.ts
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [provideZonelessChangeDetection()],
};The previous unscoped
quartz-headlesspackage is frozen at its last published version — new work happens in the two packages above.
Option B — copy the source (shadcn-style)
The CLI copies raw TypeScript into your project, so you own the code and can modify it freely.
git clone https://github.com/Andersseen/quartz.git
cd quartz && pnpm install
pnpm quartz list # see everything available
pnpm quartz add overlay # add one Core piece (+ its Core deps)
pnpm quartz add dialog splitter # add several at once
pnpm quartz add toast --output src/app/uiFiles land flat in your project's src/lib/components/<name>/ (auto-detected) or the
--output path. Core pieces (overlay, dismiss, focus, collection, directionality,
viewport, drag-drop, virtual-scroll, splitter, scroll-lock) stay pure copy-source with zero npm
dependencies — copying one pulls in its Core siblings too (e.g. overlay also copies
dismiss and directionality). Primitives
(dialog, tooltip, toast, tree, listbox, menu, popover, combobox, select, tabs, accordion,
switch, checkbox, radio-group, toggle, toggle-group, slider) depend on
@quartz-headless/core as a real npm package when they compose Core utilities. pnpm quartz
add dialog copies only dialog/ and tells you to npm install @quartz-headless/core.
🧩 Primitives
| Primitive | Package | What it gives you |
| -------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
| overlay | @quartz-headless/core | Portal-based positioning for dropdowns, menus and popovers |
| dialog | @quartz-headless/primitives | Service-driven dialog & drawer with backdrop and focus trap |
| splitter | @quartz-headless/core | Resizable panels with keyboard, touch and ARIA slider semantics |
| toast | @quartz-headless/primitives | Notification system with position groups and auto-dismiss |
| drag-drop | @quartz-headless/core | Native HTML drag & drop with sortable drop zones |
| tooltip | @quartz-headless/primitives | Hover/focus tooltip with configurable placement |
| tree | @quartz-headless/primitives | Tree view with roving tabindex, WAI-ARIA keyboard nav & lazy per-level loading |
| listbox | @quartz-headless/primitives | Single/multi selection with WAI-ARIA keyboard navigation and type-ahead |
| menu | @quartz-headless/primitives | Dropdown menu with submenus, checkbox/radio items, RTL and type-ahead |
| popover | @quartz-headless/primitives | Non-modal interactive floating content with dismiss and optional initial focus |
| combobox | @quartz-headless/primitives | Editable suggestions with input focus, active-descendant navigation and filtering |
| select | @quartz-headless/primitives | Button-triggered single select with listbox popup and type-ahead |
| tabs | @quartz-headless/primitives | Tablist, tabs and panels with roving focus, orientation and RTL support |
| accordion | @quartz-headless/primitives | Single, collapsible or multiple disclosure sections |
| switch | @quartz-headless/primitives | Button-based ARIA switch with controlled checked state |
| checkbox | @quartz-headless/primitives | Button-based ARIA checkbox with checked, unchecked and mixed state |
| radio-group | @quartz-headless/primitives | Standalone radio selection with roving focus, orientation and RTL support |
| toggle | @quartz-headless/primitives | Pressed/unpressed button semantics with aria-pressed |
| toggle-group | @quartz-headless/primitives | Single or multiple toggle-button selection with roving focus |
| slider | @quartz-headless/primitives | Single-thumb ARIA slider with keyboard, pointer, decimal step and RTL support |
| virtual-scroll | @quartz-headless/core | Windowed rendering for long lists |
| viewport | @quartz-headless/core | Reactive breakpoint service + ViewportMatchDirective |
| directionality | @quartz-headless/core | LTR/RTL resolution + logical inline-start/end and keyboard helpers |
| scroll-lock | @quartz-headless/core | Per-Document body scroll locking for modal and floating workflows |
Every primitive is zoneless, standalone and tree-shakeable. Drag & drop follows the browser's native pointer-based HTML DnD model; keyboard drag-and-drop is intentionally deferred to a dedicated future primitive.
🛠 Example
import { Component, inject, ViewContainerRef, TemplateRef, viewChild } from '@angular/core';
import { DialogService } from '@quartz-headless/primitives';
@Component({
template: `
<button (click)="open()">Open dialog</button>
<ng-template #tpl let-ref>
<div class="your-modal">
<h2>Delete item?</h2>
<button (click)="ref.close()">Cancel</button>
</div>
</ng-template>
`,
})
export class Example {
private dialog = inject(DialogService);
private vcr = inject(ViewContainerRef);
private tpl = viewChild.required<TemplateRef<unknown>>('tpl');
open() {
this.dialog.open(this.tpl(), this.vcr, { position: 'center' });
}
}📋 Requirements
| Dependency | Version |
| ----------------- | -------------------- |
| @angular/core | ^21.0.0 (zoneless) |
| @angular/common | ^21.0.0 |
| Node.js | >= 20 |
No runtime dependencies beyond @angular/*, rxjs and tslib.
💻 Local development
pnpm install
pnpm start # dev server → http://localhost:5173
pnpm build:lib # build both libraries → packages/core/dist/, packages/primitives/dist/
pnpm test # unit tests (Vitest)
pnpm e2e # end-to-end tests (Playwright)
pnpm typecheck # type check both libs + app
pnpm lint # lintThe demo/docs site is an AnalogJS app deployed to Cloudflare Pages
(pnpm pages:deploy). See docs/ai/ for architecture and contribution notes —
in particular, read docs/ai/ARCHITECTURE.md before changing how the two packages resolve
each other; the constraints there (why cross-package imports must go through node_modules
and not a source-pointing tsconfig path) aren't obvious and are easy to accidentally break.
Adding a primitive
- Create
packages/core/src/<name>/(low-level infrastructure) orpackages/primitives/src/<name>/(accessible UI pattern) following the existing pattern. - Export it from that package's
src/public-api.ts. - Register it in
cli/registry.js(name,layer, files,depsfor Core-internal siblings orpeerDeps: ['@quartz-headless/core']for a new Primitive). - Add a demo page at
src/app/pages/(docs)/<name>.page.ts, importing from@quartz-headless/coreor@quartz-headless/primitivesas appropriate.
🤝 Contributing
Issues and PRs are welcome. Run pnpm test, pnpm lint and pnpm typecheck before opening a
PR — the pre-commit hook and CI enforce all three.
