Getting started
Install, derive, and use a theme
xtyle derives a complete, internally consistent set of 310 design tokens (surfaces, text, accents, type, spacing, and more) from an algorithm and a few anchor colors. This page covers installing it, deriving a theme, using the result with or without a framework, and writing your own algorithm.
Install
xtyle is three packages. @xtyle/core is the engine: the
derivation, the xtyle command line, and the web components
(behind the @xtyle/core/elements entry). Add a binding only if
you build with that framework.
npm install @xtyle/core
# optional framework bindings
npm install @xtyle/svelte
npm install @xtyle/astroDerive a theme
An algorithm maps your anchors and knobs to a full token register. The
built-in algorithms ship with the engine under
@xtyle/core/algorithms; import one and call
derive.
import { derive, emit } from "@xtyle/core";
import { xtyleDefault } from "@xtyle/core/algorithms";
const register = derive(xtyleDefault, {
constraints: { "--bg-0": "#0b0d12", "--fg-0": "#e6e9ef", "--accent": "#6ea8fe" },
knobs: { scheme: "dark", contrastBand: "aa", vibrancy: 0.5 },
});
const css = emit(register, "css"); // a :root { … } block
const json = emit(register, "json"); // a flat token mapOr derive from the command line without writing any code:
npx xtyle derive --bg "#0b0d12" --accent "#6ea8fe" --format css > theme.cssThe built-in algorithms, each a named export and an id:
xtyleDefault
the neutral default
xtyleHc
high contrast
xtyleQuiet
restrained
xtyleLoud
vibrant
nxiNite
Day/Night, time-aware
Anchors
anchors is { bg, fg, accent }, each a hex
string. They seed the derivation; every other token solves around them.
Pass as few or as many as you like. Pin nothing and the algorithm's
own defaults fill in, or pin individual tokens through
constraints and the rest re-derive to fit.
Knobs
Knobs adjust how an algorithm derives. The common ones, plus any an
algorithm declares itself (nxi-nite adds hour):
Use the theme
A derived theme is plain CSS custom properties. Write the emitted CSS into
your page and the cascade applies it; nothing has to be running at view
time. In the browser, @xtyle/core/dom writes a register straight
onto a live element.
import { apply } from "@xtyle/core/dom";
apply(register); // write onto :root
apply(register, { target: previewEl }); // scope to one element
apply(register, { persistKey: "theme" }); // and persist itRun the engine live only when an input changes at runtime: a theme editor, or a time-aware algorithm like nxi-nite re-deriving as the hour moves. Otherwise the engine's job ends once the CSS is written.
Components
The token register is the contract: components read it with
var(--token) and never raw colors or magic numbers, so any valid
theme styles them with no extra work. Use the raw custom elements, or a
framework binding.
import "@xtyle/core/elements"; // registers <xtyle-button>, <xtyle-card>, …import { Button, Card } from "@xtyle/svelte"; @xtyle/svelte and @xtyle/astro wrap the same elements;
each depends only on @xtyle/core.
Bundlers
Components render their chrome through a sandboxed fragment runtime, and that runtime
loads a WebAssembly module of its own. Vite's dependency pre-bundling rewrites the
runtime's JavaScript into node_modules/.vite/deps/ without carrying the
wasm along, so the fetch 404s and every element mounts as an empty box. Tokens still
apply, which makes it read as a styling problem rather than a loading one.
// vite.config.ts
import { viteExcludes } from "@xtyle/core/vite";
export default defineConfig({
optimizeDeps: { exclude: [...viteExcludes] },
});
Excluding @xtyle/core alone is not enough: the runtime is reached
transitively and stays pre-bundled, so the whole chain has to be named. That is xtyle's
dependency graph, not yours, which is why the list ships from
@xtyle/core/vite and moves when the graph does. Other bundlers that rewrite
module locations can need the same treatment; the symptom is always an element that
registers, paints nothing, and reports a wasm module whose first bytes are HTML.
The other route needs no bundler config at all, and it covers every bundler rather than one: hand xtyle a QuickJS build that carries the wasm inline, and there is no separate asset left to lose.
// main.ts — no bundler config at all
import { setSandboxVariant } from "@xtyle/core";
import variant from "@jitl/quickjs-singlefile-browser-release-sync";
import "@xtyle/core/elements";
setSandboxVariant(variant);
Install the variant at the version quickjs-emscripten-core resolves to.
A mismatched one loads and then answers every sandbox call with
QuickJSContext had no callback with id 0, which reads as an element that
renders nothing. The trade is the ordinary one and it is yours: inline costs every
visitor the bytes up front and cannot be cached on its own, while a separate asset is
smaller but has to survive what your bundler does to module locations.
Write an algorithm
An algorithm is a xript mod: a directory with a mod-manifest.json
and code. Build on the shared xtyle derivation with
defineXtyleAlgorithm (declare taste; the house derivation does the
rest), or write one from scratch with defineAlgorithm.
// algorithms/sunrise/mod-manifest.json declares the mod;
// algorithms/sunrise/src/mod.ts defines it:
import { defineXtyleAlgorithm } from "@xtyle/core/authoring";
defineXtyleAlgorithm({
id: "sunrise",
anchors: { bg: "#1a1410", accent: "#ff9e5e" },
vibrancy: 0.7,
});
Drop the directory in, build the mods, and the algorithm loads by id. Same
path the built-in algorithms take. An algorithm that derives in
stages lists ordered passes; nxi-nite is a worked example, layering
a time-of-day shift onto the base derivation.
See it run
Questions or contributions live on