Using it from a framework
The tags are real custom elements. There is no adapter package, no wrapper, and nothing to install on the other side. Register them once at the app entry, then render HTML.
A copy-paste starter that does exactly that lives in starter/ of the repo. This page is the same recipes, grouped by frontend and backend.
Every Alacris prop is both an attribute and a property. Frameworks disagree about which they set — React 18 sets attributes; Vue, Svelte, Angular, and Solid set properties. Both paths work.
el.setAttribute('age', '36'); // attribute path — parsed to a numberel.age = 36; // property path — used as-isAlacris does not server-render shadow trees. Put content that must be visible before JavaScript in a slot. The element upgrades when the script loads; properties assigned before upgrade are replayed.
Frontend
Section titled “Frontend”Call applyTheme once in the browser, then render tags. Named controls are form-associated — give them a name and they submit like native fields.
No bundler. Same pattern as the starter:
<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: '#e8ad18' });</script>
<ui-button>Save</ui-button><ui-text-field name="email" label="Email"></ui-text-field>Vite, esbuild, and Rollup resolve @alacris/ui with no extra config — npm install @alacris/ui and import from the entry module.
React 19 sets properties on unknown tags and maps onX to custom events:
import { applyTheme } from '@alacris/ui';applyTheme({ seed: '#e8ad18' });
export function Form() { return ( <> <ui-text-field label="Email" name="email" /> <ui-button onClick={() => {}}>Save</ui-button> </> );}On React 18, primitives go through as attributes; use a ref for objects and custom events:
const ref = useRef();useEffect(() => { const on = (e) => console.log(e.detail); ref.current.addEventListener('input', on); return () => ref.current.removeEventListener('input', on);}, []);return <ui-text-field ref={ref} label="Email" name="email" />;Mark the module that imports @alacris/ui as a Client Component. Custom elements do not SSR:
'use client';import { applyTheme } from '@alacris/ui';applyTheme({ seed: '#e8ad18' });
export function Form() { return <ui-button>Save</ui-button>;}const nextConfig = { transpilePackages: ['@alacris/core', '@alacris/ui'],};export default nextConfig;Put visible-before-JS copy in a slot, or in a sibling of the custom element.
<script setup>import { applyTheme } from '@alacris/ui';applyTheme({ seed: '#e8ad18' });</script>
<template> <ui-text-field label="Email" name="email" @input="onInput" /> <ui-button @click="onSave">Save</ui-button></template>Tell Vue the tag is a custom element:
vue({ template: { compilerOptions: { isCustomElement: (t) => t.startsWith('ui-') } } })export default defineNuxtConfig({ vue: { compilerOptions: { isCustomElement: (t) => t.startsWith('ui-') } },});Call applyTheme from a client-only plugin, not from a server plugin.
<script> import { applyTheme } from '@alacris/ui'; applyTheme({ seed: '#e8ad18' });</script>
<ui-text-field label="Email" name="email" /><ui-button on:click={onSave}>Save</ui-button>Svelte 5 uses onclick instead of on:click. In SvelteKit, import @alacris/ui from a browser-only module (onMount, or export const ssr = false on that subtree).
Import once in main.ts, add CUSTOM_ELEMENTS_SCHEMA, then:
<ui-text-field label="Email" name="email"></ui-text-field><ui-button (click)="onSave()">Save</ui-button>import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({ standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<ui-button (click)="onSave()">Save</ui-button>`,})export class FormComponent {}Property bindings ([value]="x") set properties; attribute bindings set attributes. Both work.
Solid sets properties.
import { applyTheme } from '@alacris/ui';applyTheme({ seed: '#e8ad18' });
export function Form() { return ( <> <ui-text-field label="Email" name="email" /> <ui-button onClick={onSave}>Save</ui-button> </> );}In SolidStart, import @alacris/ui from a client-only island so applyTheme does not run on the server.
------<ui-button>Hello</ui-button>
<script> import { applyTheme } from '@alacris/ui'; applyTheme({ seed: '#e8ad18' });</script>The <script> is bundled and runs in the browser. Server-rendered markup around the tag is slotted content and is visible before the script runs.
Preact matches React 19 (properties on custom elements). Lit can host the tags in its own templates — share one @alacris/core on the page; do not load a second copy from a CDN. Remix and React Router 7 follow the React 19 path inside client modules.
Backend
Section titled “Backend”Emit the tags from the template. Load the module once on the layout. Do not call applyTheme on the server — it writes a document stylesheet.
pin "@alacris/core", to: "https://cdn.jsdelivr.net/npm/@alacris/core@0.11.3/dist/alacris.js"pin "@alacris/ui", to: "https://cdn.jsdelivr.net/npm/@alacris/ui@0.3.0/src/index.js"<%= javascript_importmap_tags %><script type="module"> import { applyTheme } from '@alacris/ui'; applyTheme({ seed: '#e8ad18' });</script>
<ui-button><%= t('save') %></ui-button><ui-text-field name="email" label="Email" value="<%= user.email %>"></ui-text-field>With jsbundling-rails or Vite Ruby, npm install @alacris/ui and import from the pack.
<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: '#e8ad18' });</script>
<ui-text-field name="email" label="Email" value="{{ user.email }}"></ui-text-field><ui-button type="submit">{% trans "Save" %}</ui-button>Form-associated ui-* controls post like <input> when they have name. A small Widget subclass can render the tag from forms.py.
import { applyTheme } from '@alacris/ui';applyTheme({ seed: '#e8ad18' });@vite(['resources/js/app.js'])
<ui-button>{{ __('Save') }}</ui-button><ui-text-field name="email" label="Email" value="{{ old('email', $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: '#e8ad18' });</script><ui-button>Save</ui-button><ui-text-field name="email" label="Email" value={@user.email}></ui-text-field>LiveView: patch attributes and keep the node’s identity. Avoid innerHTML replacements that destroy the element. For a LiveView-native runtime, see Alacris-Go.
Express, Fastify, and the rest serve the same static HTML as the starter. Template the tags with any engine:
<ui-button><%= label %></ui-button>Do not call applyTheme in Node.
The pattern is the same: emit attributes from the template, load the module on the layout.
Go (html/template):
<ui-text-field name="email" label="Email" value="{{.Email}}"></ui-text-field>ASP.NET / Razor:
<ui-text-field name="Email" label="Email" value="@Model.Email"></ui-text-field>Spring / Thymeleaf:
<ui-text-field name="email" label="Email" th:attr="value=${user.email}"></ui-text-field>Keep @alacris/ui on the layout. Swap only the body fragment. Custom elements in the swapped HTML upgrade when they connect.
<form hx-post="/save" hx-swap="outerHTML"> <ui-text-field name="email" label="Email"></ui-text-field> <ui-button type="submit">Save</ui-button></form>Named ui-* controls submit like native fields. Do not reload the import map or @alacris/core on every request.
Why attributes and properties both matter
Section titled “Why attributes and properties both matter”A component that only handles one of them works in half the ecosystem. The default’s type drives coercion, so props: { age: 0 } turns "36" into 36, and props: { tags: [] } parses '["a"]' as JSON.
The same contract is documented for components you write yourself on Using it from a framework.
- Live catalog — every component, a theme playground, nothing to clone
- Getting started — install, theming, entry points
- Starter in the repo — a page you can run with
npm run demo