@paulelyson/elyui
v0.9.1
Published
Developer-friendly Angular UI components. Minimalist, themeable with plain CSS variables.
Maintainers
Readme
elyui
Developer-friendly UI components for Angular.
→ Live demo — every component, every variant, every size, on one page.
Imagine building an autocomplete with a form control
In Angular:
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatAutocompleteModule,
AsyncPipe,
]export class Example implements OnInit {
equipment = new FormControl<Option | string>('');
options: Option[] = [
{ value: 'monitor', label: 'Monitor' },
{ value: 'keyboard', label: 'Keyboard' },
{ value: 'dock', label: 'Docking station' },
];
filtered!: Observable<Option[]>;
ngOnInit(): void {
this.filtered = this.equipment.valueChanges.pipe(
startWith(''),
map((value) => (typeof value === 'string' ? value : (value?.label ?? ''))),
map((label) =>
this.options.filter((option) =>
option.label.toLowerCase().includes(label.toLowerCase()),
),
),
);
}
displayFn(option: Option): string {
return option?.label ?? '';
}
}<mat-form-field>
<mat-label>Equipment</mat-label>
<input matInput [formControl]="equipment" [matAutocomplete]="auto" />
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" autoActiveFirstOption>
@for (option of filtered | async; track option.value) {
<mat-option [value]="option">{{ option.label }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>In elyui:
imports: [ReactiveFormsModule, Autocomplete]export class Example {
equipment = new FormControl('');
options = ['Monitor', 'Keyboard', 'Docking station'];
}<ely-autocomplete label="Equipment" [options]="options" [formControl]="equipment" />Same filtering, same keyboard navigation, same ARIA roles. No filter pipe, no displayWith, no
wrapper element. options takes a plain string[] or { value, label } objects, and the control
holds the selected value.
Or three buttons with icons and appearances
In Angular:
<button matButton="filled">
<mat-icon>save</mat-icon>
Save
</button>
<button matButton="outlined" class="btn-danger btn-sm">
<mat-icon>delete</mat-icon>
Delete
</button>
<button matIconButton class="btn-accent">
<mat-icon>bookmark</mat-icon>
</button>Material has no size scale and no danger or accent semantic, so those two are your CSS to write
— per state, forever:
.btn-danger {
--mdc-outlined-button-label-text-color: #b91c1c;
--mdc-outlined-button-outline-color: #b91c1c;
}
.btn-danger:hover { background: #fef2f2; }
.btn-sm {
--mdc-outlined-button-container-height: 32px;
font-size: 13px;
padding: 0 10px;
}
.btn-accent { --mdc-icon-button-icon-color: #7c3aed; }In elyui:
<ely-button appearance="filled" variant="primary" icon="save">Save</ely-button>
<ely-button appearance="outlined" variant="danger" size="xs" icon="delete">Delete</ely-button>
<ely-button appearance="icon" variant="accent" icon="bookmark" />No stylesheet. appearance is filled · tonal · outlined · ghost · link · icon, variant is
neutral · primary · accent · success · warning · danger, size is xs · sm · md · lg.
Install
npm install @paulelyson/elyui @angular/material @angular/cdk@angular/core, @angular/common, @angular/forms, and rxjs are also peer dependencies, but you
already have those in any Angular app.
Requires Angular 21+.
Setup
1. Import the theme. Add it to your angular.json styles array (or @import it from your own
global stylesheet). This defines every CSS variable the components render with — without it,
components render unstyled:
"styles": [
"node_modules/@paulelyson/elyui/styles/elyui.css",
"src/styles.css"
]2. Load the Material Icons font. Icon — and anything that renders one internally, which is
most things — uses ligature icon names and expects this font in your index.html:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />That's it — there's no third step. You do not need provideAnimationsAsync() or the
@angular/animations package.
Usage
Components are standalone — import the ones you use, no NgModule:
import { Component } from '@angular/core';
import { Button, Badge, Icon } from '@paulelyson/elyui';
@Component({
selector: 'app-example',
imports: [Button, Badge, Icon],
template: `
<ely-button appearance="filled" variant="primary" icon="save">Save</ely-button>
<ely-badge variant="success">Active</ely-badge>
<ely-icon name="star" size="lg" variant="warning" tooltip="favorite" />
`,
})
export class Example {}Snackbar is opened imperatively through its service — no component to import, no template markup:
import { SnackbarService } from '@paulelyson/elyui';
export class Example {
constructor(private snackbar: SnackbarService) {}
save(): void {
this.snackbar.openSnackbar({ type: 'success', message: ['Saved.'], icon: 'check' });
}
}Theming
Every size- and color-dependent value is a CSS variable — no Sass, no theming API, no rebuild.
Override any of them after importing elyui.css:
:root {
--color-bg-primary-idle: #d1fae5;
--color-text-primary-default: #047857;
--border-radius-md: 6px;
}The full token list is short and readable — skim styles/elyui.css.
Components
Icon · Badge · Button · Toggle · Snackbar (+ SnackbarService) · SegmentedControl ·
TextInput (ely-input) · Textarea · Dropdown · Autocomplete · Datepicker ·
DateRangePicker · DataRow · FileInput · Tab · Avatar · VerticalStepper · TitleSection ·
Tooltip · Menu · Navbar
Tooltip is a directive rather than a component, so it goes on any element — elyui's own or plain
HTML:
<button elyTooltip="Save this draft">Save</button>
<ely-icon name="info" elyTooltip="Optional field" tooltipPosition="right" tooltipVariant="accent" />It takes tooltipPosition (above · below · left · right, flipping automatically when the
preferred side will not fit), tooltipSize, tooltipVariant, tooltipShowDelay,
tooltipHideDelay, tooltipHasArrow, tooltipHasBorder, and tooltipDisabled. An empty string
shows nothing, so binding a value that may be blank is safe.
Menu is a dropdown of actions on the CDK menu, so outside-click and Escape to close, focus
return, arrow-key navigation and the ARIA roles come built in. Items are data; whatever you project
becomes the trigger, and elyMenuHeader elements become a header in the panel:
<ely-menu icon="more_vert" triggerLabel="More actions" [items]="actions" (itemSelected)="onAction($event)" />
<ely-menu [items]="accountActions">
<ely-avatar label="Kace Cee" [showLabel]="true" />
<strong elyMenuHeader>Kace Cee</strong>
</ely-menu>A MenuItem is { value, label, icon?, variant?, disabled?, dividerBefore? }, and
(itemSelected) emits the whole item. It also takes position (below-start · below-end ·
above-start · above-end, flipping when it will not fit), size, disabled, hasBorder and
hasFocusGlow, and emits (opened) and (closed). The trigger is already a <button>, so project
non-interactive content into it — never another button.
The panel's colours are --color-bg-menu, --color-bg-menu-hover and --color-border-menu (the
last also draws the header rule and dividers). The panel renders in an overlay on document.body,
outside the ely-menu element, so set them on :root in a global stylesheet — on the host they
never reach it.
Navbar is a responsive top bar: a brand link, router links, and an end slot for whatever you
project. The current page's link is underlined and gets aria-current="page"; below collapseAt
(768px by default) the links fold into an ely-menu hamburger while the end slot stays put:
<ely-navbar brand="Inventory" brandLink="/" [links]="links">
<ely-button>Log in</ely-button>
</ely-navbar>A NavLink is { label, route, exact? }. It also takes brandImage (a decorative logo before the
brand text — brand stays the link's accessible name), transparent, label (the <nav>
landmark's name, default Main), size, variant, hasBorder and hasFocusGlow. Height, inline
padding and link spacing are per-app tokens rather than inputs —
ely-navbar { --navbar-height: 75px; } — and --color-bg-navbar sets its background.
Navbar requires @angular/router (a peer dependency from 0.9.0) and a provideRouter() in your
app config.
Most accept size (xs · sm · md · lg), variant (neutral · primary · accent · success ·
warning · danger), and hasBorder for a seamless borderless look. Nothing is forced on you — don't
want the focus glow? [hasFocusGlow]="false". The library has opinions, not rules.
Status
Pre-1.0. The API may still shift as more components land. Pin the version if that bothers you.
License
MIT.
