HelixOverviewComponentsMapsGraphIntroductionInstallationDeriving a themeSandboxTheme builderVisual testingCortexDocsEngramBuildingTraceBuildingRecallDeclaredServicesInsightsGet in touchSource on GitHub
Essentials
Forms and validationTwenty-two component families are form-associated custom elements. A <form>
treats them as it treats native controls:It collects their values.It resets them.It restores them when you navigate back.It validates them.None of this is simulated.<form><helix-inputlabel="Name"name="name"required></helix-input><helix-selectlabel="Plan"name="plan"><helix-optionvalue="free">Free</helix-option><helix-optionvalue="pro">Pro</helix-option></helix-select><helix-buttontype="submit">Save</helix-button></form>new FormData(form) contains name and plan, and so does the POST body a
server receives.Every control renders its own label<helix-inputhint="We only use this to sign you in."label="Email"name="email"required></helix-input>Helix has no wrapping Field component, because a wrapper cannot name the
control inside it. The wrapper's label would be in one shadow root and the
control in another. We measured this in Chrome 148:An aria-labelledby IDREF cannot see across a shadow boundary.control.ariaLabelledByElements = [labelInTheWrapper] reads back as zero
elements, because the reference is silently dropped.The same assignment with a label in the control's own tree is kept.The wrapper pattern works in React component libraries because they have no
shadow boundary. Copying it into Helix would copy the API without the condition
that makes it work.Every control that takes a label has the same three properties, and it renders
their content itself:PropertyForlabelThe accessible name, and the fallback for the label slothintHelp under the control, while there is nothing wrongfeedbackWhat is wrong, in your own words. It replaces the browser's messageEach property is the fallback for a slot of the same name. To use more than
plain text, put content in the slot:<helix-inputname="email"><spanslot="label">Email <helix-badge>required</helix-badge></span></helix-input>A text node that holds only whitespace replaces a default slot's fallback. You
lose the label, and nothing warns you. We measured this in Chrome 148. A newline
between the tags is enough, so <helix-checkbox label="Accept"> with its
closing tag on the next line renders no label at all. Named slots do not have
this problem, because slot="label" is an attribute and a text node cannot
carry one. This is how the platform behaves, and it applies to every slot
fallback in Helix.ValidationThe platform's own validation works, because these controls are real form
participants:<helix-inputlabel="Email"name="email"requiredtype="email"></helix-input>Submitting an empty form is blocked, and the browser moves focus to the control
and shows its message there. Each control has the validity API of a native
control:checkValidity() and reportValidity()setCustomValidity(message)validity, validationMessage, and willValidate:invalid matches the control as it matches a native one.value-missing-message replaces the browser's message for a required control
that is empty. When it is not set, the browser shows its own message, which is
localized and familiar:<helix-inputlabel="Email"name="email"requiredvalue-missing-message="We need this to send your receipt."></helix-input>feedback says what is wrong, and the form does not submit while it is
set. Use it for a verdict the browser cannot reach, such as one your server
returned. The control shows the message under itself and is invalid until you
clear feedback:<helix-inputfeedback="That name is taken."label="Username"name="username"></helix-input>setCustomValidity() blocks the form in the same way, and the control shows
its message once the reader has edited the field.Focus and Enterfocus() on a control moves focus to the element a reader would reach with
Tab, and pressing a <label> that names the control does the same. In a group,
that is the chosen radio, or the first one when nothing is chosen.Pressing Enter in a single-line field submits its form, as Enter in a native
text field does. These fields do it:Input, NumberInput, and PinInputDateInput, TimeInput, and ColorInputCombobox, while its list is closedThe form's first submit button is pressed, and a helix-button type="submit"
counts as one, for a native field too. A form with no submit button is
submitted when it has only one text field.Radio.Group and Toggle.Group take part in the form for their members. Each
group owns the name, the value, and the validity. A single radio is an
answer, and the group is the control it answers. Checkbox.Group works the
other way. Each checkbox in it already submits itself, so the group reports no
value of its own.Disabled controls and fieldsets<fieldsetdisabled><helix-inputlabel="Name"name="name"></helix-input></fieldset>A Helix control inside a disabled fieldset is disabled too. The platform calls
formDisabledCallback to tell a form-associated element that an ancestor
disabled it, and every Helix control implements that callback. A control inside
a disabled fieldset does not accept input, and it announces itself as turned
off.Style disabled controls with :disabled. Do not use [disabled]:/* Right: matches the attribute and the ancestor fieldset. */helix-input:disabled{… }/* Wrong: misses every control inside a disabled fieldset. */helix-input[disabled]{… }:disabled matches a control that has the attribute. It also matches a control
inside a disabled fieldset, which sets no attribute on anything. With the
attribute selector, the platform still disables a control inside a disabled
fieldset, but the control is drawn as if it were active. It looks broken, and
you will not see the problem in any example without a fieldset. Helix components
follow this rule, and the same reasoning applies to your styles.disabled is mirrored to aria-disabled, and the control stays in the
accessibility tree. A screen reader can still announce that the control exists
and is turned off.Set starting values in a formA value or checked you set is a starting value, and the control then manages
itself, so a form works without any wiring. default-value is the value a reset
returns to. In a form, that is usually what you mean by a starting value:<helix-inputdefault-value="Ada"label="Name"name="name"></helix-input>See State and control.