web-toolkit-x
v0.72.0
Published
Web Toolkit X, a Web UI toolkit
Maintainers
Readme
Web Toolkit X
Web Toolkit X is a lightweight library for building Web user interfaces as a composition of small, self-contained apps. You describe what the UI should look like for a given state, and Toolkit keeps the page in sync with it.
It originates from the Opera Web UI Toolkit.
In a nutshell
- Self-contained pieces - each part of the UI owns its state, styles and behaviour, isolated from the rest of the page, so it can be built, tested and reused on its own.
- Just JavaScript - the UI is described with plain data and functions, with no template language to learn and no build step required.
- Predictable state - the state changes only in explicit, named steps, and the UI always reflects the current state.
- Built on the platform - it relies on what the browser already provides, like Web Components and ES modules, rather than working around it.
- Mistakes caught early - types and a debug mode point out errors in the UI while it is being written.
- Extensible - plugins add behaviour across all the apps, like logging, shared styles or helper methods, without changing them.
Usage
import toolkit, {
WebComponent,
type CommandsAPI,
type Template,
} from 'web-toolkit-x'
type Props = { start: number }
type State = { count: number }
const CounterCommands = {
increment() {
this.count += 1
},
} satisfies CommandsAPI<State>
class Counter extends WebComponent<State, typeof CounterCommands, Props> {
static elementName = 'my-counter'
static styles = ['styles/counter.css']
static commands = CounterCommands
getInitialState(props: Props): State {
return { count: props.start }
}
render(): Template {
return [
'button',
{ onClick: () => this.commands.increment() },
`Clicked ${this.props.count} times`,
]
}
}
await toolkit.render(Counter, document.body, { start: 0 })The types are optional, the same component works in plain JavaScript without them.
Toolkit is also available as a single script, exposing the toolkit global:
<script src="toolkit-0.72.0.js"></script>The global is typed by the web-toolkit-x/global declarations, listed in types in tsconfig.json or referenced in the scripts using it:
/// <reference types="web-toolkit-x/global" />Configuration
Toolkit renders without the debug mode and plugins by default. Both can be configured at any time, also after rendering. Options not provided keep their current values. Changed plugins are uninstalled from the created roots and the new ones installed:
toolkit.configure({ debug: true, plugins: [plugin] })Components
Web Components render the content of an app, managing its state. Nested Web Components are rendered in their own custom elements.
Their state is created from the props in getInitialState() and updated with commands, or by the parent with getUpdatedState().
Components render fragments of a Web Component from props, and pure components are just functions:
const Square = (props: { color: string; size: number }): Template => [
'section',
{
class: 'square',
style: {
backgroundColor: props.color,
height: [props.size, 'px'],
width: [props.size, 'px'],
},
},
]Both can define the onCreated(), onAttached(), onPropsReceived(), onUpdated(), onDestroyed() and onDetached() lifecycle methods.
Read more about Bragi templates, the Commands API and see a few examples.
TypeScript
Components are typed with their props, and their render() methods return a Template. Templates are type-checked, so misspelled props and attributes, unknown style properties or listeners of wrong event types are reported:
import { Component, type Template } from 'web-toolkit-x'
class Title extends Component<{ text: string }> {
render(): Template {
return ['h1', { class: 'title' }, this.props.text]
}
}Child templates passed to a component are available as this.children:
class Card extends Component<{ title: string }> {
render(): Template {
return ['section', ['h2', this.props.title], ...this.children]
}
}Web Components take the types of the state, the Commands API and props: WebComponent<State, Commands, Props>, with the Commands API and props optional, as in WebComponent<State>. The Commands API is typed with the state it changes, using satisfies CommandsAPI<State>, as in the usage example above.
The state can contain only primitives, functions, plain objects, arrays, maps, sets, dates and typed arrays. Commands record their changes on copies of these objects, sharing the unchanged parts with the previous state, while primitives and functions are just replaced.
Build
npm run buildIt creates in dist/release:
index.js- an ES module with type declarations inindex.d.ts,toolkit-<version>.js- a single script exposing thetoolkitglobal, typed inglobal.d.ts,
both with source maps, and a declaration map leading editors to the TypeScript sources.
npm run dev creates the same files in dist/dev and builds them again on every change.
Demo
npm run demoIt builds Toolkit and runs the demos with the ES module build:
- Flemmings, the default one, with flamingos walking, wading and idling, maintained by nested Web Components,
- Grid, at
/grid.html, with tiles reordered by dragging them.
Development
Toolkit requires Node 24, as defined in .nvmrc:
nvm use
npm install
npx playwright install chromium firefox # once, for running the tests| Command | Description |
| ----------------------- | ------------------------------------------------------------------- |
| npm test | runs the tests in Chromium with Vitest |
| npm run test:browsers | runs the tests in Chromium and Firefox |
| npm run build | builds Toolkit into dist/release |
| npm run dev | builds Toolkit into dist/dev on every change |
| npm run typecheck | checks the types with TypeScript |
| npm run lint | lints the code with ESLint |
| npm run verify | runs the build, the type check, the linter and the formatting check |
| npm run format | formats the code with Prettier |
| npm run coverage | runs the tests in Chromium with the coverage report |
| npm run bench | runs the benchmarks |
Git hooks format and lint the committed files, and run the checks and tests before pushing.
