Skip to content

Attribute Reflection

Reflection is the property-to-attribute direction of Lit's attribute/property sync. This lesson focuses specifically on when and why to enable it.

What 'Reflecting' a Property Means

By default, Lit synchronizes attribute-to-property in one direction only: setting the HTML attribute updates the JS property. Setting reflect: true adds the reverse direction too — whenever the property changes (from any source, including internal component logic), Lit also updates the visible HTML attribute to match.

Reflection exists mainly to keep the DOM 'honest' for external observers: CSS attribute selectors, browser DevTools, testing tools that inspect the DOM, and accessibility tooling that reads ARIA attributes all only see attributes, not internal JS property values.

class ExpandablePanel extends LitElement {
  @property({ type: Boolean, reflect: true }) open = false;

  toggle() {
    this.open = !this.open; // also updates the visible "open" attribute
  }
}
// <expandable-panel open></expandable-panel> in the DOM whenever this.open is true

Because open reflects, a CSS rule like expandable-panel[open] .content { display: block; } can style based on this internal state.

When Reflection Is Worth Enabling

reflect: true  ->  CSS attribute selectors need to see the value
reflect: true  ->  ARIA attributes must be visible for accessibility tools
reflect: true  ->  Tests/DevTools inspect the DOM directly, not JS state
reflect: false ->  Value is only ever read by your own render() logic
  • Reflect boolean 'mode' properties like open, disabled, and checked when CSS needs to style based on them.
  • Reflect ARIA-related properties so assistive technology reading the DOM sees the current state.
  • Skip reflection for properties only your own template logic consumes — it adds attribute-write overhead for no external benefit.
  • Never reflect large objects or arrays; there's no efficient or meaningful string representation for them as an attribute.

Reflection Quick Reference

Typical properties that should or shouldn't reflect.

Property Reflect? Why
disabled (Boolean) Yes CSS and assistive tech both check this attribute
open (Boolean) Yes CSS selectors style based on open/closed state
value (String, form input) Sometimes Reflect only if external CSS/tests need to see it
items (Array) No No meaningful string form; use a property binding instead
onSelect (function) No Functions cannot be represented as attributes at all

Reflection for Accessibility Attributes

Accessibility attributes like aria-expanded and aria-selected are read directly by assistive technology from the DOM, not from your component's internal JavaScript state. If you track 'expanded' as an internal @state() property without reflecting an ARIA attribute, screen readers won't know about it at all — you must explicitly bind or reflect the ARIA attribute yourself.

render() {
  return html`
    <button
      aria-expanded=${this.open ? 'true' : 'false'}
      @click=${() => (this.open = !this.open)}
    >
      Toggle
    </button>
  `;
}

Binding aria-expanded directly in the template is often clearer than relying on property reflection for accessibility attributes specifically.

The Performance Trade-off of Reflection

Writing to an attribute (setAttribute/removeAttribute) is measurably slower than an ordinary JavaScript property write, and triggers attributeChangedCallback and any active MutationObservers watching the element. For properties that change very frequently (like a scroll position or animation progress tracked every frame), avoid reflection entirely and keep the value purely in JS.

Common Mistakes

  • Enabling reflect: true on every property 'just in case', adding unnecessary attribute-write overhead across the board.
  • Assuming reflecting an internal state property automatically updates unrelated ARIA attributes — ARIA bindings must be set explicitly.
  • Reflecting a rapidly-changing property (like a live counter or scroll offset) and causing excessive DOM attribute churn.
  • Forgetting that reflect: true requires attribute to not be false — the two options work together, not independently.

Key Takeaways

  • Reflection is the property-to-attribute direction of Lit's sync; attribute-to-property sync happens by default.
  • Enable reflection when CSS, tests, DevTools, or accessibility tooling need to see the current value in the DOM.
  • Explicitly bind ARIA attributes in your template rather than relying on unrelated property reflection.
  • Avoid reflecting properties that change very frequently, due to the relative cost of attribute writes.

Pro Tip

Before enabling reflect: true, write down the specific external consumer (a CSS selector, a test, a DevTools workflow) that needs to see the attribute. If you can't name one, leave reflection off — it's easy to add later exactly when a real need appears.