-
Notifications
You must be signed in to change notification settings - Fork 21
Custom Elements API Style Guide
Looking for advice on writing base classes vs subclasses? See Base Classes
- ✅ DO reflect camelCase DOM properties as dash-case attributes
- ✅ DO reflect host attributes that a user would anyways set from the light DOM
- ✅ DO use JSDoc to document all public properties
⚠️ Avoid reflecting host attributes that expose internal state- ❌ DONT reflect host attributes that are strictly for the purpose of styling
// BAD - Don't allow the user to force `mobile` state in a non-mobile viewport @property({ reflect: true, type: Boolean }) mobile = false; // GOOD - Allow user to force `open` state by setting an attr, and provide additional visibility via `MutationObserver` @property({ reflect: true, type: Boolean }) open = false;
- ❌ DONT prefix boolean attributes and properties with
is
, it's implied ⚠️ Avoid using multiple words for public attrs and props// BAD - syntactic noise @property({ reflect: true, type: Boolean, attribute: 'is-open-mode' }) isOpenMode = false; // GOOD - concise user-facing API @property({ reflect: true, type: Boolean }) open = false;
For things like mobile
above, prefer to set a class on a private (i.e. shadow DOM) element:
#screenSize = new ScreenSizeController(this);
render() {
const { mobile } = this.#screenSize;
return html`
<div id="container" class=${classMap({ mobile })}>...</div>
`;
}
- ✅ DO extend
Event
- ✅ DO export your subclassed events, so that users can
instanceof
- ✅ DO set state on the event object instead of
detail
, because each event is its own object - ❌ DONT use
new CustomEvent()
because this is a holdover from beforeclass extends Event
was legalexport class JazzHandsSelectEvent extends Event { declare target: RhJazzHands; constructor( /** The Jazz era selected */ public era: 'ragtime'|'golden'|'smooth' ) { super('select'); } }
When listening for events, be sure to check that the event in an instance of the expected event, to prevent name collisions.
#onSelect(event: Event) {
if (event instanceof JazzHandsSelectEvent) {
this.#swing();
}
}
- ✅ DO write one element class per file
- ✅ DO use ECMAScript
#private
fields and methods - ✅ DO use TypeScript's
protected
keyword, because it communicates intent to users - ✅ DO use TypeScript's
override
keyword, because it can surface more compile-time errors ⚠️ Avoid using the TypeScriptprivate
keyword, because ECMA#private
is now available at the language level Exception: decorated private members, but in that case consider refactoring- ❌ DONT use
_
prefix to simulate privacy, because there are now language features for that
- Statics
- Public reactive properties
- Public fields
- Private reactive state
- Private fields
- Lifecycle methods
constructor
connectedCallback
update
render
firstUpdated
updated
disconnectedCallback
- Private and protected methods
- Public methods
Statics should come first because there's really not much better place for them, and because that convention is generally well accepted in OOP. Public instance fields come next because they represent the element's public HTML and DOM APIs (excepting slots, which are listed in JSDoc). Private state comes next, because they fluently follow from the public fields.
Prefer to list the lifecycle methods in their order of execution, rather than listing render()
first. While the benefits of listing render
- i.e. the element's DOM template - separately are
compelling, syntax-highlighting text editors make visually finding the template easier, while
listing the callbacks in order aids in reasoning around state management and performance.
List private and protected methods before public methods to make the public JavaScript API easier to find - simply navigate to the end of the file.
Moved to JSDoc.
static readonly shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true };
delegatesFocus
will only apply focus to a shadowRoot owned object. Slotted elements will not receive focus through delegation. Use focus()
override to target the slotted element.
focus() {
this.slottedEl.focus();
}
Questions? Please contact [email protected]. Please review our Code of Conduct