{
  "name": "form-controls",
  "title": "Form Controls",
  "description": "Using eurodata-ui form controls.",
  "docs": "https://test-apps.eurodata.de/a/eurodata-ui/docs/form-controls/",
  "sections": [
    {
      "heading": "Form Controls",
      "markdown": "eurodata-ui form controls are form-associated custom elements, meaning they will submit with forms just like native `<form>` controls. They also support constraint validation, which is the platform's version of client-side form validation."
    },
    {
      "heading": "Constraint Validation",
      "markdown": "eurodata-ui form controls participate in the browser's [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) the same way native `<input>`, `<textarea>`, and `<select>` do. You define the validation rules with attributes — `required`, `pattern`, `minlength`, `maxlength`, `min`, `max`, etc. — and the browser evaluates them continuously. A control fails validation as soon as its value doesn't satisfy the rules; what _changes_ with user interaction is only whether the error UI surfaces. Submission is blocked while any control in the form fails validation.\n\neurodata-ui implements many of the same attributes as native form controls, but the exact set varies per component. See each component's reference for the list of supported attributes.\n\nIf you don't want to use client-side validation, you can suppress this behavior by adding `novalidate` to the surrounding `<form>` element. And if this syntax looks unfamiliar, don't worry — most of what you're learning on this page is platform knowledge that applies to native form controls, too.\n\nClient-side validation can be used to improve the UX of forms, but it is not a replacement for server-side validation. You should always validate and sanitize user input on the server!\n\n### Required Fields\n\nTo make a field required, use the `required` attribute. Required fields will automatically receive an asterisk after their labels — specifically, when the label is set via the `label` attribute or the named `label` slot. The form will not be submitted if a required field is incomplete.\n\n```html\n<form class=\"input-validation-required\">\n  <ed-input name=\"name\" label=\"Name\" required></ed-input>\n  <br />\n  <ed-select label=\"Favorite Animal\" with-clear required>\n    <ed-option value=\"birds\">Birds</ed-option>\n    <ed-option value=\"cats\">Cats</ed-option>\n    <ed-option value=\"dogs\">Dogs</ed-option>\n    <ed-option value=\"other\">Other</ed-option>\n  </ed-select>\n  <br />\n  <ed-textarea name=\"comment\" label=\"Comment\" required></ed-textarea>\n  <br />\n  <ed-slider></ed-slider>\n  <br />\n  <ed-checkbox required>Check me before submitting</ed-checkbox>\n  <br /><br />\n  <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n  <ed-button type=\"reset\" appearance=\"outlined\" variant=\"brand\">Reset</ed-button>\n</form>\n\n<script type=\"module\">\n  const form = document.querySelector('.input-validation-required');\n\n  // Wait for controls to be defined before attaching form listeners\n  await Promise.all([\n    customElements.whenDefined('ed-button'),\n    customElements.whenDefined('ed-checkbox'),\n    customElements.whenDefined('ed-input'),\n    customElements.whenDefined('ed-option'),\n    customElements.whenDefined('ed-select'),\n    customElements.whenDefined('ed-textarea'),\n  ]).then(() => {\n    form.addEventListener('submit', event => {\n      event.preventDefault();\n      alert('All fields are valid!');\n    });\n  });\n</script>\n```\n\nTry filling in and resetting the form to see how validation state and the values are restored together. See [Form Reset](https://test-apps.eurodata.de/a/eurodata-ui/docs/form-controls/#form-reset) for the full list of what reset touches and what it leaves alone.\n\n### Input Patterns\n\nTo restrict a value to a specific [pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern), use the `pattern` attribute. This example only allows the letters A-Z, so the form will not submit if a number or symbol is entered. This only works with `<ed-input>` elements.\n\n```html\n<form class=\"input-validation-pattern\">\n  <ed-input name=\"letters\" required label=\"Letters\" pattern=\"[A-Za-z]+\"></ed-input>\n  <br />\n  <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n  <ed-button appearance=\"outlined\" type=\"reset\" variant=\"neutral\">Reset</ed-button>\n</form>\n\n<script type=\"module\">\n  const form = document.querySelector('.input-validation-pattern');\n\n  // Wait for controls to be defined before attaching form listeners\n  await Promise.all([customElements.whenDefined('ed-button'), customElements.whenDefined('ed-input')]).then(() => {\n    form.addEventListener('submit', event => {\n      event.preventDefault();\n      alert('All fields are valid!');\n    });\n  });\n</script>\n```\n\n### Input Types\n\nSome input types will automatically trigger constraints, such as `email` and `url`.\n\n```html\n<form class=\"input-validation-type\">\n  <ed-input type=\"email\" label=\"Email\" placeholder=\"you@example.com\" required></ed-input>\n  <br />\n  <ed-input type=\"url\" label=\"URL\" placeholder=\"https://example.com/\" required></ed-input>\n  <br />\n  <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n  <ed-button appearance=\"outlined\" type=\"reset\" variant=\"neutral\">Reset</ed-button>\n</form>\n\n<script type=\"module\">\n  const form = document.querySelector('.input-validation-type');\n\n  // Wait for controls to be defined before attaching form listeners\n  await Promise.all([customElements.whenDefined('ed-button'), customElements.whenDefined('ed-input')]).then(() => {\n    form.addEventListener('submit', event => {\n      event.preventDefault();\n      alert('All fields are valid!');\n    });\n  });\n</script>\n```\n\n### Supported attributes by control\n\nThe exact set of constraint attributes varies by control. The table below shows which attributes each form control honors:\n\n| Control | required | pattern | min/max | step | minlength/maxlength |\n| --- | --- | --- | --- | --- | --- |\n| `ed-input` | ✓ | ✓ | ✓ | ✓ | ✓ |\n| `ed-textarea` | ✓ | — | — | — | ✓ |\n| `ed-select` | ✓ | — | — | — | — |\n| `ed-combobox` | ✓ | — | — | — | — |\n| `ed-number-input` | ✓ | — | ✓ | ✓ | — |\n| `ed-checkbox` | ✓ | — | — | — | — |\n| `ed-switch` | ✓ | — | — | — | — |\n| `ed-radio-group` | ✓ | — | — | — | — |\n| `ed-slider` | — | — | ✓ | ✓ | — |\n\nFor controls with `required`, the field must be non-empty (or, for `ed-checkbox` / `ed-switch`, checked) for the form to submit. For numeric controls, `min`/`max` define the inclusive bounds and `step` defines the allowed increment. For text-like controls, `minlength`/`maxlength` define character bounds; `pattern` defines a regex against the value.\n\n### Where the message appears\n\nA form control renders two separate lines below itself, in this order:\n\n1.  **The message** (`::part(message)`) — the state the control is in: a validation error, a consumer `error`, a `warning`, or the AI note. One at a time, in that priority.\n2.  **The hint** (`::part(hint)`) — your own explanation, from the `hint` attribute or the `hint` slot.\n\nThey are independent. A control showing an error still shows its hint, so the instruction that explains how to fill the field in stays readable at the moment it is needed. Both are named in the control's `aria-describedby`, hint first, so assistive technology reads the explanation before the complaint.\n\nThe message element is always in the DOM, even with nothing to say — it is the control's live region, and a live region has to exist _before_ its first message for that message to be announced. While empty it has no height and no accessible content.\n\nForm controls collapse the whitespace inside their own shadow DOM (`white-space-collapse: collapse`), so an inherited `white-space: pre-wrap` from a surrounding page does not turn a control's internal line breaks into blank lines. Wrapping is untouched: a `white-space: nowrap` on a table or grid cell still keeps a message on one line. To preserve line breaks in your own slotted content, set `white-space` on the slotted element itself rather than on an ancestor.\n\nIf you place a form control in a CSS grid and position `::part(hint)` by hand, position `::part(message)` too — it is a grid item like any other. See [Customizing Label Position](https://test-apps.eurodata.de/a/eurodata-ui/docs/components/input#customizing-label-position).\n\n### Custom Error Messages\n\nThere are two ways to set custom error messages on form controls. Both display the message inline below the control and block form submission.\n\nThe two APIs are layered, not independent: setting the `error` property internally calls `setCustomValidity()` with the same message. They share the underlying validation message — the difference is _when the message becomes visible_. `setCustomValidity()` follows the platform's progressive-disclosure semantic (visible only after user interaction). The `error` property bypasses that gate and surfaces the message immediately. Use `error` for most cases; reach for `setCustomValidity()` directly when you want the after-interaction behavior.\n\n#### Using setCustomValidity()\n\nThe `setCustomValidity()` method integrates with the browser's constraint validation API. The message only appears after the user has interacted with the control (e.g. after a form submission attempt or when the user blurs the field). This is the same progressive disclosure behavior as native HTML validation attributes like `required` or `pattern`.\n\n**Use this when:** you want the error to behave like built-in constraint validation — hidden initially and revealed only after interaction.\n\n```html\n<form class=\"input-validation-custom-validity\">\n  <ed-input label=\"Type eurodata\" required></ed-input>\n  <br />\n  <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n  <ed-button appearance=\"outlined\" type=\"reset\" variant=\"neutral\">Reset</ed-button>\n</form>\n\n<script type=\"module\">\n  const form = document.querySelector('.input-validation-custom-validity');\n  const input = form.querySelector('ed-input');\n\n  await Promise.all([customElements.whenDefined('ed-button'), customElements.whenDefined('ed-input')]).then(() => {\n    form.addEventListener('submit', event => {\n      event.preventDefault();\n      alert('All fields are valid!');\n    });\n\n    input.addEventListener('input', () => {\n      if (input.value === 'eurodata') {\n        input.setCustomValidity('');\n      } else {\n        input.setCustomValidity(\"Hey, you're supposed to type 'eurodata' before submitting this!\");\n      }\n    });\n  });\n</script>\n```\n\n#### Using the error property\n\nThe `error` property shows an error immediately — the message is visible without requiring user interaction first. Unlike `setCustomValidity()`, the error state is active as soon as the property is set.\n\n**Use this when:** you want to show an error immediately, e.g. after server-side validation or when the user types invalid input.\n\n```html\n<form class=\"input-validation-custom\">\n  <ed-input label=\"Input\" required error=\"Immediately showing error example\"></ed-input>\n  <br />\n  <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n  <ed-button appearance=\"outlined\" type=\"reset\" variant=\"neutral\">Reset</ed-button>\n</form>\n```\n\nWhen using the `error` attribute the form-control stays invalid until you remove the `error` attribute manually again. It is not automatically cleared when an user types in valid values - you have to take care of this.\n\n#### Error styling without a message\n\nSet `error` (or `warning`) as a bare attribute — or to an empty string — to apply the styled border and icon without rendering any message text. The hint stays where it is. This is useful in narrow layouts (data-grid cells, compact tables, toolbars) where vertical space for a message is not available, or when the error is surfaced elsewhere (parent widget, grouped error summary).\n\nAn empty `error` still blocks form submission — the validation message falls back to a localized generic term (\"Error\" in English, \"Fehler\" in German, etc.). An empty `warning` produces only the styling.\n\n```html\n<ed-input label=\"Amount\" error></ed-input>\n<br />\n<ed-input label=\"Quantity\" warning></ed-input>\n```\n\nTo clear the state in JavaScript, set the property to `null`:\n\n```js\ninput.error = null; // clears error\ninput.error = ''; // error styling, no message\ninput.error = 'Invalid value'; // styling + message\n```\n\nYou should never show the error or warning state without a message describing what the error/warning actually is. This feature only exists to allow you to make your own message visualization like shown in the next section.\n\n#### Showing the message in a tooltip\n\nWhen you need the error styling to stay compact but still want consumers to see the message, anchor an `<ed-tooltip>` to the control via its `for` attribute. This composition pattern gives you control over placement, trigger, and stacking context:\n\n```html\n<ed-input id=\"email-invalid\" type=\"email\" label=\"Email\" error></ed-input>\n<ed-tooltip for=\"email-invalid\" placement=\"bottom\">Email format is invalid</ed-tooltip>\n```\n\nThis pattern pairs naturally with the empty-error feature: the border and icon communicate _something is wrong_, and the tooltip reveals _what_ on hover or focus. For assistive-technology users, add `aria-describedby=\"<tooltip-id>\"` to the control (and an `id` on the tooltip) so the message is announced alongside the `aria-invalid` state.\n\nYou should always use inline-message over tooltips since it is the better UX. Multi-column forms that would \"jump\" when a form-control is invalid need to have a different layout. Not column based, it must be row based.\n\n```html\n<form class=\"ed-stack\">\n  <div class=\"ed-grid\">\n    <ed-input name=\"first\" label=\"First name\" required></ed-input>\n    <ed-input name=\"last\" label=\"Last name\" required></ed-input>\n  </div>\n  <div class=\"ed-flank:end\" style=\"--ed-flank-size: 6ch; align-items: flex-start;\">\n    <ed-input name=\"street\" label=\"Street\" required></ed-input>\n    <ed-input name=\"house\" label=\"No.\" required></ed-input>\n  </div>\n  <div class=\"ed-flank\" style=\"--ed-flank-size: 10ch; align-items: flex-start;\">\n    <ed-input name=\"zip\" label=\"ZIP\" required></ed-input>\n    <ed-input name=\"city\" label=\"City\" required></ed-input>\n  </div>\n  <ed-textarea name=\"bio\" label=\"About you\" maxlength=\"120\"></ed-textarea>\n  <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n</form>\n```\n\nInside the form's outer `ed-stack`, each row picks the utility that matches its column proportions:\n\n-   **`ed-grid`** for equal-width pairs (first / last name). Its grid default `align-items: stretch` keeps siblings aligned when one grows.\n-   **`ed-flank`** (use the `:end` modifier to flip which side is narrow) for asymmetric two-input rows. `--ed-flank-size` fixes the narrow side; the wide side flexes.\n-   **`ed-cluster`** with explicit `flex` per child is the right tool for rows with more than two inputs and free-form proportions (not shown above).\n\n`ed-flank` and `ed-cluster` default to `align-items: center`, which is fine for icon-text or button rows but causes form controls to float up and down when one input grows. For form rows that may show errors or hints, override to `align-items: flex-start` (as on the address rows above) so the input boxes stay top-aligned and growth extends downward predictably."
    },
    {
      "heading": "Progressive Disclosure",
      "markdown": "eurodata-ui form controls follow the platform's progressive-disclosure pattern: validation messages and the `user-*` validity states only surface after the user has interacted with the control. The underlying signal is an internal `hasInteracted` flag exposed on each control as `el.hasInteracted`.\n\nUntil `hasInteracted` flips:\n\n-   `:state(invalid)` / `:state(valid)` reflect the constraint validity but the error UI stays hidden (no message text, no `aria-invalid`).\n-   `:state(user-invalid)` / `:state(user-valid)` are inactive.\n-   `:state(dirty)` is inactive.\n-   `:state(ai)` and the AI note stay visible (assuming `ai` is set). The note is a message, so it appears above the hint rather than in place of it — a field the system pre-filled still shows how to fill it in.\n\nOnce `hasInteracted` is true:\n\n-   Constraint-validation messages surface in the message area and `aria-invalid` is set on the inner control.\n-   `:state(user-invalid)` / `:state(user-valid)` activate.\n-   `:state(dirty)` activates.\n-   `:state(ai)` and the AI note are suppressed. The hint is unaffected and stays where it is.\n\nEach component decides what counts as \"interaction\" — typically a combination of the `input`, `blur`, `click`, or `keydown` events. As a rule of thumb, text-like controls (`ed-input`, `ed-textarea`, `ed-select`, `ed-combobox`, `ed-number-input`) require both a value change _and_ a blur, while toggles and selections (`ed-checkbox`, `ed-switch`, `ed-radio`, `ed-slider`) flip on a single value-change event. Form submission also counts as interaction — when the form is submitted and the control is invalid, the `invalid` event fires and `hasInteracted` flips. On submission with invalid controls, focus also moves to the first invalid control in document order — the same behavior as `reportValidity()` and as native form controls.\n\n`hasInteracted` is reset to `false` by [`formResetCallback`](https://test-apps.eurodata.de/a/eurodata-ui/docs/form-controls/#form-reset)."
    },
    {
      "heading": "Validation Methods",
      "markdown": "Form controls expose two standard methods for checking validity programmatically. They mirror the platform's [`HTMLInputElement.checkValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity) and [`HTMLInputElement.reportValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity) and behave like the corresponding methods on native `<input>`/`<textarea>`/`<select>`:\n\n-   **`checkValidity()`** — returns `true` if the control satisfies all validation rules, `false` otherwise. Fires an `invalid` event when invalid, but does **not** surface the validation UI (no message, no focus). Use it to query validity silently.\n-   **`reportValidity()`** — same return value and `invalid` event, but additionally surfaces the validation UI: messages appear in the message area, `aria-invalid` is set, and focus moves to the first invalid control in the form. Use it to _trigger_ the validation UI on demand.\n\nBoth methods are also defined on the surrounding `<form>` element by the browser. eurodata-ui form controls register as `formAssociated`, so they appear in `form.elements` and participate in the form's static-validation algorithm.\n\n### The invalid event\n\nWhenever a control becomes invalid (whether via `checkValidity()`, `reportValidity()`, or form submission), the browser dispatches an `invalid` event on it. Listen for it to react to validation failures:\n\n```js\nconst input = document.querySelector('ed-input');\n\ninput.addEventListener('invalid', event => {\n  // eurodata-ui controls suppress the browser's default validation popup via\n  // event.preventDefault() in their own handler before this listener runs.\n  console.log(input.validationMessage);\n});\n```\n\n### Caveat: form-level vs element-level\n\nElement-level `checkValidity()` is silent — eurodata-ui flags the call internally and the `invalid` event handler skips the focus/`hasInteracted` flip:\n\n```js\nconst input = document.querySelector('ed-input');\n\n// Silent — does not move focus, does not flip hasInteracted.\ninput.checkValidity();\n```\n\n`form.checkValidity()` is **not** silent on eurodata-ui controls. The browser-implemented form-level validation dispatches `invalid` events directly on each child without going through the child's `checkValidity()` method, so each child treats the event the same as one fired by `reportValidity()` — focus moves to the first invalid control and `hasInteracted` flips. To do a silent form-wide check, iterate the form's controls yourself:\n\n```js\nconst form = document.querySelector('form');\n\nconst allValid = [...form.elements].every(el =>\n  typeof el.checkValidity !== 'function' || el.checkValidity()\n);\n```\n\n### React\n\neurodata-ui form controls participate in the parent `<form>`'s validation pipeline natively — no extra wiring is required. Submitting the form or calling `form.reportValidity()` works the same as with native inputs.\n\nFor element-level checks, use a ref:\n\n```tsx\nimport { useRef } from 'react';\nimport type EdInputElement from '@eurodata/eurodata-ui/dist/components/input/input.js';\n\nfunction MyComponent() {\n  const inputRef = useRef<EdInputElement>(null);\n\n  return (\n    <>\n      <ed-input ref={inputRef} required label=\"Email\" type=\"email\" />\n      <ed-button onClick={() => inputRef.current?.reportValidity()}>\n        Check\n      </ed-button>\n    </>\n  );\n}\n```\n\nFor form submission, the standard React form pattern works because eurodata-ui controls behave like native form controls:\n\n```tsx\nfunction MyForm() {\n  return (\n    <form\n      onSubmit={event => {\n        event.preventDefault();\n        // The form-level submit triggers reportValidity-style behavior — any\n        // invalid control surfaces its error UI and submission is blocked.\n        const data = new FormData(event.currentTarget);\n        // …send `data` to your backend\n      }}\n    >\n      <ed-input name=\"email\" required type=\"email\" label=\"Email\" />\n      <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n    </form>\n  );\n}\n```\n\n#### React Hook Form\n\nFor [React Hook Form](https://react-hook-form.com/), use the `Controller` pattern. It gives RHF explicit control over the value/onChange/onBlur wiring (needed for custom elements that don't follow React Hook Form's `register` heuristics) and lets you surface validation errors via the control's `error` prop:\n\n```tsx\nimport { Controller, useForm } from 'react-hook-form';\nimport type EdInputElement from '@eurodata/eurodata-ui/dist/components/input/input.js';\n\ntype FormValues = { email: string };\n\nconst emailRules = {\n  required: 'Email is required',\n  pattern: { value: /^[^@]+@[^@]+$/, message: 'Enter a valid email address' },\n};\n\nfunction MyForm() {\n  const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { email: '' } });\n\n  return (\n    <form onSubmit={handleSubmit(data => console.log(data))}>\n      <Controller\n        name=\"email\"\n        control={control}\n        rules={emailRules}\n        render={({ field, fieldState }) => (\n          <ed-input\n            ref={field.ref}\n            label=\"Email\"\n            type=\"email\"\n            value={field.value}\n            onInput={event => field.onChange((event.target as EdInputElement).value)}\n            onBlur={field.onBlur}\n            error={fieldState.error?.message ?? null}\n          />\n        )}\n      />\n      <ed-button type=\"submit\" variant=\"brand\">Submit</ed-button>\n    </form>\n  );\n}\n```\n\nBecause we wire `fieldState.error?.message` into the control's `error` prop, RHF's validation errors surface immediately on submit without waiting for the field's own progressive-disclosure gate — matching what a RHF user expects.\n\nThe same patterns work with the React wrappers used in React 18 and earlier. See the [React frameworks guide](https://test-apps.eurodata.de/a/eurodata-ui/docs/frameworks/react) for general setup."
    },
    {
      "heading": "Form Reset",
      "markdown": "Use `<button type=\"reset\">` inside a `<form>`, or call `form.reset()` programmatically, to restore the form's controls to their initial state. eurodata-ui form controls implement [`formResetCallback`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/formAssociatedCallback) so they participate in this the same way native controls do.\n\n**What gets reset**\n\n-   **Value** — restored to the value declared in the HTML (the `value` attribute for inputs and textareas, the `checked` state for checkboxes and switches, the initially selected option for selects, etc.). Each component handles its own value-shape restoration.\n-   **Custom validity message** — any message set via `setCustomValidity()` is cleared, and constraint validation re-runs against the restored value.\n-   **`hasInteracted`** — flipped back to `false`. As a consequence:\n    -   `:state(user-invalid)` and `:state(user-valid)` clear (the underlying `:state(invalid)` / `:state(valid)` still apply if the restored value fails constraints).\n    -   `:state(dirty)` clears.\n    -   `:state(ai)` reactivates if the `ai` attribute is set (AI styling that was suppressed during interaction returns).\n-   **Validation message visibility** — error messages produced by constraint validation (e.g. \"Please fill out this field\" from `required`) are hidden again until the user interacts or submits.\n\n**What does _not_ get reset**\n\n-   **The `error` property.** It's consumer-set visual state, not validity state — if you set `<ed-input error=\"Backend rejected this\">`, the error survives reset. To clear it programmatically:\n    \n    ```js\n    input.error = null;\n    ```\n    \n-   **The `warning` property.** Same model as `error`.\n    \n-   **The `ai` attribute.** The attribute itself is preserved; only the suppressed-on-interaction _state_ resets (the styling returns once `hasInteracted` clears).\n    \n-   **Component configuration** like `required`, `pattern`, `minlength`, etc. These are validation rules, not validation state.\n    \n\nIn short: form reset restores the _value_ and clears _transient_ validation state. Consumer-controlled visual state (`error`, `warning`, `ai`) persists, matching the platform's separation between value and configuration."
    },
    {
      "heading": "Custom Validation Styles",
      "markdown": "eurodata-ui provide out of the box validation styles for form controls as part of its default theme.\n\nDue to the many ways form controls are used, you may need to overwrite the default styling. The following [custom states](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states) will be applied to reflect a control's validity and visual state. You can use them to create custom styles for any of the states you're interested in.\n\n-   `:state(required)` - the form control is required\n-   `:state(optional)` - the form control is optional\n-   `:state(invalid)` - the form control fails constraint validation (set immediately, before any user interaction)\n-   `:state(valid)` - the form control satisfies constraint validation (set immediately, before any user interaction)\n-   `:state(user-invalid)` - the form control is invalid _and_ the user has interacted with it\n-   `:state(user-valid)` - the form control is valid _and_ the user has interacted with it\n-   `:state(dirty)` - the user has interacted with the form control (same trigger as `user-invalid`/`user-valid`'s interaction gate)\n-   `:state(error)` - active when the consumer has set the `error` property (including the empty string) or when the control is `:state(user-invalid)`\n-   `:state(warning)` - active when the consumer has set the `warning` property (including the empty string)\n-   `:state(ai)` - active when the `ai` attribute is set _and_ the user has not yet interacted with the control\n-   `:state(disabled)` - the form control is disabled\n\nThese custom states work alongside the browser's built-in pseudo classes for validation: [`:required`](https://developer.mozilla.org/en-US/docs/Web/CSS/:required), [`:optional`](https://developer.mozilla.org/en-US/docs/Web/CSS/:optional), [`:invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid), [`:valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:valid), [`:user-invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid), and [`:user-valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-valid)."
    }
  ]
}
