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
- Keep
render()declarative: Returnhtml, aNode, a string, or nothing. Do not start timers, fetch data, subscribe to global events, or mutate state insiderender(). - Use observed attributes for public inputs: Put external inputs in
static observedAttributes, provide defaults as class fields, and read them fromthis.props. - Use state for owned UI data: Put private component data in
initialStateand update it withthis.setState(). - Pass reactive getters directly when possible: Use
${this.state.count}or${this.props.label}in templates. Call getters inside calculations, conditions, or event handlers. - Use lifecycle hooks for side effects: Start work in
onMount(), return cleanup fromonMount()or useonDestroy(), and respond to prop changes inonUpdate(). - Prefer
refbeforequerySelector: Usereffor template-owned elements and reach forcontentRootonly when dynamic querying is genuinely clearer. - Dispatch public events: Use
this.dispatch(name, detail)for component outputs. Consumers listen with normal DOM APIs or Markupon...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}