Attributes, Properties & Events
Every custom element has two ways to receive data and one way to send it. Attributes arrive as strings in HTML markup. Properties arrive as JavaScript values set by code. Events carry information outward to the rest of the application. Understanding which channel to use — and when — is the key to writing custom elements that feel native and compose cleanly with any framework or plain JavaScript.
This module covers five lessons:
index— attributes vs properties vs events overview (this page)reflecting-attributes— keeping attributes and properties in syncproperties-vs-attributes— when to use each, and the type-coercion gotchascustom-events— dispatching and listening toCustomEventcomposed-events— crossing shadow-DOM boundaries withcomposed: true
Data flow in and out of a custom element
Section titled “Data flow in and out of a custom element”flowchart LR HTML["HTML Markup"] -->|attributes| CE["Custom Element"] JS["JavaScript"] -->|properties| CE CE -->|events| App["App / Parent"]
| Channel | Direction | Carrier type | Set from |
|---|---|---|---|
| Attribute | in | string | HTML or setAttribute() |
| Property | in | any JS value | JavaScript assignment |
| Event | out | Event / CustomEvent | dispatchEvent() inside element |
Attributes are the natural interface from HTML — they are what you write in markup and what HTML parsers understand. Properties are the JavaScript interface — they can hold any type: numbers, booleans, arrays, objects. Events are the only sanctioned way for an element to communicate outward; they keep the element decoupled from whoever is using it.
Attributes are always strings
Section titled “Attributes are always strings”When the browser parses <attr-demo label="42">, the value "42" is a string. If your element needs a number, your code must convert it. This is one of the most common sources of subtle bugs when moving from framework components (where props can be any type) to custom elements.
class AttrDemo extends HTMLElement { static get observedAttributes() { return ['count']; }
attributeChangedCallback(name, oldVal, newVal) { // newVal is always a string — coerce explicitly var count = Number(newVal); this.textContent = 'Count: ' + count; }}customElements.define('attr-demo', AttrDemo);Properties bypass the string constraint
Section titled “Properties bypass the string constraint”Setting a property directly in JavaScript lets you pass rich values without serialisation:
var el = document.querySelector('my-chart');el.data = [{ x: 1, y: 2 }, { x: 3, y: 4 }]; // object, not a stringThe element’s setter receives the value as-is. No string conversion happens unless you write it yourself. This is why complex configuration — arrays, objects, functions — belongs on properties, not attributes.
Events carry data out
Section titled “Events carry data out”An element should never reach up into the DOM to mutate a parent. Instead it dispatches an event and lets the parent decide what to do:
this.dispatchEvent(new CustomEvent('demo-click', { bubbles: true, composed: true, detail: { label: this.getAttribute('label') }}));The parent listens with addEventListener('demo-click', handler). The element and the parent are fully decoupled.
Live demo
Section titled “Live demo”The element below reads its label attribute and displays it in a styled box. Click the box to dispatch a demo-click event — the page catches it and shows a log entry.