Ignite-Element is a framework-agnostic way to build stateful Custom Elements. Bring your state library (XState, Redux, MobX), get typed commands, states, and emit, and render with the built-in Ignite JSX runtime or lit.
Quick links: Quick start · Install matrix · Typed events · Styling · Examples
- Works with XState, Redux, or MobX (shared or per-element state, inferred automatically)
- Fully Typed commands and emit
- Tiny runtime; no React/Solid dependency for JSX
- Configurable renderer and global styles through
ignite.config.ts
- Install
npm install ignite-element xstate- TypeScript JSX (required if you use the Ignite JSX renderer)
If you can’t change tsconfig, add /** @jsxImportSource ignite-element/jsx */ at the top of each JSX/TSX file instead.
- Add config (all fields are optional)
// ignite.config.ts
import { defineIgniteConfig } from "ignite-element/config";
export default defineIgniteConfig({
styles: new URL("./styles.css", import.meta.url).href,
renderer: "ignite-jsx", // or "lit"
logging: "warn",
});- Wire the Vite plugin
// vite.config.ts
import { defineConfig } from "vite";
import { igniteConfigVitePlugin } from "ignite-element/config/vite";
export default defineConfig({ plugins: [igniteConfigVitePlugin()] });- Create a component
import { createMachine } from "xstate";
import { igniteCore } from "ignite-element/xstate";
const machine = createMachine({
initial: "off",
states: {
off: { on: { TOGGLE: "on" } },
on: { on: { TOGGLE: "off" } }
}
});
const component = igniteCore({
source: machine,
events: (event) => ({ toggled: event<{ isOn: boolean }>() }),
states: (snapshot) => ({ isOn: snapshot.matches("on") }),
commands: ({ actor, emit }) => ({
toggle: () => {
actor.send({ type: "TOGGLE" });
emit("toggled", { isOn: actor.getSnapshot().matches("on") });
},
}),
});
component("toggle-button", ({ isOn, toggle }) => (
<button onClick={toggle}>{isOn ? "On" : "Off"}</button>
));- Use it
<toggle-button></toggle-button>- XState:
npm install ignite-element xstate - Redux:
npm install ignite-element @reduxjs/toolkit - MobX:
npm install ignite-element mobx
- Isolated adapters (the default when you pass factories or definitions) are created per custom element. Ignite Element automatically calls
stop()on disconnect, so no extra work is required. - Shared adapters (long-lived instances you construct once) are reference-counted and stopped automatically when the final element disconnects. Set
cleanup: falseif you want to keep them alive and stop them manually.
// Shared XState actor example
const actor = createActor(machine);
actor.start();
const shared = igniteCore({
source: actor,
cleanup: false, // leave actor running until the host decides to stop it
states: (snapshot) => ({ count: snapshot.context.count }),
});
shared("shared-counter", ({ count }) => <span>{count}</span>);
// Stop the actor when your host application shuts down
window.addEventListener("beforeunload", () => actor.stop());Use the same approach for shared Redux stores, MobX observables, or any custom adapters: set cleanup: false if they outlive your elements and stop them yourself when the host app shuts down.
igniteCore merges the outputs of your facade callbacks into the render arguments:
states(snapshot)derives the values your component needs to display.commands({ actor, emit, host })returns the actions your component can call; when you declareevents, it also includes the typedemithelper and thehostelement.
Both callbacks run once per adapter instance (shared) or per element (isolated), so you can safely memoize values or close over resources without worrying about duplicate subscriptions.
Opt in by declaring an events map:
const registerCounter = igniteCore({
source: counterSlice,
events: (event) => ({
"counter:incremented": event<{ amount: number }>(),
}),
commands: ({ actor, emit }) => ({
add: (amount: number) => {
actor.dispatch(counterSlice.actions.addByAmount(amount));
emit("counter:incremented", { amount });
},
}),
});Commands receive { actor, emit, host }. The emit helper dispatches bubbling, composed CustomEvent instances so parents can listen with addEventListener. When no events map is supplied the helper is omitted, keeping render args lean.
Heads-up: event name inference is most reliable when
eventsis declared beforecommands. We’re tightening this in a future release.
You can:
- Declare component-wide styles in
ignite.config.ts(styles, formerlyglobalStyles, accepts a string URL or object literal stylesheet). These are injected into each component’s shadow root, not the page’s light DOM. - Provide custom CSS per component.
- Combine both for progressive enhancement.
For page shell / light-DOM styling (e.g. body background, layout), import a stylesheet in your app entry or include a <link> in index.html. Use styles for the component layer.
If you aren’t using the Vite/Webpack plugins, keep ignite.config.ts and import it in your app’s entry point (e.g. main.ts) so styles and renderer defaults are applied before you register components.
Every example demonstrates a different pattern and styling approach:
| Example | State Library | Styling | Highlights |
|---|---|---|---|
| XState + Tailwind | XState | Tailwind CSS | Isolated machine vs. shared actor, gradient sub-component |
| Redux + Bootstrap | Redux Toolkit | Bootstrap | Store factory vs. shared store, scoped Bootstrap link injection |
| MobX + Custom | MobX | Custom CSS | Observable reuse vs. new instances, hybrid global + component styles |
pnpm run examples:xstate
pnpm run examples:redux
pnpm run examples:mobx💡 Start with the XState example to see shared and isolated behaviour side-by-side.
Ignite-Element targets evergreen browsers with:
- Custom Elements v1
- Shadow DOM v1
- ES Modules
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✅ 67+ | ✅ 63+ | ✅ 10.1+ | ✅ 79+ |
For legacy support, include the webcomponents polyfills.
| Package | Description | Size (min + gzip) |
|---|---|---|
ignite-element |
Core runtime (facades, adapters) | ~3.2 KB |
ignite-element (Ignite JSX) |
Core runtime + Ignite JSX renderer | ~4.2 KB |
ignite-element + lit-html |
Optional lit strategy | ~8.3 KB |
Rendering engines and state libraries (lit-html, XState, Redux Toolkit, MobX) are optional peer dependencies. Mix only what your project needs—ignite-element itself adds ~4 KB on top of the stack you choose.
- Ignite Element v2 (Starlight)
- Getting Started (v2)
- Core Concepts (v2)
- API Notes
- Styling Guide
- Examples Overview
| Symptom | Fix |
|---|---|
| Component not rendering | Ensure you've configured jsxImportSource (or installed lit-html and selected the lit strategy). |
| State not updating | Confirm you’re using the provided send function and that your store/machine handles the event. |
| TypeScript errors | Align adapter dependencies (xstate, @reduxjs/toolkit, mobx) with the versions in package peer requirements. |
Need more help? Check the FAQ or open an issue.
Best fit:
- Building reusable, state-driven component libraries.
- Projects that need framework flexibility or native web component distribution.
- Teams looking for deterministic state management with minimal runtime overhead.
Consider alternatives when:
- You are deeply invested in a single framework (React, Vue, etc.) and prefer their native component models.
- Server-side rendering is a strict requirement today (SSR support is on the roadmap).
We welcome all contributions!
- 🐛 Report bugs
- 💡 Propose ideas
- 📝 Improve docs, clarify examples, or fix typos
- 🔨 Submit pull requests
git clone https://github.com/<your-username>/ignite-element.git
cd ignite-element
pnpm install
git checkout -b feature/my-awesome-feature
pnpm testPlease review our Code of Conduct before contributing.
Ignite-Element is released under the MIT License.
We appreciate feedback—let us know what helps or what’s missing.