quartz-headless
v0.2.1
Published
Headless Angular 21 UI primitives — overlay, dialog, splitter, toast, drag-drop, tooltip, tree, listbox, virtual scroll, viewport
Maintainers
Readme
🪨 Quartz Headless
Unstyled, accessible Angular 21 UI primitives.
You own the styles — Quartz owns the behaviour.
🌐 Live docs & demos · 📦 npm · 🐛 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.
// Behaviour in, styling yours.
import { OverlayTriggerDirective } from 'quartz-headless';
@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 package
npm install quartz-headless # or: pnpm add quartz-headless// app.config.ts
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [provideZonelessChangeDetection()],
};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. Transitive dependencies are resolved automatically.
git clone https://github.com/Andersseen/quartz.git
cd quartz && pnpm install
pnpm quartz list # see everything available
pnpm quartz add overlay # add one primitive (+ its deps)
pnpm quartz add dialog splitter # add several at once
pnpm quartz add toast --output src/app/uiFiles land in your project's src/lib/components/<name>/ (auto-detected) or the --output path.
🧩 Primitives
| Primitive | What it gives you |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| overlay | Portal-based positioning for dropdowns, menus and popovers |
| dialog | Service-driven dialog & drawer with backdrop and focus trap |
| splitter | Resizable panels with keyboard, touch and ARIA slider semantics |
| toast | Notification system with position groups and auto-dismiss |
| drag-drop | Native HTML drag & drop with sortable drop zones |
| tooltip | Hover/focus tooltip with configurable placement |
| tree | Tree view with roving tabindex, WAI-ARIA keyboard nav & lazy per-level loading |
| listbox | Single/multi selection with WAI-ARIA keyboard navigation and type-ahead |
| virtual-scroll | Windowed rendering for long lists |
| viewport | Reactive breakpoint service + ViewportMatchDirective |
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';
@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 the library → dist/quartz/
pnpm test # unit tests (Vitest)
pnpm e2e # end-to-end tests (Playwright)
pnpm typecheck # type check lib + 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.
Adding a primitive
- Create
packages/quartz/src/lib/<name>/following the existing pattern. - Export it from
packages/quartz/src/public-api.ts. - Register it in
cli/registry.js(name, files, optionaldeps). - Add a demo page at
src/app/pages/(docs)/<name>.page.ts.
🤝 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.
