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.
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.
You now know how to design a clean properties API. Next, learn about @state() for internal-only reactive fields.