Coding guidelines
How frontend code is written, from TypeScript and Lit to events, dialogs, styling, and text. What reviewers look for in every pull request.
These are the conventions of the frontend repository. Linters enforce some of them. The rest is what reviewers ask for, so following them saves you a round of review.
- TypeScript runs in strict mode. Avoid
any: use the existing Home Assistant types, or write a narrow one. - Use
import typefor imports that are only types. ESLint enforces it. - Shape types after the source that owns the data, like the WebSocket command in Core, not after how one component happens to use it.
- Put a shared type in the data module that owns it, under
src/data/, and import it from there. - Don't use
consolein production code. Show the user an error, or use the existing logging.
- Elements are named with a prefix:
ha-for shared Home Assistant components,hui-for dashboard ("Lovelace") components, anddialog-for dialogs. - Use
@property()for the public API of a component and@state()for its internal state. - The project uses Lit's TypeScript decorators with
useDefineForClassFields: false. Match the existing declarations. Don't use theaccessorkeyword. - Use
@query()for elements in your own template, instead of querying the DOM by hand. - Compute cheap values in
render(). UsewillUpdate()for values that other lifecycle steps need, andmemoizeOneonly when it saves real work. - In lifecycle methods, type the changed properties as
PropertyValues<this>, or plainPropertyValueswhen you read private properties.
Leaf components consume the narrowest Lit context that covers what they read, instead of taking the whole hass object. A component that reads hass rerenders on every state change in the home, even when nothing it shows changed. Containers that own hass and pass it on may keep it.
Import consume from src/common/decorators/consume, not from @lit/context. ESLint enforces that too.
- Fire events with
fireEvent()fromsrc/common/dom/fire_event.ts. Events bubble and cross shadow roots by default. - Declare every custom event and its detail type by extending the
HASSDomEventsinterface, sofireEvent()checks the payload. - Type event handlers with the helpers from the same file:
HASSDomEvent<T>for thedetail,HASSDomCurrentTargetEvent<T>to read the element the listener is on,HASSDomTargetEvent<T>for the element that fired it. UseValueChangedEvent<T>fromsrc/types.tsforvalue-changed.
fireEvent(this, "item-selected", { id: item.id });
declare global {
interface HASSDomEvents {
"item-selected": { id: string };
}
}Build with the shared components in src/components/ before you write your own. Many of them, like ha-button, ha-dialog, ha-switch, and ha-tooltip, wrap Web Awesome components. Don't add new mwc-* Material components: those are being replaced.
- Dialogs: use
ha-dialogwith theDialogMixinfromsrc/dialogs/dialog-mixin.ts. Open a dialog by firingshow-dialogwith the tag, an import function, and the parameters. Put actions inha-dialog-footer, in theprimaryActionandsecondaryActionslots. - Buttons:
ha-buttonhas avariant(brand,neutral,danger,warning,success), anappearance(accent,filled,outlined,plain), and asize. Useappearance="plain"for cancel, andvariant="danger"for destructive actions. - Forms:
ha-formrenders a form from a schema of selectors. UsecomputeLabel,computeHelper, andcomputeErrorto give it translated text. - Messages:
ha-alertwithalert-typeerror,warning,info, orsuccess. - Keyboard shortcuts:
ShortcutManagerfromsrc/common/keyboard/shortcuts.ts.
Each component has a page in the Design area that explains when to use it and how.
- Use the theme variables and design tokens, never hardcoded colors. Use the
--ha-space-*tokens for spacing. - Scope styles to the component. Don't rely on global styles for the inside of a component.
- Design for mobile first, then add what wider screens need.
- Keep layouts right-to-left safe. Prefer logical properties, like
margin-inline-startovermargin-left. - Make touch targets big enough, and give every control clear hover, focus, active, and disabled states.
- Keep motion subtle, and use the
--ha-animation-duration-*tokens. They respect the reduced motion setting.
- Every string people see goes through the translation system. Add it to
src/translations/en.json, and uselocalizein the code. Translations in the frontend shows how. - Use whole sentences with placeholders. Never glue translated fragments together.
- Write in American English, in sentence case, in the active voice. Write "Home Assistant" in full.
- Use "Remove" for something that can be added back, and "Delete" for something that is gone for good. Use "Add" for something that exists, and "Create" for something new.
The UI text section of the Design area has the complete guidance.
- Handle loading, error, unavailable, and missing entities. Check that an entity exists before you read from it.
- Remove event listeners, timers, observers, and subscriptions when the component disconnects.
- Make every control usable with a keyboard and a screen reader.
A pre-commit hook runs ESLint, Prettier, and the Lit analyzer on the files you commit, and fixes what it can. Only edit English text, in src/translations/en.json. The other languages are translated in Lokalise and downloaded at build time, and the hook refuses commits that change those downloaded files.
Keep dependency updates in their own pull request.