Guide & Best Practices

Use @beforesemicolon/web-component when you want native custom elements with a small reactive layer for templates, props, state, scoped styles, lifecycle hooks, and form integration.

Core Rules

  1. Keep render() declarative: Return html, a Node, a string, or nothing. Do not start timers, fetch data, subscribe to global events, or mutate state inside render().
  2. Use observed attributes for public inputs: Put external inputs in static observedAttributes, provide defaults as class fields, and read them from this.props.
  3. Use state for owned UI data: Put private component data in initialState and update it with this.setState().
  4. Pass reactive getters directly when possible: Use ${this.state.count} or ${this.props.label} in templates. Call getters inside calculations, conditions, or event handlers.
  5. Use lifecycle hooks for side effects: Start work in onMount(), return cleanup from onMount() or use onDestroy(), and respond to prop changes in onUpdate().
  6. Prefer ref before querySelector: Use ref for template-owned elements and reach for contentRoot only when dynamic querying is genuinely clearer.
  7. Dispatch public events: Use this.dispatch(name, detail) for component outputs. Consumers listen with normal DOM APIs or Markup on... handlers.

Component Shape

javascript
1import { WebComponent, html, css } from '@beforesemicolon/web-component'2 3class ProductCounter extends WebComponent {4    static observedAttributes = ['label', 'max']5    label = 'Quantity'6    max = 107    initialState = { value: 1 }8 9    stylesheet = css`10        :host {11            display: inline-grid;12            gap: 0.5rem;13        }14    `15 16    setValue = (value) => {17        const next = Math.min(Number(this.props.max()), Math.max(1, value))18        this.setState({ value: next })19        this.dispatch('change', { value: next })20    }21 22    render() {23        return html`24            <label>${this.props.label}</label>25            <button26                type="button"27                onclick="${() => this.setValue(this.state.value() - 1)}"28            >29                -30            </button>31            <output>${this.state.value}</output>32            <button33                type="button"34                onclick="${() => this.setValue(this.state.value() + 1)}"35            >36                +37            </button>38        `39    }40}41 42customElements.define('product-counter', ProductCounter)

Attribute Formatting

Break long custom element usage across lines. It keeps examples readable and avoids horizontal scroll.

html
1<product-counter2    label="Team seats"3    max="25"4    onchange="console.log(event.detail.value)"5></product-counter>

Props vs State

Use props for values controlled by the outside page. Use state for values the component owns.

javascript
1class UserBadge extends WebComponent {2    static observedAttributes = ['name', 'status']3    name = 'Guest'4    status = 'offline'5    initialState = { expanded: false }6 7    render() {8        return html`9            <button10                type="button"11                aria-expanded="${this.state.expanded}"12                onclick="${() =>13                    this.setState(({ expanded }) => ({14                        expanded: !expanded,15                    }))}"16            >17                ${this.props.name} is ${this.props.status}18            </button>19        `20    }21}

Side Effects

Use onMount() for browser subscriptions and return a cleanup function.

javascript
1class ViewportMeter extends WebComponent {2    initialState = { width: window.innerWidth }3 4    onMount() {5        const update = () => {6            this.setState({ width: window.innerWidth })7        }8 9        window.addEventListener('resize', update)10        return () => window.removeEventListener('resize', update)11    }12 13    render() {14        return html`<output>${this.state.width}px</output>`15    }16}

Styling

Use stylesheet for static CSS and css when styles depend on props or state.

javascript
1class ModePanel extends WebComponent {2    static observedAttributes = ['mode']3    mode = 'info'4 5    stylesheet = css`6        :host {7            display: block;8            border-color: ${() => {9                return this.props.mode() === 'danger'10                    ? 'var(--danger)'11                    : 'var(--primary)'12            }};13        }14    `15 16    render() {17        return html`<slot></slot>`18    }19}

Form Controls

Use static formAssociated = true only for controls that should participate in native form submission or validation.

javascript
1class RatingInput extends WebComponent {2    static formAssociated = true3    initialState = { value: 0 }4 5    choose = (value) => {6        this.setState({ value })7        this.internals.setFormValue(String(value))8        this.dispatch('change', { value })9    }10 11    render() {12        return html`13            <button type="button" onclick="${() => this.choose(1)}">1</button>14            <button type="button" onclick="${() => this.choose(2)}">2</button>15            <button type="button" onclick="${() => this.choose(3)}">3</button>16        `17    }18}
edit this doc