@gravionlabs/helix-ui
Vanilla Angular components on the Helix design tokens: standalone, signal-based, plain CSS, no runtime styling engine. It replaced the vendored PrimeNG fork @gravionlabs/helix-core (ADR 0001, ADR 0002).
Install and styles
npm install @gravionlabs/helix-ui/* the components, in the `components` cascade layer */
@import "@gravionlabs/helix-ui/styles.css";
/* the tokens the components read; load them in every app */
@import "@gravionlabs/helix-ui/tokens.css";styles.cssstates the layer order Tailwind uses (theme, base, components, utilities) and puts the component CSS incomponents: an app's unlayered CSS and Tailwind's utilities win over it.core.cssis the smaller alternative tostyles.css, see Component-only CSS.tokens.cssis the token data ofprojects/tokensresolved at build time: light on:root, dark under theapp-darkclass, every token the components read. Load it in every app. The tokens reference the semantic ones (--h-primary-color, …), so a runtime change of the primary colour or the dark mode (provideHxTheme) reaches every component.- Naming: components are elements (
<hx-select>), simple controls are attributes on the native element (<button hx-button>,<input hx-input>), always kebab-case with the prefixhx; classes use it too. The custom properties keep the prefix--h-.
Component-only CSS
@gravionlabs/helix-ui/core.css has the same header (layer order statement) and the same single components layer as styles.css, but only 36 stylesheets. Load either styles.css or core.css, plus tokens.css.
Output of pnpm build:lib:css:ui | Raw size |
|---|---|
styles.css (60 components) | 266.1 kB |
core.css (36 stylesheets, 48 %) | 128.3 kB |
tokens.css | 129.0 kB |
For comparison, @gravionlabs/helix-shell/styles.css is about 47 kB and primeicons.css about 19 kB, all raw. tokens.css stays whole in both cases, so it is the larger share of a component-only setup.
- In
core.css: accordion, avatar, badge, button, button-group, card, checkbox, chip, confirm, dialog, divider, drawer, fieldset, float-label, icon-field, input, input-group, input-number, listbox, menu, message, panel, password, popover, progress, radio, select, skeleton, split-button, switch, tabs, tag, toast, toolbar, tooltip, and the CDK overlay styles. - Only in
styles.css: chart; galleria, carousel, image; table, data-table, tree, timeline, paginator; date-picker, time-picker, date-time-picker, date-time-range-picker, date-range-quick-select; menubar, breadcrumb, stepper; auto-complete, multi-select, file-upload, rating, slider, select-button, toggle-button.
A component outside the list renders unstyled with core.css; use styles.css then.
Components
One page per component (usage, inputs and outputs, accessibility, tokens): see the component reference.
- Buttons and actions: Button, Button group, Split button, Toggle button, Select button
- Form controls: Form controls, Float label, Icon field, Input group, Input number, Password, Select, Auto complete, Multi select, Listbox, Date picker, Date time picker, Date time range picker, Date range quick select, Time picker, File upload, Rating, Slider
- Containers: Card, Panel, Fieldset, Accordion, Toolbar, Divider
- Overlays and messages: Dialog, Drawer, Popover, Confirm, Toast, Message, Tooltip
- Navigation: Menu, Menubar, Tabs, Stepper, Paginator, Breadcrumb
- Display: Avatar, Badge, Chip, Tag, Progress, Skeleton, Timeline
- Media: Carousel, Galleria, Image
- Data: Table, Data table, Tree, Chart (entry point
@gravionlabs/helix-ui/chart)
Theming
tokens.css gives every --h-* token its Helix value, light and dark. The theme service changes the colour choice while the app runs, with CSS custom properties only: no styling engine, no rebuild.
// app.config.ts
provideHxTheme({ storageKey: 'my-app-theme' })
// anywhere
readonly theme = inject(HxTheme);
theme.toggleDark();
theme.setPrimary('emerald'); // one of HX_PRIMARY_COLORS
theme.setSurface('zinc'); // one of HX_SURFACE_NAMES; null is the Helix default| Member | What it does |
|---|---|
dark(), setDark(on), toggleDark() | Dark mode: the class app-dark on <html> (or an attribute, see darkSelector). |
primary(), setPrimary(name | null) | The primary colour: emerald, green, lime, orange, amber, yellow, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose, or noir (black on light, white on dark). null is the colour of the preset. |
surface(), setSurface(name | null) | The grey scale behind backgrounds, borders and text: slate, gray, zinc, neutral, stone, soho, viva, ocean. null is the Helix default. |
css() | The style text of the overrides, for an app that renders it itself (server rendering). |
Options of provideHxTheme:
| Option | Default | |
|---|---|---|
darkSelector | '.app-dark' | A class or an attribute ('[data-theme="dark"]'); the selector tokens.css was built for. The published tokens.css keys on .app-dark only. |
manageDark | true | false: the host app owns the dark class or attribute, HxTheme never writes it to <html>; dark() still follows setDark() for the colour overrides. Example: Theming. |
storageKey | null | Remembers the choice in localStorage under this key; a damaged or blocked storage is ignored. |
dark | the system setting | Dark mode when nothing is remembered (prefers-color-scheme). |
primary, surface | null | The colours when nothing is remembered. |
How it works: the primary scale --h-primary-50 … 950 is pointed at another colour scale (var(--h-emerald-500)), the roles that depend on it (--h-primary-color is step 600 in light and 400 in dark) stay those of the tokens, so every component follows. A surface replaces --h-surface-0 … 950 in both colour schemes. The overrides live in one style sheet of the service (a constructed one where the browser has them, otherwise a <style data-hx-theme>), with a selector that wins over tokens.css in light and dark; the service removes it when it is destroyed. Nothing touches the DOM on the server.
Menu item model
HxMenuItem is the entry of every menu-like component (breadcrumb now; menu, menubar and split button follow):
import type { HxMenuItem } from '@gravionlabs/helix-ui';
const items: HxMenuItem[] = [
{ label: 'Settings', icon: 'pi pi-cog', routerLink: ['/settings'] },
{ separator: true },
{ label: 'Docs', url: 'https://example.com/docs', target: '_blank' },
{ label: 'Sign out', command: ({ item }) => signOut(item) },
];| Field | Type | Meaning |
|---|---|---|
label, ariaLabel | string | Text; ariaLabel is the accessible name when an icon-only entry has no label. |
icon | string | CSS classes of an icon font, e.g. pi pi-home. |
routerLink, queryParams | string | unknown[], object | Router navigation; wins over url. |
url, target | string | External link and its target. |
command | (event: { originalEvent: Event; item: HxMenuItem }) => void | Called when the entry is activated. |
items | HxMenuItem[] | Submenu, or a group when the entry has no action. |
disabled, visible, separator, id, badge | State, a hidden entry, a divider line, the element id, a short text next to the label. |
HxBreadcrumbItem is the same type.
Validators
@gravionlabs/helix-ui/validators (a secondary entry point) holds the validators that carry their own error message. The error text is the value of the error object, so a form field can show it directly.
import { FormControl } from '@angular/forms';
import { Validators } from '@gravionlabs/helix-ui/validators';
const email = new FormControl('', [
Validators.required('Email is required'),
Validators.email('Not an email address'),
]);
email.errors; // { Required: 'Email is required' } while the field is empty- Each validator takes the message first, either a
stringor a function of the value ((value) => string). - Validators for optional values take
allowEmpty(defaulttrue): an empty value is valid unless you passfalse. - The error key is the
ValidatorKeyenum member (Required,Email,Number,Integer,Min,Max,MinLength,MaxLength,Pattern,Date,OneOf,AllOf), so templates and error resolvers can switch on it. helix-zodand the shell use this entry point.
Icons
Components draw the few icons they need themselves, as CSS masks in the colour of the element, so an app needs no icon font for helix-ui to look right. The icons live in projects/ui/styles/_icons.scss (a partial: it emits no CSS of its own) on a 24 × 24 grid with round caps and joins:
chevron-down, chevron-up, chevron-left, chevron-right, check, close, plus, minus, search, calendar, eye, eye-off, upload, spinner, info, success, warn, error, star, star-filled, bars.
@use 'icons' as *;
.hx-select-chevron {
@include hx-icon(chevron-down, 1rem, var(--h-select-dropdown-color), 2.4);
}hx-icon($name, $size: 1em, $color: currentcolor, $stroke: 2) sets the box, the colour and the mask; an unknown name stops the build with the list of known icons. To add one, add its path to the $hx-icons map (a test compiles every entry). hx-icon-spin() and hx-icon-spin-keyframes() turn an icon, with a slower turn for users who prefer reduced motion.
Apps are free to keep PrimeIcons (pi pi-*) for the content of their own pages and for the icon field of menu items; helix-ui components that take an icon (icon="pi pi-home") accept the classes of any icon font.
Relation to helix-core
helix-ui replaced the components of the vendored PrimeNG fork @gravionlabs/helix-core (ADR 0001, ADR 0002). The fork is removed from the workspace; helix-shell, helix-zod, helix-ag-grid and the demo use helix-ui only, and pnpm lint:no-core fails when anything imports helix-core.
Rules of the package
- Nothing in
helix-shell,helix-zod,helix-ag-gridor the demo app imports@gravionlabs/helix-core(pnpm lint:no-core). - Nothing imports
@primeuix/*orprimeng(pnpm lint:no-primeuix,pnpm lint:no-primeng). - A test fails when a stylesheet reads a
--h-*token thattokens.cssdoes not define, or when a directive sets a class its stylesheet does not style. - The component styles are SCSS (
projects/ui/styles/*.scss) compiled at build time (pnpm build:lib:css:ui) to plain CSS: loops and mixins keep repetitive parts, such as the button severities, in one place, and the package ships and needs no SCSS. Components still read only--h-*tokens. - Every component has unit tests, a page in
docs/componentsand a section in the demo app.