-
Notifications
You must be signed in to change notification settings - Fork 34
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 Eventwas 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
#privatefields and methods - ✅ DO use TypeScript's
protectedkeyword, because it communicates intent to users - ✅ DO use TypeScript's
overridekeyword, because it can surface more compile-time errors ⚠️ Avoid using the TypeScriptprivatekeyword, because ECMA#privateis 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
constructorconnectedCallbackupdaterenderfirstUpdatedupdateddisconnectedCallback
- 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