Skip to content

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 sync
  • properties-vs-attributes — when to use each, and the type-coercion gotchas
  • custom-events — dispatching and listening to CustomEvent
  • composed-events — crossing shadow-DOM boundaries with composed: true
flowchart LR
  HTML["HTML Markup"] -->|attributes| CE["Custom Element"]
  JS["JavaScript"] -->|properties| CE
  CE -->|events| App["App / Parent"]
Data flow in and out of a custom element
ChannelDirectionCarrier typeSet from
AttributeinstringHTML or setAttribute()
Propertyinany JS valueJavaScript assignment
EventoutEvent / CustomEventdispatchEvent() 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.

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);

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 string

The 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.

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.

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.

What is the primary difference between an attribute and a property on a custom element?
Which direction do events flow in the custom element data model?
What is observedAttributes used for?