Form
ha-form renders a complete form from a schema, with the right field for each value, and gives back the data.
The form component turns a schema into a form. The schema is a list of fields. Each field has a name, and says what kind of value it holds. The form picks the right control for each field, lays them out, and reports the data back as one object, keyed by the field names.
You find it all over Home Assistant: in the config flows of integrations, in the options of helpers, in the visual editors of dashboard cards, and in the settings of automations and scripts. The forms of config flows come straight from the integration, as a schema the backend sends. The frontend renders them with the same component.
The demos on this page use readable names, like Host, because the name is the label when there is nothing else. In real code, names are keys like host, and the labels come from translations. See labels and helpers.
Use the form for:
- A form with more than a field or two. You write less code, and your form looks and behaves like every other form in Home Assistant: same spacing, same controls, same validation.
- Settings that are saved together, like the options of a helper or the editor of a card
Don't use the form for:
- A single control that does its own thing, like a switch that applies a setting right away. Use that component directly, like a switch or an input.
- Form error: an error about the whole form, at the top
- Field error or warning: an alert right above the field it is about
- Field: the control for one value, with its label and helper. Most fields are a selector.
- Grid: optional, fields next to each other
- Expandable section: optional, fields under a header that opens and closes
Almost every field in the frontend is a selector: a short description of the value, like "an entity from the light domain" or "a number between 0 and 100". The selector picks the control. These are the same selectors integrations use for the fields of their actions, and blueprints use for their inputs. The selector page lists them all, with a demo of each.
A required field gets a required marker, and an empty value fails validation. A default is the value when people leave the field empty. For a field that is not required, the default shows as the placeholder.
Every field gets a label, and can get help text below it. Both come from the translations. Without a translated label, the form shows the name of the field. That is fine for a quick test, but not for people: names are keys like scan_interval, and they are not translated. The values inside a field, like the options of a select, are translated too.
A grid puts fields next to each other. Use it for short fields that belong together, like a minimum and a maximum. The columns are at least 200 pixels wide, and wrap to new rows when there is no room, so a grid stacks on a phone.
An expandable section groups fields under a header that people can open and close. Use it to keep a long form calm: put the fields most people need at the top, and tuck the rest away, like advanced options or the interactions of a card.
The header text is the title of the section, or, without one, its translated label. A section can have an icon in front of the header. Sections start closed, unless they are set to start open. The help text of the section shows as a description at the top of its content. The card editors use these sections a lot, with an icon, like Content and Interactions.
Some fields only matter when another field has a certain value. Such a field is only shown when its condition matches. Here, the username and password only show once authentication is turned on:
Before selectors, the form had its own field types, and it still renders them. You mostly see them in config flows, where the backend still sends them. In new forms, use selectors.
The full list of types is under other field types, in the code section.
- Changes: every change in any field updates the data of the form, and reports all of it at once.
- Errors: a form error shows at the top, in an error alert. An error for a field shows in an error alert right above that field. A list of errors shows as a bulleted list in the alert.
- Warnings: a warning for a field shows in a warning alert right above that field, when the field has no error. There are no warnings for the whole form.
- Validation: checking the form checks every visible field, and moves focus to the first field that is not valid.
- Hidden fields: a field whose condition does not match is not rendered, and is skipped when the form is checked. Its value no longer counts for the conditions of other fields.
- Disabled: a disabled form disables all its fields. A single field can be disabled too.
The form only sets the spacing between its parts. Each field has the specs of its own component.
| Property | Value | Token |
|---|---|---|
| Space between fields | 24px | None |
| Space between a field alert and its field | 4px | None |
| Grid column gap | 8px | --ha-space-2 |
| Grid row gap | 24px | --ha-space-6 |
| Grid minimum column width | 200px | --form-grid-min-width |
| Grid columns | As many as fit | --form-grid-column-count |
| Expandable section | Outlined expansion panel, 8px corner radius | --ha-border-radius-md |
| Expandable section content padding | 12px | None |
| Space below a section description | 24px | None |
| Section icon color | Secondary text color | --secondary-text-color |
- Make fields optional when that makes sense, and give them a sensible default. A good default helps people, without getting in the way of those who want something else.
- Give every field a proper label, helper text where it helps, and clear validation feedback. See UI text for the words.
The form adds no keys of its own: each field handles its own keyboard input. Tab moves through the fields in the order of the schema. Focusing the form focuses its first field. Checking the form moves focus to the first field that is not valid.
Each field is announced by its own label, as described on the page of its component. Errors and warnings that appear on a form that is already open are read out right away by most screen readers, but they are not linked to their field. The header of an expandable section is announced as a heading.
- The form delegates focus, so focusing
<ha-form>focuses its first field. - The form is not a
<form>element and has no role of its own. - Each field gets its label from
computeLabel, and the field component turns it into the accessible name. See the page of each component for how. - Errors and warnings are alerts with the role
alert. When they appear on a form that is already open, most screen readers read them out right away. They are not connected to their field witharia-describedby. - The header of an expandable section has the role
heading, at level 3 by default.
- Selector: the fields of a form
- Expansion panel: the base of expandable sections
- Alert: how errors and warnings show
- UI text: how to write labels, helpers, and errors
The element is <ha-form>. The schema, the data, and the functions can only be set from code, as properties.
const SCHEMA = [
{ name: "host", required: true, selector: { text: {} } },
{ name: "port", selector: { number: { min: 1, max: 65535, mode: "box" } } },
{ name: "ssl", selector: { boolean: {} } },
] as const satisfies readonly HaFormSchema[];
html`
<ha-form
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA}
.error=${this._errors}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged}
></ha-form>
`;In HTML on this site, the properties go in data-props, as JSON:
<ha-form data-props='{"schema": [{"name": "Host", "selector": {"text": {}}}], "data": {}}'></ha-form>A field looks like this:
{ name: "brightness", selector: { number: { min: 0, max: 100, unit_of_measurement: "%" } } }Every field can also have:
required: the field needs a value. It gets a required marker, and empty values fail validation.default: the value when people leave the field empty. For a field that is not required, the default shows as the placeholder.disabled: shows the field, but people can't change it.description: an object with asuggested_value, which is filled in when the form opens, and asuffix, shown at the end of the older text and number fields.visible: when to show the field. See conditions.context: passes the value of another field to this one. See context.
In real code, pass computeLabel. It gets the schema of each field, and returns the label. Most forms look the name up in the translations:
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass.localize(`ui.panel.config.my_feature.fields.${schema.name}`);computeHelper works the same, and returns the help text below a field. Return nothing for fields that don't need help.
For the values inside a field, like the options of a select, pass localizeValue. Selectors with a translation_key look up their option labels through it.
Set error to show what went wrong. It is an object, keyed by field name. The key base is for errors about the whole form, like a failed connection.
Error messages are shown as they are, unless you pass computeError. Config flows send translation keys as errors, and use computeError to turn them into text.
warning works the same, with a warning instead of an error, and computeWarning to turn it into text. It has no base.
{
name: "",
type: "grid",
schema: [
{ name: "min", selector: { number: { mode: "box" } } },
{ name: "max", selector: { number: { mode: "box" } } },
],
}With an empty name, the fields of the grid write to the data of the form itself. With a name, they write to a nested object under that name, unless you set flatten: true. Set column_min_width to change the minimum column width.
A section has type: "expandable" and its own schema. Add an icon or an iconPath for an icon in front of the header. Set expanded: true to open a section from the start. Like grids, the fields of a section write to a nested object, unless you set flatten: true. Set headingLevel to change the heading level of the header, which is 3 by default.
Set visible on a field to show it only on a condition. It can be true or false, a condition, or a list of conditions that must all match.
{
name: "password",
visible: { field: "use_auth", value: true },
selector: { text: { type: "password" } },
}A condition compares a field with a value. The operator defaults to eq, and can also be not_eq, in, not_in (with a list of values), exists, or not_exists. To combine conditions, use condition: "and", "or", or "not", with a list of conditions.
Some selectors need another value to work: the state selector needs to know which entity to list the states of. Set context on the field, to map a context key of the selector to the name of another field. The state selector, for example, reads the entity from the filter_entity key:
{ name: "state", selector: { state: {} }, context: { filter_entity: "entity" } }string: a text field. Withformatset toemailorurl, it uses that input type. Fields with a name that containspassword,secret, ortokenare masked.integer: a whole number. With bothvalueMinandvalueMax, it shows a slider.float: a decimal number.boolean: a checkbox.select: one option out of a list of value and label pairs.multi_select: several options, as checkboxes, or as a dropdown when the list is long.positive_time_period_dict: a duration in hours, minutes, and seconds.constant: shows the label and a fixedvalue, as text.
The types grid, expandable, optional_actions, and divider are for layout. optional_actions hides its fields behind an add button, so people only add the ones they need. divider draws a line between fields.
These can only be set from code:
schema: the list of fields. Required.data: the values, as an object keyed by field name.hass: the Home Assistant object. Most selectors need it.error: errors to show, keyed by field name, withbasefor the whole form.warning: warnings to show, keyed by field name.computeLabel: a function that gets the schema of a field and the data, and returns the label.computeHelper: a function that gets the schema of a field, and returns the help text.computeError: a function that gets an error and the schema, and returns the text to show.computeWarning: the same, for warnings.localizeValue: a function that gets a translation key, and returns the text. Selectors use it for the labels of their options.context: values passed to every selector in the form, merged with the context of each field.
disabled: disables all fields.narrow: tells the selectors the form is on a narrow screen.
reportValidity(): checks every visible field, focuses the first invalid one, and returnstruewhen the form is valid. Call it before you save.
value-changed: fires on every change in any field.event.detail.valueholds all the data of the form, not only the changed field. The form updates its owndataas well.
--form-grid-column-count: the number of columns in a grid. By default, as many as fit.--form-grid-min-width: the minimum width of a grid column. Defaults to200px.
The wrapper around the fields is exposed as the root part.