Skip to content

Property Options

@property() accepts a configuration object with several options controlling exactly how a reactive property behaves. This lesson covers every option in detail.

The Full Property Options Object

Every option passed to @property({ ... }) (or listed per-property in static properties) controls one specific aspect of how that property interacts with attributes, change detection, or the generated accessor. Most properties only need type, but knowing every option helps you solve less common cases correctly instead of working around them.

These same options apply whether you use the TypeScript decorator syntax or the plain JavaScript static properties object — they're just two different syntaxes for configuring the exact same underlying system.

@property({
  type: Number,       // attribute <-> property conversion
  attribute: 'max-value', // custom attribute name
  reflect: true,      // property changes update the attribute too
  hasChanged(newVal, oldVal) {
    return Math.abs(newVal - oldVal) > 0.001; // custom change detection
  },
})
maxValue = 100;

Four options working together: type conversion, a renamed attribute, reflection, and custom float-tolerant change detection.

All Available Options

type          -> String | Number | Boolean | Object | Array | custom converter
attribute     -> true | false | 'custom-name'
reflect       -> true | false
hasChanged    -> (newVal, oldVal) => boolean
converter     -> { fromAttribute, toAttribute }
state         -> true | false (equivalent to @state())
noAccessor    -> true | false
  • type also accepts a full custom converter function/object for values that don't fit the built-ins.
  • attribute: false combined with reflect: true is invalid/meaningless — reflection requires an attribute to reflect to.
  • noAccessor: true skips Lit's generated getter/setter, useful only if you're implementing custom accessors yourself.
  • state: true is exactly what the @state() decorator sets for you automatically.

Property Options Reference

Every option, its type, and what it controls.

Option Type Controls
type Constructor or converter object Attribute string <-> property value conversion
attribute boolean | string Whether/what attribute is observed
reflect boolean Property change writes back to the attribute
hasChanged (new, old) => boolean Custom equality check for change detection
converter { fromAttribute, toAttribute } Fully custom serialization logic
state boolean Marks the property internal (no attribute)
noAccessor boolean Skips generating a reactive accessor

Custom Attribute Names

Because HTML attributes are conventionally lowercase and kebab-case, while JavaScript properties are camelCase, you'll sometimes want an attribute name that doesn't directly match the property name — for example, aligning with an existing design system's naming, or avoiding an awkward auto-lowercased name.

@property({ type: Boolean, attribute: 'is-open' })
isOpen = false;

// <my-el is-open></my-el>  <->  element.isOpen === true

Full Custom Converters

For value types the built-in converters don't handle well — a comma-separated list, a custom serialized format — pass a converter object with fromAttribute and toAttribute functions instead of a type constructor.

@property({
  converter: {
    fromAttribute: (value) => value?.split(',').map((s) => s.trim()) ?? [],
    toAttribute: (value) => value.join(', '),
  },
  reflect: true,
})
tags = [];

// <my-el tags="red, green, blue"></my-el> -> element.tags === ['red', 'green', 'blue']

Custom converters give full control when the built-in String/Number/Boolean/Object/Array types don't fit your data shape.

Common Mistakes

  • Setting reflect: true alongside attribute: false, which has no effect since there's no attribute to reflect to.
  • Writing a hasChanged function that forgets to handle the initial undefined old value, throwing on first render.
  • Reaching for a full custom converter when a built-in type (like Array with JSON) would already work fine.
  • Using noAccessor: true without implementing your own getter/setter, silently breaking reactivity for that property.

Key Takeaways

  • @property() options give fine-grained control over attribute naming, reflection, change detection, and conversion.
  • attribute: false and reflect: true are mutually pointless together — reflection needs an attribute.
  • Custom converters (fromAttribute/toAttribute) handle value shapes the built-in types don't cover.
  • state: true is exactly what @state() configures automatically — the two are equivalent under the hood.

Pro Tip

Reach for the built-in type converters first, and only write a custom converter when you've confirmed the built-in Object/Array JSON-based conversion genuinely doesn't fit — it covers more cases than developers coming from other frameworks often expect.