Skip to content

Lit Properties

Public reactive properties are a Lit component's external API. This lesson goes deep on declaring them well, choosing sensible defaults, and designing a predictable interface for consumers.

Properties as a Component's Public API

Every @property() you declare (or list in static properties) becomes part of your component's contract with the outside world — other developers will set these from HTML attributes, from JavaScript, or by binding data from a parent Lit component. Treat this list the way you'd treat a function's parameters: minimal, well-named, and well-typed.

Default values matter more than they might seem to. Because Lit calls your constructor before any attributes are processed, setting a default in the constructor (or as a class field initializer) ensures the component behaves sensibly even before a consumer sets anything explicitly.

import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('rating-stars')
export class RatingStars extends LitElement {
  @property({ type: Number }) value = 0;
  @property({ type: Number }) max = 5;
  @property({ type: Boolean, reflect: true }) readonly = false;

  render() {
    return html`<span>${'\u2605'.repeat(this.value)}${'\u2606'.repeat(this.max - this.value)}</span>`;
  }
}

Three well-named, well-defaulted properties (value, max, readonly) form a small, predictable public API for this component.

Declaring Properties: Two Equivalent Syntaxes

// TypeScript decorators
@property({ type: Number }) count = 0;

// Plain JavaScript / static field
static properties = { count: { type: Number } };
constructor() { super(); this.count = 0; }
  • Decorators are more concise and colocate the default value with the declaration.
  • The plain static properties form works in any JavaScript project, with no build-time decorator transform required.
  • Both forms produce identical runtime behavior — pick based on whether your toolchain supports decorators.
  • Property names should be camelCase; Lit derives a lowercase attribute name automatically unless you override it.

Property Declaration Cheat Sheet

The property options you'll use most often, and their defaults.

Option Default Effect
type String Attribute <-> property type conversion
attribute true Whether an attribute is created/observed
reflect false Property changes also update the attribute
hasChanged !== Custom change-detection function
converter based on type Custom attribute<->property conversion logic
noAccessor false Skip generating a reactive getter/setter

Designing a Clean Property API

Good property design favors a small number of well-named, primitive-typed properties over one large configuration object. Consumers of your component benefit from being able to set individual attributes declaratively in HTML, and from clearer autocomplete/type checking in TypeScript.

  • Prefer several focused properties (size, variant, disabled) over one options object property.
  • Use booleans for on/off toggles and reflect them (reflect: true) when CSS or tests need to select on them.
  • Reserve object/array properties (set via attribute: false) for genuinely structured data, like a list of rows.

Setting Default Values Safely

Class field initializers (count = 0) run during construction, before any attribute is applied, and are the simplest way to give a property a sensible default. If you need computed defaults based on other properties, do that computation in the constructor after calling super().

class VolumeControl extends LitElement {
  @property({ type: Number }) volume = 50; // simple default

  constructor() {
    super();
    if (window.matchMedia('(prefers-reduced-motion)').matches) {
      this.animated = false; // computed default based on environment
    }
  }
}

Common Mistakes

  • Exposing one large object property instead of several small, declarative attribute-friendly properties.
  • Forgetting to set a default value, leaving a property undefined until a consumer explicitly sets it.
  • Overusing reflect: true on properties that never need to be visible as attributes, adding unnecessary attribute writes.
  • Not documenting which properties are meant to be public API versus internal implementation detail (use @state() for the latter).

Key Takeaways

  • Reactive properties are your component's public contract — design them deliberately, like function parameters.
  • Both decorator and plain static properties syntax produce identical runtime behavior.
  • Set sensible default values via class field initializers so components behave well before any attribute is set.
  • Prefer several small, typed properties over one large configuration object for better ergonomics and tooling support.

Pro Tip

Before adding a new property, ask whether it's genuinely part of the component's public contract or purely internal — internal-only reactive fields belong in @state(), not @property(), to keep your public API surface small and intentional.