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.
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.
You now understand attribute reflection deeply. Next, learn about property converters for custom type conversion.