Skip to content

Attributes and Properties

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.
  • Attribute-to-property sync happens automatically for declared reactive properties; property-to-attribute sync requires reflect: true.
  • 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.