Getting started
Alacris is the runtime. Alacris UI (@alacris/ui on npm) is a complete design system built with it: 68 custom elements, a three-tier token system, and a theme engine that re-skins the whole page with one stylesheet write. Material Design is the default. Your design system is the point.
The tags are real custom elements. They work in a static HTML file, inside React, Vue, Svelte, Angular, Rails, Django, and — later — Alacris-Go live views. There is no adapter package.
Install
Section titled “Install”npm install @alacris/uiThat installs @alacris/core as well. Theme first, so the first paint is already themed, then use the tags:
import { applyTheme } from '@alacris/ui';
applyTheme({ seed: '#0b57d0' });<ui-button>Hello</ui-button><ui-text-field label="Email" clearable></ui-text-field><ui-switch label="Dark mode"></ui-switch>Importing @alacris/ui registers every component. For a smaller page, import only what you use — each component module is self-contained:
import { applyTheme } from '@alacris/ui/theme';import '@alacris/ui/components/ui-button.js';import '@alacris/ui/components/ui-text-field.js';
applyTheme({ seed: '#0b57d0' });No framework, no bundler
Section titled “No framework, no bundler”The published package is plain ESM. An import map lets you write the same bare specifiers a bundler would resolve:
<!doctype html><html lang="en"> <head> <meta charset="utf-8"> <script type="importmap"> { "imports": { "@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11.3/dist/alacris.js", "@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.3.0/src/index.js" } } </script> <script type="module"> import { applyTheme } from '@alacris/ui'; applyTheme({ seed: '#0b57d0' }); </script> </head> <body> <ui-button>Hello</ui-button> </body></html>The URLs pin @alacris/core@0.11.3 and @alacris/ui@0.3.0. Never mix two versions of
@alacris/core on one page — two copies means two reactive graphs.
<script type="importmap">{ "imports": { "@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11.3/dist/alacris.js", "@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.3.0/src/index.js" }}</script><script type="importmap">{ "imports": { "@alacris/core": "https://unpkg.com/@alacris/core@0.11.3/dist/alacris.js", "@alacris/ui": "https://unpkg.com/@alacris/ui@0.3.0/src/index.js" }}</script>In a framework
Section titled “In a framework”Register the elements once at the app entry, then render tags. Props are both attributes and properties, so React 19, Vue, Svelte, Angular, and server-rendered HTML all work without wrappers. The full recipes — Next, Nuxt, SvelteKit, Rails, Django, Laravel, Phoenix, HTMX — are on Using it from a framework.
<script type="module"> import { applyTheme } from '@alacris/ui'; applyTheme({ seed: '#0b57d0' });</script>
<ui-button>Save</ui-button><ui-text-field name="email" label="Email"></ui-text-field>import { applyTheme } from '@alacris/ui';applyTheme({ seed: '#0b57d0' });
export function Form() { return ( <> <ui-text-field label="Email" name="email" /> <ui-button onClick={() => {}}>Save</ui-button> </> );}React 19 sets properties and maps onX to events. On React 18, primitives go through as attributes; use a ref for objects and custom events.
<script setup>import { applyTheme } from '@alacris/ui';applyTheme({ seed: '#0b57d0' });</script>
<template> <ui-text-field label="Email" name="email" /> <ui-button @click="onSave">Save</ui-button></template>Tell Vue the tag is a custom element:
vue({ template: { compilerOptions: { isCustomElement: (t) => t.startsWith('ui-') } } })<script> import { applyTheme } from '@alacris/ui'; applyTheme({ seed: '#0b57d0' });</script>
<ui-text-field label="Email" name="email" /><ui-button on:click={onSave}>Save</ui-button>Add CUSTOM_ELEMENTS_SCHEMA, import @alacris/ui once in main.ts, then:
<ui-text-field label="Email" name="email"></ui-text-field><ui-button (click)="onSave()">Save</ui-button>Server-rendered markup is the easy case — the element upgrades when the script loads:
<ui-button><%= t('save') %></ui-button><ui-text-field name="email" label="Email" value="<%= user.email %>"></ui-text-field><script type="importmap">{ "imports": { "@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11.3/dist/alacris.js", "@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.3.0/src/index.js" }}</script><script type="module"> import { applyTheme } from '@alacris/ui'; applyTheme({ seed: '#0b57d0' });</script>Named controls are form-associated. Give ui-text-field, ui-switch, ui-checkbox, ui-select and the rest a name and they submit, reset, and follow <fieldset disabled> like native fields.
Theming
Section titled “Theming”Every visual decision flows through CSS custom properties. Components never hardcode a colour, radius, or duration — they speak only sys.* tokens — so a theme change restyles everything without a re-render.
import { createTheme, applyTheme, setScheme, toggleScheme } from '@alacris/ui/theme';
applyTheme({ seed: '#6750a4' });applyTheme({ seed: '#b3261e', density: -1 });setScheme('dark');toggleScheme();
applyTheme(createTheme({ colors: { primary: '#0b57d0', tertiary: '#00695c' }, typography: { brand: 'Inter, sans-serif', scale: 1.05 }, shape: { radius: 0.5 }, overrides: { light: { 'color-surface': '#faf7f2' } },}));applyTheme writes one document-level stylesheet of --ui-* custom properties and replaces it in place on the next call. Scheme switching after that is data-ui-scheme on <html> (light | dark | auto).
Three places to intervene, from broadest to narrowest:
- Re-theme the system —
applyTheme({ seed, shape, … }). Every component follows. Dark mode is generated automatically. - Re-skin one component type —
ui-button { --ui-button-radius: 4px; }. - Reach inside one instance —
ui-dialog::part(surface) { backdrop-filter: blur(8px) }.
The last two are the same consumer theming contract Alacris itself teaches (vars(), ::part). Alacris UI adds the first layer: a generated Material-shaped palette and a sys accessor so components never write raw var(--ui-…) strings.
Motion
Section titled “Motion”Durations and easings are tokens, so the theme’s motion.scale governs CSS transitions and JS animation alike, and prefers-reduced-motion is honoured automatically:
import { html } from '@alacris/core';import { animate, fx, presence } from '@alacris/ui/motion';
animate(el, fx.slideInUp, { duration: 'medium2', easing: 'emphasizedDecelerate' });
html`${presence(open, () => html`<div class="sheet">…</div>`, { enter: fx.slideInUp, exit: fx.slideOutDown,})}`;presence is the exit-animation hook fine-grained rendering does not give you on its own.
Entry points
Section titled “Entry points”| Import | What it is |
|---|---|
@alacris/ui |
registers every component; re-exports theme, motion, tokens, utilities |
@alacris/ui/theme |
createTheme, applyTheme, scheme switching |
@alacris/ui/motion |
animate, fx, presence, withFlip, ripple |
@alacris/ui/tokens |
sys and the token engines |
@alacris/ui/components/ui-button.js |
one element, registered as a side effect |
Component modules have side effects (they call define()). Theme, motion and token modules do not, so a bundler can tree-shake them.
Kitchen sink and starter
Section titled “Kitchen sink and starter”The live catalog is the kitchen sink: every component plus a theme playground, composed from the same tags you import from npm. It is a real page on this site — link people there.
A small app that uses those tags lives in starter/ of the repo. Copy that folder as an install guide; it is not the published package. From a clone, npm run demo serves the catalog at http://localhost:5173/ui/ and the starter at http://localhost:5173/starter/.
- Live catalog — every component, a theme playground, nothing to clone
- Using it from a framework — attributes vs properties, Next, Nuxt, Rails, Django, HTMX
- Your first component — the Alacris model, if you are writing elements of your own
- Theming for consumers —
vars(),::part, andadoptGlobal