@ravindrasingh040690/ui
v0.1.0
Published
Accessible, standalone Angular UI components with a CSS custom-property theme system.
Maintainers
Readme
@ravindrasingh040690/ui
Accessible, standalone Angular components with a CSS custom-property theme system.
- Angular 22, standalone components, signal inputs and
OnPusheverywhere - No runtime dependencies beyond
@angular/core,@angular/commonand@angular/forms - One token file drives every colour, radius, shadow and spacing value
- Light and dark themes with no per-component overrides
- Tree-shakeable, with the theme stylesheet explicitly preserved
Installation
npm install @ravindrasingh040690/uiRequires Angular ^22.0.0.
Setup
Add the theme stylesheet to your build so the --ui-* custom properties exist:
// angular.json
{
"styles": ["node_modules/@ravindrasingh040690/ui/styles/ui-theme.css", "src/styles.scss"],
}Because the components fall back to build-time defaults, they render acceptably even before this step — but colours and radii come from the tokens, so do not skip it.
Then import only what you use. Components are standalone, so there is no module to install:
import { Component } from '@angular/core';
import { UiButton, UiCard, UiBadge } from '@ravindrasingh040690/ui';
@Component({
selector: 'app-pricing',
imports: [UiButton, UiCard, UiBadge],
template: `
<ui-card title="Team" subtitle="For growing teams">
<ui-badge uiCardActions variant="success" [dot]="true">Live</ui-badge>
<p>Unlimited projects and SSO.</p>
<div uiCardFooter>
<ui-button variant="primary">Upgrade</ui-button>
</div>
</ui-card>
`,
})
export class PricingComponent {}Components
<ui-button>
Wraps a native <button>, so keyboard, focus and form semantics stay intact. Native (click)
works as usual and does not fire while the button is disabled or loading.
| Input | Type | Default | Notes |
| ----------- | -------------------------------------------------------------- | ----------- | ------------------------------------------------------ |
| variant | 'primary' \| 'secondary' \| 'outline' \| 'ghost' \| 'danger' | 'primary' | |
| size | 'sm' \| 'md' \| 'lg' | 'md' | |
| type | 'button' \| 'submit' \| 'reset' | 'button' | |
| disabled | boolean | false | |
| loading | boolean | false | Shows a spinner, disables the button, sets aria-busy |
| block | boolean | false | Full width |
| ariaLabel | string \| null | null | Use for icon-only buttons |
<ui-button variant="primary" (click)="save()">Save</ui-button>
<ui-button variant="danger" [loading]="deleting()">Delete</ui-button>
<ui-button variant="outline" size="sm" ariaLabel="Close panel">✕</ui-button><ui-input>
Implements ControlValueAccessor, so [formControl], formControlName, [(ngModel)] and
ngModel all work without a wrapper.
| Input | Type | Default | Notes |
| -------------- | --------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------- |
| label | string | '' | Rendered as a real <label for> |
| type | 'text' \| 'email' \| 'number' \| 'password' \| 'search' \| 'tel' \| 'url' | 'text' | |
| placeholder | string | '' | |
| hint | string | '' | Shown below the field while there is no error |
| error | string | '' | Presence marks the field invalid and is announced via role="alert" |
| size | 'sm' \| 'md' \| 'lg' | 'md' | |
| autocomplete | string \| null | null | |
| inputmode | string \| null | null | |
| id | string | auto | Override the generated control id |
| required | boolean | false | Sets the native attribute and renders an asterisk |
| disabled | boolean | false | Merged with the forms API's disabled state |
| readonly | boolean | false | Declared as readOnly, exposed under the name readonly |
| clearable | boolean | false | Adds a button that resets the value to '' |
| clearLabel | string | 'Clear' | Accessible name for the clear button |
Slots: [uiPrefix] and [uiSuffix] for affixes. They are projected verbatim, so style them from
your own component stylesheet.
<ui-input label="Email" type="email" [formControl]="email" [error]="emailError()" />
<ui-input label="Amount" size="sm" hint="Charged monthly">
<span uiPrefix>$</span>
<span uiSuffix>USD</span>
</ui-input>Values are always strings, including for type="number" — this matches how ControlValueAccessor
behaves for any other Angular input control.
<ui-card>
| Input | Type | Default | Notes |
| ------------- | -------------------------------------- | ------------ | ---------------------------------------------- |
| variant | 'elevated' \| 'outlined' \| 'filled' | 'elevated' | |
| padding | 'none' \| 'sm' \| 'md' \| 'lg' | 'md' | Applied to header, body and footer alike |
| interactive | boolean | false | Hover elevation plus a tabindex="0" tab stop |
| title | string | '' | Its presence enables the header row |
| subtitle | string | '' | |
Slots: the default slot is the body, [uiCardActions] lands in the header, [uiCardFooter] becomes
a separated footer. Both attribute slots are hidden automatically when nothing is projected.
The header only renders when
titleorsubtitleis set, so[uiCardActions]needs one of them.
<ui-card title="Starter" subtitle="For side projects">
<ui-button uiCardActions variant="ghost" size="sm">Edit</ui-button>
<p>One project and one collaborator.</p>
<div uiCardFooter>
<ui-button size="sm">Choose Starter</ui-button>
</div>
</ui-card><ui-modal>
Built on the native <dialog> element, so focus trapping, inert background content, top-layer
stacking and Esc handling all come from the platform.
| Input | Type | Default | Notes |
| ----------------- | ------------------------------ | --------- | -------------------------------- |
| open | two-way model<boolean> | false | [(open)]="isOpen" |
| title | string | '' | Also used as the accessible name |
| size | 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | |
| closeOnBackdrop | boolean | true | |
| closeOnEscape | boolean | true | |
| showClose | boolean | true | |
| ariaLabel | string \| null | null | Used when there is no title |
| closeLabel | string | 'Close' | |
| Output | Payload | When |
| ----------- | ----------------------------------- | ---------------------------------------------------- |
| dismissed | 'close' \| 'backdrop' \| 'escape' | Only when the user closed the dialog |
| closed | – | Whenever the dialog finishes closing, for any reason |
Method: close() closes the dialog programmatically without reporting a dismissal.
Slots: the default slot is the scrollable body, [uiModalFooter] becomes the action row.
<ui-modal [(open)]="confirmOpen" title="Delete project" size="sm" (dismissed)="log($event)">
<p>This cannot be undone.</p>
<div uiModalFooter>
<ui-button variant="ghost" (click)="confirmOpen.set(false)">Cancel</ui-button>
<ui-button variant="danger" (click)="remove()">Delete</ui-button>
</div>
</ui-modal><ui-alert>
| Input | Type | Default | Notes |
| ------------- | ---------------------------------------------- | ----------- | ----- |
| variant | 'info' \| 'success' \| 'warning' \| 'danger' | 'info' | |
| appearance | 'soft' \| 'solid' \| 'outline' | 'soft' | |
| title | string | '' | |
| dismissible | boolean | false | |
| icon | boolean | true | |
| closeLabel | string | 'Dismiss' | |
Output: dismissed — emitted once, when the user dismisses it.
danger and warning render with role="alert"; info and success use the polite role="status".
<ui-alert variant="success" title="Deployed">Build #482 is live.</ui-alert>
<ui-alert variant="danger" title="Payment failed" [dismissible]="true" (dismissed)="track()">
Update your billing details.
</ui-alert><ui-badge>
| Input | Type | Default | Notes |
| ------------ | ------------------------------------------------------------------------ | ----------- | ------------------------------ |
| variant | 'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger' | 'neutral' | |
| size | 'sm' \| 'md' \| 'lg' | 'md' | |
| appearance | 'soft' \| 'solid' \| 'outline' | 'soft' | |
| pill | boolean | false | Fully rounded |
| dot | boolean | false | Prepends a coloured status dot |
<ui-badge variant="success" [dot]="true">Live</ui-badge>
<ui-badge variant="warning" appearance="outline">Degraded</ui-badge>
<ui-badge variant="primary" size="lg" [pill]="true">v0.1.0</ui-badge><ui-spinner>
| Input | Type | Default | Notes |
| ---------- | -------------------------------------- | ----------- | ------------------------------------------------ |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | |
| tone | 'primary' \| 'neutral' \| 'inherit' | 'primary' | inherit picks up the surrounding color |
| label | string | 'Loading' | Announced to assistive technology, never painted |
| overlay | boolean | false | Covers the nearest positioned ancestor |
| backdrop | boolean | true | Dims the backdrop in overlay mode |
<ui-spinner />
<ui-spinner size="lg" tone="neutral" label="Fetching members" />
<div class="panel">
<p>Content that is loading…</p>
@if (loading()) {
<ui-spinner [overlay]="true" />
}
</div>Theming
Every token is emitted as a --ui-* custom property on :root. Override them anywhere — no build
step, no Sass, no ::ng-deep:
:root {
--ui-color-primary: #0ea5e9;
--ui-radius-md: 0px;
--ui-font-family: 'Inter', system-ui, sans-serif;
}Because the same values are compiled into the components as fallbacks, a token you have not overridden still resolves correctly.
Dark mode
<!-- Always dark -->
<html data-ui-theme="dark">
<!-- Follow the operating system -->
<html data-ui-theme="auto"></html>
</html>[data-ui-theme="dark"] can be applied to any element, not just <html>, which makes scoped themes
possible.
Overriding at compile time
If you would rather not ship a second stylesheet, consume the Sass source directly and let the components inherit your values:
// angular.json — so that `@use 'ui-theme'` resolves
"stylePreprocessorOptions": {
"includePaths": ["node_modules/@ravindrasingh040690/ui/styles"]
}// styles.scss
@use '@ravindrasingh040690/ui/styles/tokens' as ui with (
$defaults: map.merge(
ui.$defaults,
(
'color-primary': #0ea5e9,
)
)
);
@use 'ui-theme';Accessibility notes
- The modal uses
<dialog>+showModal(), so focus is trapped and background content is inert by the browser, not by JavaScript. - Focus rings use
:focus-visibleonly, so pointer users never see them. prefers-reduced-motion: reducedisables the animations and transitions the library ships.- Alerts select
role="alert"only fordangerandwarning; the rest announce politely.
License
MIT
