Skip to content

@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 ​

bash
npm install @gravionlabs/helix-ui
css
/* 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.css states the layer order Tailwind uses (theme, base, components, utilities) and puts the component CSS in components: an app's unlayered CSS and Tailwind's utilities win over it.
  • core.css is the smaller alternative to styles.css, see Component-only CSS.
  • tokens.css is the token data of projects/tokens resolved at build time: light on :root, dark under the app-dark class, 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 prefix hx; 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:uiRaw size
styles.css (60 components)266.1 kB
core.css (36 stylesheets, 48 %)128.3 kB
tokens.css129.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.

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.

ts
// 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
MemberWhat 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:

OptionDefault
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.
manageDarktruefalse: 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.
storageKeynullRemembers the choice in localStorage under this key; a damaged or blocked storage is ignored.
darkthe system settingDark mode when nothing is remembered (prefers-color-scheme).
primary, surfacenullThe 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.

HxMenuItem is the entry of every menu-like component (breadcrumb now; menu, menubar and split button follow):

ts
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) },
];
FieldTypeMeaning
label, ariaLabelstringText; ariaLabel is the accessible name when an icon-only entry has no label.
iconstringCSS classes of an icon font, e.g. pi pi-home.
routerLink, queryParamsstring | unknown[], objectRouter navigation; wins over url.
url, targetstringExternal link and its target.
command(event: { originalEvent: Event; item: HxMenuItem }) => voidCalled when the entry is activated.
itemsHxMenuItem[]Submenu, or a group when the entry has no action.
disabled, visible, separator, id, badgeState, 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.

ts
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 string or a function of the value ((value) => string).
  • Validators for optional values take allowEmpty (default true): an empty value is valid unless you pass false.
  • The error key is the ValidatorKey enum member (Required, Email, Number, Integer, Min, Max, MinLength, MaxLength, Pattern, Date, OneOf, AllOf), so templates and error resolvers can switch on it.
  • helix-zod and 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.

scss
@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-grid or the demo app imports @gravionlabs/helix-core (pnpm lint:no-core).
  • Nothing imports @primeuix/* or primeng (pnpm lint:no-primeuix, pnpm lint:no-primeng).
  • A test fails when a stylesheet reads a --h-* token that tokens.css does 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/components and a section in the demo app.

Released under the MIT licence.