Skip to content

Getting started

source → states → view

Connect your state source to Web Components, React, or headless consumers.

Your source owns state and behavior.

Use states to derive values for the view and commands to send actions to the source.

For ignite-element@latest (v2.2.2), see the stable v2 documentation.

Use an existing TypeScript/JSX web project, or start a new one below.

Set up a new project

In an empty directory, with Node 22.12+ and pnpm installed, run:

Terminal
pnpm init
pnpm add -D vite typescript

Vite builds and serves the example locally.

Ignite also works with other TypeScript/JSX web tooling.

Install Ignite and XState:

Terminal
pnpm add ignite-element@beta xstate

This light switch is a Web Component.

Try it: each flip changes the bulb and increments the count.

Download the complete example, or read the three steps below.

The download contains the same component, styles, and HTML shown below.

Unzip it, then run pnpm install and pnpm dev from its directory.

Create src/light-switch.tsx.

The first line selects Ignite JSX for this file.

src/light-switch.tsx
/** @jsxImportSource ignite-element/jsx */
import { igniteCore } from "ignite-element/xstate";
import { assign, createMachine } from "xstate";
const toggleMachine = createMachine(
{
context: { count: 0 },
initial: "off",
states: {
off: { on: { FLIP: { target: "on", actions: "countFlip" } } },
on: { on: { FLIP: { target: "off", actions: "countFlip" } } },
},
},
{
actions: {
countFlip: assign({ count: ({ context }) => context.count + 1 }),
},
},
);
export const core = igniteCore({
source: toggleMachine,
states: (snapshot) => ({
isOn: snapshot.matches("on"),
label: snapshot.matches("on") ? "On" : "Off",
count: snapshot.context.count,
}),
commands: ({ source }) => ({
toggle: () => source.send({ type: "FLIP" }),
}),
});
core("ignite-light-switch", (ctx) => (
<section class="light-switch" data-state={ctx.label}>
<link
rel="stylesheet"
href={new URL("./light-switch.css", import.meta.url).href}
/>
<svg class="bulb" viewBox="0 0 64 80" aria-hidden="true">
<path d="M22 56C22 46 10 44 10 28a22 22 0 0 1 44 0c0 16-12 18-12 28Z" />
<path d="M23 64h18M26 72h12" />
</svg>
<p class="state">{ctx.label}</p>
<p class="count">Toggled: {ctx.count}</p>
<button
type="button"
role="switch"
aria-label="Light"
aria-checked={String(ctx.isOn)}
onClick={() => ctx.toggle()}
>
<span class="track" aria-hidden="true">
<span class="thumb" />
</span>
Flip
</button>
</section>
));

toggleMachine owns the on/off state and flip count.

The states callback exposes isOn, label, and count.

The commands callback exposes toggle().

The view reads these values and calls the command through ctx.

Each <ignite-light-switch> gets its own actor, so its state is independent of other light switches on the page.

Create src/light-switch.css alongside the component.

Its stylesheet loads inside the element’s shadow root, keeping the bulb and switch styles local.

View the stylesheet
src/light-switch.css
:host {
display: block;
font-family: system-ui, sans-serif;
color: inherit;
}
.light-switch {
display: grid;
justify-items: center;
gap: 0.75rem;
padding: 2rem 1rem;
}
.bulb {
width: 5rem;
height: 6.25rem;
fill: none;
stroke: currentColor;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
[data-state="On"] .bulb {
fill: #facc15;
filter: drop-shadow(0 0 1rem #facc1566);
}
.state,
.count {
margin: 0;
}
.state {
font-size: 1.5rem;
font-weight: 700;
}
.count {
font-variant-numeric: tabular-nums;
}
button {
display: flex;
align-items: center;
gap: 0.75rem;
min-height: 2.75rem;
margin-top: 0.5rem;
padding: 0.5rem 0.75rem;
border: 2px solid currentColor;
border-radius: 0.5rem;
color: inherit;
background: transparent;
font: inherit;
cursor: pointer;
}
button:hover {
background: color-mix(in srgb, currentColor 10%, transparent);
}
button:focus-visible {
outline: 3px solid currentColor;
outline-offset: 4px;
}
.track {
display: block;
width: 2.5rem;
padding: 0.2rem;
border: 2px solid currentColor;
border-radius: 2rem;
}
.thumb {
display: block;
width: 1rem;
height: 1rem;
border-radius: 50%;
background: currentColor;
}
[aria-checked="true"] .thumb {
transform: translateX(1.5rem);
}
@media (prefers-reduced-motion: no-preference) {
.thumb {
transition: transform 150ms ease;
}
}

In an existing app, import src/light-switch.tsx from your entry module and add <ignite-light-switch></ignite-light-switch> to the page.

Run with Vite

If you used the Vite setup above, create index.html in the project root:

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ignite light switch</title>
</head>
<body>
<ignite-light-switch></ignite-light-switch>
<script type="module" src="/src/light-switch.tsx"></script>
</body>
</html>

Run this command from the project root, then open the local URL it prints:

Terminal
pnpm exec vite

Ignite manages each element’s private actor.

The core stays available for the page’s lifetime.

See Ownership & cleanup for application teardown.

Configure JSX for the project (optional)

For a project using only Ignite JSX, add this tsconfig.json and remove the first-line pragma from src/light-switch.tsx.

v3 uses native ES modules.

tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"jsxImportSource": "ignite-element/jsx",
"strict": true,
"noEmit": true
},
"include": ["src"]
}
  • Sources: choose an XState, Redux, or MobX source.
  • Views: render with Ignite JSX or React.
  • Examples: run a complete application.
  • API reference: look up exact contracts.

See Compatibility for supported runtimes and current limitations.