HTML attributes and JavaScript properties look similar but behave very differently. This lesson clarifies exactly how Lit keeps them in sync for reactive properties.
Attributes Are Strings, Properties Are Any Value
An HTML attribute is always a string (or absent). A JavaScript property can be any type — a number, a boolean, an object, an array, a function. When you declare a reactive property with type: Number or type: Boolean, Lit automatically converts incoming attribute strings into the right JS type, and vice versa.
This conversion is one-directional by default: setting the attribute updates the property, but setting the property does *not* automatically update the attribute back — unless you explicitly opt in with reflect: true.
class VolumeSlider extends LitElement {
static properties = {
value: { type: Number }, // attribute "value" <-> property .value (Number)
muted: { type: Boolean, reflect: true }, // also reflects back to the attribute
};
}
// In HTML:
// <volume-slider value="75" muted></volume-slider>
// element.value === 75 (a Number, not "75")
// element.muted === true
type: Number parses the attribute string into a real number; type: Boolean treats the attribute's mere presence as true.
Attribute <-> Property Conversion Rules
type: String -> attribute value used as-is
type: Number -> Number(attributeValue)
type: Boolean -> present = true, absent = false
type: Object -> JSON.parse(attributeValue)
type: Array -> JSON.parse(attributeValue)
Boolean attributes follow the HTML convention: presence means true, not the string value "true" or "false".
Object/Array types use JSON parsing, so malformed attribute JSON will throw — prefer setting these from JavaScript as properties instead.
You can rename the observed attribute with attribute: 'my-attr' when the property name isn't valid or desired as an attribute name.
Set attribute: false to make a property purely a JS API with no corresponding HTML attribute at all.
Attributes vs Properties Cheat Sheet
Key differences to keep straight when debugging sync issues.
Aspect
HTML Attribute
JS Property
Type
Always a string
Any JavaScript value
Set from HTML
<el attr="1">
Not directly — read by Lit
Set from JS
el.setAttribute('attr', '1')
el.prop = 1
Read from JS
el.getAttribute('attr')
el.prop
Updates the other?
Yes, by default (attr -> prop)
Only if reflect: true (prop -> attr)
Disable syncing
attribute: false on the property
N/A
Why Property-to-Attribute Reflection Is Opt-In
Reflecting every property change back to an attribute sounds convenient, but it has a real cost: writing to attributes is comparatively slow, and for properties like large arrays or objects it's often meaningless (there's no sensible string representation). Lit defaults to reflect: false so you only pay that cost when you actually need the attribute to stay visible — commonly for CSS selectors like [aria-expanded] or for styling hooks like [disabled].
Enable reflect: true for properties that CSS or external tooling needs to select on.
Leave reflect: false (the default) for properties only your own template logic reads.
Never reflect large objects/arrays — there's no efficient, meaningful string form for them.
Renaming or Disabling the Attribute
Property names are camelCase by JavaScript convention, but HTML attributes are conventionally lowercase/kebab-case. Lit lets you control the mapping explicitly.
static properties = {
isOpen: { type: Boolean, attribute: 'is-open' }, // custom attribute name
internalCache: { attribute: false }, // no attribute at all
};
attribute: false is a good default for properties that only make sense set from JavaScript, such as objects or callback functions.
Common Mistakes
Setting disabled="false" in HTML and expecting the boolean property to be false — the attribute's mere presence makes it true.
Expecting a property set from JavaScript to automatically update the visible HTML attribute without reflect: true.
Passing complex objects/arrays through HTML attributes as JSON strings instead of setting them directly as properties from JavaScript.
Forgetting that attribute names are case-insensitive and typically kebab-case, while property names are camelCase.
Key Takeaways
Attributes are always strings; properties can be any JavaScript value.
Boolean attributes follow HTML's presence-based convention, not string "true"/"false" values.
Prefer setting complex values (objects, arrays, functions) as JS properties, not JSON-encoded attributes.
Pro Tip
When integrating a Lit component into a non-Lit page (plain HTML, or a CMS template), remember only attributes can be set declaratively in markup — anything beyond strings/booleans/numbers needs a small script that sets the property directly.
You now understand exactly how attributes and properties relate. Next, dive deeper into template expressions and binding syntax.