# Radix NG Primitives — Full Documentation
Unstyled, accessible, signals-first Angular primitives for building design systems and web apps.
Examples use Angular signals and Tailwind CSS v4 utilities; the primitives themselves are styling-agnostic.
---
# Accordion
#### A vertically stacked set of interactive headings that each reveal an associated section of content.
```html
Yes. It adheres to the WAI-ARIA design pattern.
Yes. It's unstyled by default, giving you freedom over the look and feel.
Yes! You can animate the Accordion with CSS or JavaScript.
```
## Features
- ✅ Full keyboard navigation, with optional focus looping (`loopFocus`).
- ✅ Supports horizontal/vertical orientation.
- ✅ Supports Right to Left direction.
- ✅ Expand a single item or multiple items at once.
- ✅ Can be controlled or uncontrolled.
- ✅ Emits an event per item when its panel opens or closes (`onOpenChange`).
- ✅ Can keep collapsed panels mounted in the DOM (`keepMounted`).
## Import
Get started with importing the directives:
```typescript
import {
RdxAccordionRootDirective,
RdxAccordionItemDirective,
RdxAccordionHeaderDirective,
RdxAccordionTriggerDirective,
RdxAccordionContentDirective
} from '@radix-ng/primitives/accordion';
```
## Anatomy
```html
```
## Examples
### Disabled
Disable the whole accordion via `disabled` on the root, or a single item via `disabled` on the item.
```typescript
import { Component } from '@angular/core';
import {
RdxAccordionContentDirective,
RdxAccordionHeaderDirective,
RdxAccordionItemDirective,
RdxAccordionRootDirective,
RdxAccordionTriggerDirective
} from '@radix-ng/primitives/accordion';
import { cn, demoAccordion } from '../../storybook/styles';
@Component({
selector: 'accordion-disabled-example',
imports: [
RdxAccordionRootDirective,
RdxAccordionItemDirective,
RdxAccordionHeaderDirective,
RdxAccordionTriggerDirective,
RdxAccordionContentDirective
],
template: `
Yes. It adheres to the WAI-ARIA design pattern.
Yes. It's unstyled by default, giving you freedom over the look and feel.
Yes! You can animate the Accordion with CSS or JavaScript.
`
})
export class AccordionDisabledExample {
protected readonly cn = cn;
protected readonly a = demoAccordion;
}
```
### Multiple
Set `multiple` (or `type="multiple"`) to let several items stay open at once.
```typescript
import { Component } from '@angular/core';
import {
RdxAccordionContentDirective,
RdxAccordionHeaderDirective,
RdxAccordionItemDirective,
RdxAccordionRootDirective,
RdxAccordionTriggerDirective
} from '@radix-ng/primitives/accordion';
import { cn, demoAccordion } from '../../storybook/styles';
@Component({
selector: 'accordion-multiple-example',
imports: [
RdxAccordionRootDirective,
RdxAccordionItemDirective,
RdxAccordionHeaderDirective,
RdxAccordionTriggerDirective,
RdxAccordionContentDirective
],
template: `
Yes. It adheres to the WAI-ARIA design pattern.
Yes. It's unstyled by default, giving you freedom over the look and feel.
Yes. It's unstyled by default, giving you freedom over the look and feel.
`
})
export class AccordionMultipleExample {
protected readonly cn = cn;
protected readonly a = demoAccordion;
}
```
### Collapsible
In single mode, `collapsible` lets the user close the currently open item.
```typescript
import { Component } from '@angular/core';
import {
RdxAccordionContentDirective,
RdxAccordionHeaderDirective,
RdxAccordionItemDirective,
RdxAccordionRootDirective,
RdxAccordionTriggerDirective
} from '@radix-ng/primitives/accordion';
import { cn, demoAccordion } from '../../storybook/styles';
@Component({
selector: 'accordion-collapsible-example',
imports: [
RdxAccordionRootDirective,
RdxAccordionItemDirective,
RdxAccordionHeaderDirective,
RdxAccordionTriggerDirective,
RdxAccordionContentDirective
],
template: `
Yes. It adheres to the WAI-ARIA design pattern.
Yes. It's unstyled by default, giving you freedom over the look and feel.
Yes! You can animate the Accordion with CSS or JavaScript.
`
})
export class AccordionCollapsibleExample {
protected readonly cn = cn;
protected readonly a = demoAccordion;
}
```
### Horizontal
Set `orientation="horizontal"` to lay items out in a row; arrow-key navigation follows the orientation.
```typescript
import { Component } from '@angular/core';
import {
RdxAccordionContentDirective,
RdxAccordionHeaderDirective,
RdxAccordionItemDirective,
RdxAccordionRootDirective,
RdxAccordionTriggerDirective
} from '@radix-ng/primitives/accordion';
import { cn, demoAccordion } from '../../storybook/styles';
@Component({
selector: 'accordion-horizontal-example',
imports: [
RdxAccordionRootDirective,
RdxAccordionItemDirective,
RdxAccordionHeaderDirective,
RdxAccordionTriggerDirective,
RdxAccordionContentDirective
],
template: `
Yes. It adheres to the WAI-ARIA design pattern.
Yes. It's unstyled by default, giving you freedom over the look and feel.
Yes! You can animate the Accordion with CSS or JavaScript.
`
})
export class AccordionHorizontalExample {
protected readonly cn = cn;
protected readonly a = demoAccordion;
}
```
### Events
Each item emits `onOpenChange` with its new open state whenever it expands or collapses.
```typescript
import { Component, signal } from '@angular/core';
import {
RdxAccordionContentDirective,
RdxAccordionHeaderDirective,
RdxAccordionItemDirective,
RdxAccordionRootDirective,
RdxAccordionTriggerDirective
} from '@radix-ng/primitives/accordion';
import { cn, demoAccordion } from '../../storybook/styles';
@Component({
selector: 'accordion-events-example',
imports: [
RdxAccordionRootDirective,
RdxAccordionItemDirective,
RdxAccordionHeaderDirective,
RdxAccordionTriggerDirective,
RdxAccordionContentDirective
],
template: `
Yes. It adheres to the WAI-ARIA design pattern.
Yes. It's unstyled by default.
Last event: {{ status() }}
`
})
export class AccordionEventsExample {
protected readonly cn = cn;
protected readonly a = demoAccordion;
readonly status = signal('—');
log(title: string, open: boolean): void {
this.status.set(`${title} ${open ? 'opened' : 'closed'}`);
}
}
```
### Keep mounted
With `keepMounted`, collapsed panels keep their element in the DOM instead of receiving a `hidden`
attribute — useful to preserve form state and keep content reachable by the browser's find-in-page.
```typescript
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
RdxAccordionContentDirective,
RdxAccordionHeaderDirective,
RdxAccordionItemDirective,
RdxAccordionRootDirective,
RdxAccordionTriggerDirective
} from '@radix-ng/primitives/accordion';
import { cn, demoAccordion, demoInput } from '../../storybook/styles';
/**
* With `keepMounted`, collapsed panels keep their element in the DOM (no `hidden`
* attribute) instead of being hidden from assistive tech and find-in-page. Type
* something below, collapse the panel, and reopen it — the value is retained and
* the field stays reachable by the browser's Ctrl/Cmd+F search.
*/
@Component({
selector: 'accordion-keep-mounted-example',
imports: [
FormsModule,
RdxAccordionRootDirective,
RdxAccordionItemDirective,
RdxAccordionHeaderDirective,
RdxAccordionTriggerDirective,
RdxAccordionContentDirective
],
template: `
Same as shipping.
`
})
export class AccordionKeepMountedExample {
protected readonly cn = cn;
protected readonly a = demoAccordion;
protected readonly input = demoInput;
address = '';
}
```
## Keyboard interactions
| Key | Description |
| -------------------------- | ----------------------------------------------------------------------------------- |
| `Space` / `Enter` | Toggles the focused item (in single non-collapsible mode an open item stays open). |
| `ArrowDown` / `ArrowRight` | Moves focus to the next trigger, wrapping to the first when `loopFocus` is enabled. |
| `ArrowUp` / `ArrowLeft` | Moves focus to the previous trigger, wrapping to the last when `loopFocus` is enabled. |
| `Home` | Moves focus to the first trigger. |
| `End` | Moves focus to the last trigger. |
Arrow keys follow `orientation`: Up/Down for vertical accordions, Left/Right for horizontal ones.
## Data attributes
State is exposed through `data-*` attributes for styling:
| Attribute | Parts | Values |
| ----------------- | ------------------------------ | ----------------------------------------------------------- |
| `data-state` | item, header, trigger, content | `"open"` \| `"closed"` |
| `data-disabled` | root, item, header, content | Present when disabled |
| `data-orientation`| root, item, header, trigger, content | `"horizontal"` \| `"vertical"` |
| `data-index` | item, header, trigger, content | Zero-based position of the item |
| `data-panel-open` | trigger | Present while the trigger's panel is open (e.g. rotate a chevron) |
## API Reference
### RdxAccordionRootDirective
### RdxAccordionItemDirective
### RdxAccordionTriggerDirective
### RdxAccordionContentDirective
---
# Alert Dialog
#### A modal dialog that interrupts the user with important content and expects a response.
Alert Dialog is the Dialog primitive with three fixed invariants: it is always modal, it does **not**
dismiss on outside clicks or focus leaving the popup (only an explicit action or Escape closes it), and
its popup uses `role="alertdialog"`. Each `rdxAlertDialog*` part is a thin wrapper around the matching
dialog part.
```typescript
import { Component } from '@angular/core';
import { alertDialogImports } from '@radix-ng/primitives/alert-dialog';
import { cn, demoButton, demoDialog } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-default',
imports: [...alertDialogImports],
template: `
Are you absolutely sure?
This action cannot be undone. This will permanently delete your account and remove your data
from our servers.
`
})
export class RdxAlertDialogDefaultComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
}
```
## Features
- ✅ Always modal: document scrolling is locked and outside pointer events are disabled.
- ✅ Does not close on outside interaction — requires an explicit choice; Escape still closes.
- ✅ Renders the popup with `role="alertdialog"` for assertive screen-reader semantics.
- ✅ Supports uncontrolled state, `defaultOpen`, and Angular two-way binding with `[(open)]`.
- ✅ Supports multiple triggers, controlled `triggerId`, and detached triggers through a shared handle.
- ✅ Traps and restores focus; links the popup to title and description for accessible labeling.
## Import
```typescript
import {
alertDialogImports,
createRdxAlertDialogHandle,
RdxAlertDialogBackdrop,
RdxAlertDialogClose,
RdxAlertDialogDescription,
RdxAlertDialogPopup,
RdxAlertDialogPortal,
RdxAlertDialogPortalPresence,
RdxAlertDialogRoot,
RdxAlertDialogTitle,
RdxAlertDialogTrigger
} from '@radix-ng/primitives/alert-dialog';
```
Or import all parts through the module:
```typescript
import { RdxAlertDialogModule } from '@radix-ng/primitives/alert-dialog';
```
## Anatomy
```html
Are you sure?
This action cannot be undone.
```
## Examples
### Default
A destructive confirmation with Cancel and an action button. Clicking the backdrop keeps it open.
```typescript
import { Component } from '@angular/core';
import { alertDialogImports } from '@radix-ng/primitives/alert-dialog';
import { cn, demoButton, demoDialog } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-default',
imports: [...alertDialogImports],
template: `
Are you absolutely sure?
This action cannot be undone. This will permanently delete your account and remove your data
from our servers.
`
})
export class RdxAlertDialogDefaultComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
}
```
### Controlled
Bind `[(open)]` to open or close the alert dialog from component state.
```typescript
import { Component, signal } from '@angular/core';
import { alertDialogImports } from '@radix-ng/primitives/alert-dialog';
import { cn, demoButton, demoDialog } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-controlled',
imports: [...alertDialogImports],
template: `
Alert dialog is {{ open() ? 'open' : 'closed' }}
Discard unsaved changes?
The open state is owned by the component and bound with
[(open)]
.
`
})
export class RdxAlertDialogControlledComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
protected readonly open = signal(false);
}
```
### Multiple triggers
Several triggers can open the same alert dialog. The active trigger's `payload` is exposed on the root
so the content can adapt to what is being confirmed.
```typescript
import { Component } from '@angular/core';
import { alertDialogImports } from '@radix-ng/primitives/alert-dialog';
import { cn, demoButton, demoDialog } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-multiple-triggers',
imports: [...alertDialogImports],
template: `
Delete this {{ root.payload() || 'item' }}?
Every trigger opens the same alert dialog; the active trigger's
payload
decides what is being deleted.
`
})
export class RdxAlertDialogMultipleTriggersComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
}
```
### Controlled mode with multiple triggers
Bind both `[(open)]` and `[(triggerId)]` to choose which trigger is active from component state.
```typescript
import { Component, signal } from '@angular/core';
import { alertDialogImports } from '@radix-ng/primitives/alert-dialog';
import { cn, demoButton, demoDialog } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-controlled-multiple',
imports: [...alertDialogImports],
template: `
Both
open
and
triggerId
are bound, so the active action is driven from component state.
`
})
export class RdxAlertDialogControlledMultipleComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
protected readonly open = signal(false);
protected readonly triggerId = signal(null);
protected confirmDelete() {
this.triggerId.set('delete');
this.open.set(true);
}
}
```
### Detached triggers
Connect a trigger rendered outside the root with `createRdxAlertDialogHandle()`; the handle also exposes
imperative `open(id)` / `close()`.
```typescript
import { Component } from '@angular/core';
import { alertDialogImports, createRdxAlertDialogHandle } from '@radix-ng/primitives/alert-dialog';
import { cn, demoButton, demoDialog } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-detached',
imports: [...alertDialogImports],
template: `
Delete this file?
The trigger and this alert dialog are connected with
createRdxAlertDialogHandle()
.
`
})
export class RdxAlertDialogDetachedComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
protected readonly handle = createRdxAlertDialogHandle();
}
```
### Close confirmation
A regular Dialog editor asks an Alert Dialog to confirm before discarding unsaved changes — the classic
reason to reach for an alert dialog.
```typescript
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { alertDialogImports } from '@radix-ng/primitives/alert-dialog';
import { dialogImports, RdxDialogOpenChange } from '@radix-ng/primitives/dialog';
import { cn, demoButton, demoDialog, demoInput } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-close-confirmation',
imports: [...dialogImports, ...alertDialogImports, FormsModule],
template: `
Edit note
Closing with unsaved changes asks an alert dialog to confirm.
Discard changes?
Your unsaved note will be lost.
`
})
export class RdxAlertDialogCloseConfirmationComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
protected readonly input = demoInput;
protected readonly editorOpen = signal(false);
protected readonly confirmOpen = signal(false);
protected text = '';
private get hasChanges() {
return this.text.trim().length > 0;
}
protected onEditorOpenChange(change: RdxDialogOpenChange) {
// Re-open the editor and ask for confirmation when there are unsaved changes.
if (!change.open && this.hasChanges) {
this.editorOpen.set(true);
this.confirmOpen.set(true);
}
}
protected requestClose() {
if (this.hasChanges) {
this.confirmOpen.set(true);
} else {
this.editorOpen.set(false);
}
}
protected save() {
this.text = '';
this.editorOpen.set(false);
}
protected discard() {
this.text = '';
this.confirmOpen.set(false);
this.editorOpen.set(false);
}
}
```
### Open from a menu
Control the alert dialog's `open` state from a menu item to launch a destructive confirmation from a `Menu`.
```typescript
import { Component, signal } from '@angular/core';
import { alertDialogImports } from '@radix-ng/primitives/alert-dialog';
import { RdxMenuModule } from '@radix-ng/primitives/menu';
import { cn, demoButton, demoDialog, demoMenu } from '../../storybook/styles';
@Component({
selector: 'rdx-alert-dialog-from-menu',
imports: [...alertDialogImports, RdxMenuModule],
template: `
@if (menu.open()) {
}
Delete project?
This permanently deletes the project. Opened by controlling the alert dialog from a menu
item.
`
})
export class RdxAlertDialogFromMenuComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
protected readonly d = demoDialog;
protected readonly m = demoMenu;
protected readonly deleteOpen = signal(false);
}
```
## API Reference
### Root
`RdxAlertDialogRoot` composes the Dialog root and forces alert semantics (always modal, no pointer
dismissal, `role="alertdialog"`).
### Trigger
`RdxAlertDialogTrigger` opens the alert dialog; it behaves like the dialog trigger.
### Portal, Backdrop, Popup, Title, Description, Close, Viewport
These are thin wrappers around the matching dialog parts and read their behavior from context. See the
Dialog docs for their full reference.
---
# Aspect Ratio
#### Displays content within a desired ratio.
```html
```
---
# Avatar
#### An image element with a fallback for representing the user.
```html
```
## Features
- ✅ Automatic and manual control over when the image renders.
- ✅ Fallback part accepts any children.
- ✅ Optionally delay fallback rendering to avoid content flashing.
## Import
```typescript
import {
RdxAvatarRootDirective,
RdxAvatarImageDirective,
RdxAvatarFallbackDirective
} from '@radix-ng/primitives/avatar';
```
## Anatomy
Assemble the parts: a root, the image, and a fallback shown until the image loads.
```html
AB
```
## Global configuration
Configure the default options for all avatars with `provideRdxAvatarConfig` in a providers array.
```ts
import { provideRdxAvatarConfig } from '@radix-ng/primitives/avatar';
bootstrapApplication(AppComponent, {
providers: [provideRdxAvatarConfig({ delayMs: 1000 })]
});
```
## Examples
### Sizes
`sm`, `md`, and `lg` from the demo style layer.
```html
```
### Fallback
The fallback renders when there is no image or the image fails to load.
```html
```
### Delayed fallback
`delayMs` waits before showing the fallback, so it only appears for slower connections.
```html
```
## API Reference
### Image
### Fallback
---
# Button
#### A button, or any element that should behave like one.
Headless button behavior modeled on [Base UI](https://base-ui.com/)'s `useButton`. It carries no
styles — visual variants in the examples come from the centralized demo style layer (see the
**Guides/Styling** page).
```typescript
import { Component } from '@angular/core';
import { cn, demoButton } from '../../storybook/styles';
import { RdxButtonDirective } from '../src/button.directive';
/**
* Visual variants — the recommended `demoButton` styling from the centralized
* style layer applied on top of the headless `rdxButton` directive.
*/
@Component({
selector: 'rdx-button-variants',
imports: [RdxButtonDirective],
template: `
`
})
export class RdxButtonVariantsComponent {
protected readonly cn = cn;
protected readonly b = demoButton;
}
```
## Features
- ✅ Works on a native `