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 {}| π¨ 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. |
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.
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) 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) depend on @quartz-headless/core as a real npm
package instead of copied source β pnpm quartz add dialog copies only dialog/ and tells
you to npm install @quartz-headless/core.
| 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 |
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 |
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.
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' });
}
}| Dependency | Version |
|---|---|
@angular/core |
^21.0.0 (zoneless) |
@angular/common |
^21.0.0 |
| Node.js | >= 20 |
No runtime dependencies beyond @angular/*, rxjs and tslib.
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.
- 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.
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.
MIT Β© Andersseen