@beforesemicolon/web-component

Web Components.
Now Reactive.

A tiny reactive layer over the native Web Components API. Props, state, scoped styles, and lifecycles — all built on Markup.

Native CUSTOM ELEMENTS
0 DEPENDENCIES
FRAMEWORK-FRIENDLY
hello-world.js
javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class HelloWorld extends WebComponent {4    static observedAttributes = ['name']5 6    name = 'World'7 8    render() {9        return html`<h1>Hello, ${this.props.name}!</h1> `10    }11}12 13customElements.define('hello-world', HelloWorld)14// <hello-world name="James"></hello-world>

Part of the Markup family.

Web Component is built on Markup, and Router is built on top of Web Component — same engine, modular packages.

Markup

@beforesemicolon/markup

The 9Kb reactive templating system that powers Web Component. Use it directly for non-component templates.

Read the docs

Router

@beforesemicolon/router

Declarative routing as web component tags — built on top of Web Component. Nested routes, params, 404s.

Read the docs

Custom Elements, supercharged.

Everything you love about native Web Components — plus the reactivity, ergonomics, and tiny footprint of Markup.

Reactive props & state

Observed attributes become reactive props. Updating state updates only the DOM that depends on it.

Tiny

A thin layer over the native Web Components API. Built on Markup — no extra dependencies.

Scoped styles

Per-component stylesheets that ship with the element. No leaks, no conflicts, no CSS-in-JS runtime.

Native lifecycles

onMount, onUpdate, onDestroy, onAdoption — first-class hooks for everything Custom Elements expose.

Works everywhere

Custom Elements run in any framework, in plain HTML, or in any tool that renders DOM.

Built on Markup

Same reactive engine, same templating, same 0-build philosophy. Drop in a script tag and ship.

Real components. Native tags.

Build focused elements that own their props, state, styles, lifecycle work, and public events.

EXAMPLE 01
Quantity stepper
quantity-stepper.js
javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class QuantityStepper extends WebComponent {4    static observedAttributes = ['label', 'min', 'max']5    label = 'Seats'6    min = 17    max = 108    initialState = { value: 1 }9 10    setValue = (value) => {11        const next = Math.min(12            Number(this.props.max()),13            Math.max(Number(this.props.min()), value)14        )15        this.setState({ value: next })16        this.dispatch('change', { value: next })17    }18 19    render() {20        return html`21            <label>${this.props.label}</label>22            <button onclick="${() => this.setValue(this.state.value() - 1)}">23                -24            </button>25            <output>${this.state.value}</output>26            <button onclick="${() => this.setValue(this.state.value() + 1)}">27                +28            </button>29        `30    }31}32 33customElements.define('quantity-stepper', QuantityStepper)
EXAMPLE 02
Todo list
todo-list.js
javascript
1import { WebComponent, html, repeat } from '@beforesemicolon/web-component'2 3class TodoList extends WebComponent {4    initialState = {5        draft: '',6        todos: ['Plan release', 'Write docs'],7    }8 9    add = () => {10        const text = this.state.draft().trim()11        if (!text) return12 13        this.setState(({ todos }) => ({14            draft: '',15            todos: [...todos, text],16        }))17    }18 19    render() {20        return html`21            <input22                value="${this.state.draft}"23                oninput="${(event) =>24                    this.setState({ draft: event.target.value })}"25            />26            <button onclick="${this.add}">Add</button>27            <ul>28                ${repeat(this.state.todos, (todo) => html`<li>${todo}</li>`)}29            </ul>30        `31    }32}33 34customElements.define('todo-list', TodoList)
EXAMPLE 03
Pricing toggle
pricing-toggle.js
javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class PricingToggle extends WebComponent {4    static observedAttributes = ['monthly', 'yearly']5    monthly = 196    yearly = 1907    initialState = { yearly: false }8 9    toggle = () => {10        this.setState(({ yearly }) => ({ yearly: !yearly }))11    }12 13    render() {14        return html`15            <button16                type="button"17                aria-pressed="${this.state.yearly}"18                onclick="${this.toggle}"19            >20                Bill yearly21            </button>22            <strong>23                $${() => {24                    return this.state.yearly()25                        ? this.props.yearly()26                        : this.props.monthly()27                }}28            </strong>29        `30    }31}32 33customElements.define('pricing-toggle', PricingToggle)
EXAMPLE 04
Validated field
email-field.js
javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class EmailField extends WebComponent {4    static formAssociated = true5    initialState = { value: '', error: '' }6 7    update = (event) => {8        const value = event.target.value9        const valid = value.includes('@')10 11        this.setState({12            value,13            error: valid ? '' : 'Enter a valid email.',14        })15        this.internals.setFormValue(value)16        this.internals.setValidity(17            valid ? {} : { customError: true },18            valid ? '' : 'Enter a valid email.',19            this.refs.input?.[0]20        )21    }22 23    render() {24        return html`25            <input26                ref="input"27                type="email"28                value="${this.state.value}"29                oninput="${this.update}"30            />31            <small>${this.state.error}</small>32        `33    }34}35 36customElements.define('email-field', EmailField)
EXAMPLE 05
Reactive stylesheet
status-pill.js
javascript
1import {2    WebComponent,3    html,4    css,5    is,6    when,7} from '@beforesemicolon/web-component'8 9class StatusPill extends WebComponent {10    static observedAttributes = ['status']11    status = 'ready'12 13    stylesheet = css`14        :host {15            display: inline-flex;16            border-radius: 999px;17            padding: 0.35rem 0.7rem;18            background: ${when(19                is(this.props.status, 'ready'),20                '#dcfce7',21                '#dbeafe'22            )};23            color: ${when(24                is(this.props.status, 'ready'),25                '#166534',26                '#1d4ed8'27            )};28        }29    `30 31    render() {32        return html`<slot>${this.props.status}</slot>`33    }34}35 36customElements.define('status-pill', StatusPill)

Install in seconds.

Choose your preferred installation method. Works everywhere JavaScript runs.

<script src="https://unpkg.com/@beforesemicolon/web-component/dist/client.js"></script>
npm install @beforesemicolon/web-component
yarn add @beforesemicolon/web-component
pnpm add @beforesemicolon/web-component

Build reactive Web Components, your way.

Combine the simplicity of vanilla Web Standards with the power of modern reactivity.