Props

Props are the external inputs to your component. In WebComponent, props are bound to HTML attributes, providing a reactive and seamless way to receive data from parents or external consumers.

Declaring Props

To declare reactive props, add their attribute names to the static observedAttributes array. WebComponent automatically watches these attributes and sets up reactive getters/setters on the element instance.

javascript
1import { WebComponent } from '@beforesemicolon/web-component'2 3class MyCard extends WebComponent {4    static observedAttributes = ['card-title', 'is-open']5}

Case Conversion

HTML attributes are case-insensitive and conventionally written in kebab-case (e.g., card-title). WebComponent automatically converts these kebab-case attributes to camelCase properties on the class instance (e.g., cardTitle).

For example:

Default Values

You can specify default values for your props by defining them as class fields. If the corresponding attribute is not present on the element when it mounts, the default value will be used.

javascript
1import { WebComponent } from '@beforesemicolon/web-component'2 3class MyCard extends WebComponent {4    static observedAttributes = ['card-title', 'is-open']5 6    // Default values7    cardTitle = 'Untitled'8    isOpen = false9}

Reading Props in Templates

Inside the component class, you can access the reactive getter functions via this.props.

Since these props are reactive signal getters, they should be invoked as functions to read their current value (e.g., this.props.cardTitle()). When used in Markup templates, passing the getter function directly allows the template to automatically subscribe to updates:

javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class MyCard extends WebComponent {4    static observedAttributes = ['card-title', 'is-open']5 6    cardTitle = 'Untitled'7    isOpen = false8 9    render() {10        return html`11            <div12                class="card ${() => (this.props.isOpen() ? 'open' : 'closed')}"13            >14                <h3>${this.props.cardTitle}</h3>15                <slot></slot>16            </div>17        `18    }19}

[!NOTE] Passing this.props.cardTitle directly as an expression in the template (without calling it) works because Markup is designed to resolve getters reactively. If you are doing calculations or conditional rendering inside a function callback, make sure to call it as a function: this.props.isOpen().

Attribute Parsing & Serialization

WebComponent does not serialize and deserialize prop values as a component data protocol.

There are two different paths:

  1. String attributes: Literal HTML attributes and setAttribute() values are browser strings. WebComponent passes those strings through its internal JSON parser, so primitive-looking values can become primitives and JSON-looking values can become arrays or objects.
  2. Property/reference values: Values assigned through the component property, including values passed from Markup expressions, are kept as JavaScript values. Objects, arrays, functions, and other non-primitives are passed by reference and are not written back to HTML attributes.
  3. Primitive property values: When you assign a primitive value directly to the component property, WebComponent mirrors it back to the matching kebab-case attribute.
html
1<my-card2    card-title="My Project"3    count="3"4    enabled="true"5    tags='["ui", "release"]'6></my-card>

In this HTML-only example, WebComponent receives strings from the browser and parses JSON-compatible values before updating this.props:

When a parent template passes a non-primitive value, keep it as a reference instead of stringifying it:

javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3const project = {4    title: 'Launch',5    owner: 'Design',6}7 8class ProjectShell extends WebComponent {9    render() {10        return html`<project-card project=${project}></project-card>`11    }12}

The project prop is the original object reference. It is not converted to JSON, placed in the DOM as an attribute string, and parsed again later.

Imperative Updates

You can also read and write props imperatively directly on the element instance. Primitive values sync back to HTML attributes. Non-primitive values update the reactive prop directly and stay as references.

javascript
1const card = document.querySelector('my-card')2 3// Read value imperatively4console.log(card.cardTitle) // Logs "My Project"5 6// Update value imperatively7card.cardTitle = 'Updated Title' // Syncs to attribute: card-title="Updated Title"
javascript
1const project = {2    title: 'Launch',3    tasks: ['Design', 'Build'],4}5 6card.project = project7 8console.log(card.props.project() === project) // true9console.log(card.hasAttribute('project')) // false

Practical Component Example

javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class PlanCard extends WebComponent {4    static observedAttributes = ['name', 'price', 'featured']5    name = 'Starter'6    price = 197    featured = false8 9    render() {10        return html`11            <article12                class="${() =>13                    this.props.featured() ? 'featured' : 'standard'}"14            >15                <h3>${this.props.name}</h3>16                <strong>$${this.props.price}</strong>17                <slot></slot>18            </article>19        `20    }21}22 23customElements.define('plan-card', PlanCard)
html
1<plan-card name="Team" price="49" featured>2    Includes shared projects and priority support.3</plan-card>
edit this doc