Signals-first Angular primitives for building scalable, accessible UI systems.
Most Angular component libraries hand you finished UI. @snatuva/primitives hands you the foundations — unstyled, composable, and accessible building blocks you wire together your way.
No NgModule. No fighting the design system. Just clean primitives built on Angular Signals.
npm install @snatuva/primitivesRequires Angular 17+.
| Traditional component libraries | @snatuva/primitives |
|---|---|
| Opinionated styles, hard to override | Zero styles — you own the look |
| Bulky bundles with unused components | Tree-shakable, import only what you use |
| RxJS-heavy internal state | Angular Signals throughout |
| NgModule-based | Standalone-first |
A fully accessible tabs implementation following the ARIA tabs pattern.
Directives
| Directive | Description | Inputs |
|---|---|---|
apTabs |
Root scope and state provider | — |
apTabList |
Container for tab triggers | orientation?: 'horizontal' | 'vertical' (default: 'horizontal') |
apTabTrigger |
Interactive tab button | tabId: string, disabled?: boolean |
apTabPanel |
Panel region | id: string, disabled?: boolean |
apTabContent |
Structural directive — conditionally renders active panel | tabId: string |
Import
import {
TabsDirective,
TabListDirective,
TabTriggerDirective,
TabPanelDirective,
TabContentDirective,
} from '@snatuva/primitives';Usage
<div apTabs>
<div apTabList>
<button apTabTrigger tabId="overview">Overview</button>
<button apTabTrigger tabId="details">Details</button>
<button apTabTrigger tabId="settings" [disabled]="true">Settings</button>
</div>
<section apTabPanel id="overview">
<p>Overview content</p>
</section>
<section apTabPanel id="details">
<p>Details content</p>
</section>
<section apTabPanel id="settings">
<p>Settings content</p>
</section>
</div>ARIA attributes (role="tab", aria-selected, aria-controls, role="tabpanel", aria-labelledby, aria-hidden) are applied automatically.
Conditional rendering with apTabContent
Use apTabContent to defer rendering panel content until the tab is active:
<section apTabPanel id="analytics">
<ng-template apTabContent tabId="analytics">
<!-- Only rendered when this panel is active -->
<app-analytics-chart />
</ng-template>
</section>A vertically stacked set of interactive headings that each reveal an associated section of content.
Directives
| Directive | Description | Inputs |
|---|---|---|
apAccordion |
Root scope and state provider | type?: 'single' | 'multiple'collapsible?: booleanorientation?: 'vertical' | 'horizontal'disabled?: booleandefaultValue?: string | string[]value?: string | string[] |
apAccordionItem |
Container for a single accordion item | itemId: string, disabled?: boolean |
apAccordionTrigger |
Interactive toggle button | — |
apAccordionContent |
Collapsible panel region | — |
Import
import {
AccordionDirective,
AccordionItemDirective,
AccordionTriggerDirective,
AccordionContentDirective,
} from '@snatuva/primitives';Usage
<div apAccordion type="single" [collapsible]="true">
<div apAccordionItem itemId="item-1">
<button apAccordionTrigger>Is it accessible?</button>
<div apAccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</div>
</div>
<div apAccordionItem itemId="item-2">
<button apAccordionTrigger>Is it unstyled?</button>
<div apAccordionContent>Yes. It's fully headless and unstyled.</div>
</div>
</div>Keyboard navigation (Arrow keys, Home, End) and ARIA attributes (aria-expanded, aria-controls, role="region") are handled internally.
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
Directives
| Directive | Description | Inputs / Outputs |
|---|---|---|
apDialog |
Root state provider | open?: boolean(openChange)modal?: boolean (default: true)closeOnBackdropClick?: boolean (default: true) |
apDialogTrigger |
Button to open the dialog | — |
apDialogOverlay |
Backdrop element | — |
apDialogContent |
Modal content container | — |
apDialogTitle |
ARIA title (aria-labelledby) |
— |
apDialogDescription |
ARIA description (aria-describedby) |
— |
apDialogClose |
Button to close the dialog | — |
Import
import {
DialogDirective,
DialogTriggerDirective,
DialogOverlayDirective,
DialogContentDirective,
DialogTitleDirective,
DialogDescriptionDirective,
DialogCloseDirective,
} from '@snatuva/primitives';Usage
<div apDialog>
<button apDialogTrigger>Edit Profile</button>
<div apDialogOverlay class="overlay-styles">
<div apDialogContent class="panel-styles">
<h2 apDialogTitle>Edit Profile</h2>
<p apDialogDescription>Make changes to your profile here.</p>
<!-- Your form content -->
<button apDialogClose>Save Changes</button>
</div>
</div>
</div>Focus trapping, background scroll locking, and Escape key functionality are built-in.
A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
Directives
| Directive | Description | Inputs |
|---|---|---|
apTooltip |
Root state provider | — |
apTooltipTrigger |
Element that triggers the tooltip | — |
apTooltipContent |
Structural template for tooltip content | tooltipId?: string |
Import
import {
TooltipDirective,
TooltipTriggerDirective,
TooltipContentDirective,
} from '@snatuva/primitives';Usage
<div apTooltip>
<button apTooltipTrigger>
Hover or focus me
</button>
<ng-template apTooltipContent tooltipId="custom-tooltip-id">
<div class="tooltip-panel">
This is a helpful tooltip message!
</div>
</ng-template>
</div>Tooltips automatically map aria-describedby and attach via Angular CDK Overlay dynamically.
Every primitive is built to satisfy ARIA authoring practices out of the box:
- Semantic roles applied automatically (
role="tab",role="region",role="dialog",role="tooltip", etc.) - Keyboard navigation (Arrow keys, Home, End, Tab, Escape)
- ARIA states (
aria-selected,aria-expanded,aria-hidden, etc.) wired up seamlessly - Focus management and trapping handled internally for overlays like Dialog
- Disabled states respected by both keyboard and assistive technology
Signals-first. State is managed with Angular Signals. RxJS is used only where it is the right tool, not the default.
Standalone. No NgModule required. Drop a directive into any standalone component.
Headless. Primitives ship with zero styles. You apply your design system on top — CSS, Tailwind, or anything else.
Composable. Primitives are scoped and self-contained. Nest them, extend them, or combine them without fighting internal abstractions.
Tree-shakable. Only the primitives you import end up in your bundle.
- Tabs
- Accordion
- Dialog / Modal
- Tooltip
- Popover
- Select
- Checkbox & Radio group
- Toggle / Switch
- Accessibility utilities
- CDK integrations
- Documentation site
Contributions are welcome. If you have ideas for new primitives, accessibility improvements, or API refinements, open an issue or pull request.
Built by Siva Sridhar Natuva.
MIT