Changelog Azalea Kit UI 101 Usage Customizing Utilities Form controls Laravel Vue FHIR Types Accessibility Icons Actions Button Button group Layout and structure Card Card section Care summary Care timeline Divider Empty state Footer Header Layout Stack Table Selection and input Calendar Checkbox Checkbox group Color picker Combobox Combobox option Input Input mask Memorable date Number input Option Phone input Radio Radio button Radio group Range Select SSN input Tag Textarea Toggle ZIP code input Images and icons Avatar Icon Icon button Logo Feedback indicators Badge Banner Progress bar Skeleton Skeleton body text Skeleton display text Skeleton thumbnail Spinner Typography Text Navigation Breadcrumb Breadcrumb item Dropdown Menu Menu item Menu label Navigation Nav group Nav item Nav toggle Pagination Tab Tab group Tab panel Top bar Tree Tree item User menu Overlays Dialog Popover Tooltip Utilities Collapsible FHIR address FHIR human name FHIR quantity FHIR questionnaire Format bytes Format date Format number Relative time Resize observer Split panel Tooltip Border Color Shadow Spacing Transition Typography Z-index Contributing Visual Tests Light Dark System

Form Controls

Every Azalea Kit UI component makes use of a shadow DOM to encapsulate markup, styles, and behavior. Historically, native <form> elements did not recognize form controls located inside a shadow root.

Azalea Kit UI form controls are form-associated custom elements, so the browser treats them like native form controls: they appear in form.elements, submit their values through FormData, participate in constraint validation (including form.reportValidity() and form.checkValidity()), respond to form resets, and honor the form attribute for associating with a form by id. Things “just work” — including with form serialization libraries built on FormData.

For privacy reasons, Azalea Kit UI form controls never ask the browser to remember their values for session restore — after a browser restart or back/forward-cache restore, form controls come back empty rather than repopulated. This is deliberate: Azalea products handle protected health information on shared workstations.

Data serialization

If you’re relying on standard form submissions, e.g. <form action="...">, you can probably skip this section. However, most modern apps use the Fetch API or a library such as axios to submit forms using JavaScript.

The FormData interface offers a standard way to serialize forms in the browser. You can create a FormData object from any <form> element like this.

const form = document.querySelector('form');
const data = new FormData(form);

// All form control data is available in a FormData object

However, FormData can be tricky to work with or you might need to pass a JSON payload to the server. To accommodate this, Azalea Kit UI offers a serialization utility that gathers form data and returns a simple JavaScript object instead.

import { serialize } from '@azaleahealth/azalea-kit-ui/utilities/form.js';

const form = document.querySelector('form');
const data = serialize(form);

// All form control data is available in a plain object

This results in an object with name/value pairs that map to each form control. If more than one form control shares the same name, the values will be passed as an array, e.g. { name: ['value1', 'value2'] }.

Constraint validation

Client-side validation can be enabled through the browser’s Constraint Validation API for Azalea Kit UI form controls. You can activate it using attributes such as required, pattern, minlength, and maxlength. Azalea Kit UI implements many of the same attributes as native form controls, but check each form control’s documentation for a list of all supported properties.

If you don’t want to use client-side validation, you can suppress this behavior by adding novalidate to the surrounding <form> element.

Client-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!

Required fields

To make a field required, use the required attribute. The form will not be submitted if a required form control is empty. Required fields will automatically receive a * after their labels. This is configurable through the --ahi-form-control-required-content custom property.

The form will not be submitted if a required field is incomplete.


Male Female Other Unknown

Check me before submitting

Submit
<form class="input__required-example">
  <ahi-input name="name" label="Name" required></ahi-input>
  <br />
  <ahi-select label="Gender" clearable required>
    <ahi-option value="male">Male</ahi-option>
    <ahi-option value="female">Female</ahi-option>
    <ahi-option value="other">Other</ahi-option>
    <ahi-option value="unknown">Unknown</ahi-option>
  </ahi-select>
  <br />
  <ahi-textarea name="comment" label="Comment" required></ahi-textarea>
  <br />
  <ahi-checkbox required>Check me before submitting</ahi-checkbox>
  <br /><br />
  <ahi-button type="submit" variant="primary">Submit</ahi-button>
</form>

<script type="module">
  const form = document.querySelector('.input__required-example');

  // Wait for controls to be defined before attaching form listeners
  await Promise.all([
    customElements.whenDefined('ahi-button'),
    customElements.whenDefined('ahi-checkbox'),
    customElements.whenDefined('ahi-input'),
    customElements.whenDefined('ahi-option'),
    customElements.whenDefined('ahi-select'),
    customElements.whenDefined('ahi-textarea')
  ]).then(() => {
    form.addEventListener('submit', event => {
      event.preventDefault();
      alert('All fields are valid!');
    });
  });
</script>

Input types

Some input types will automatically trigger constraints, such as email and url.



Submit Reset
<form class="input__type-example">
  <ahi-input type="email" label="Email" placeholder="you@example.com" required></ahi-input>
  <br />
  <ahi-input type="url" label="URL" placeholder="https://example.com/" required></ahi-input>
  <br />
  <div style="display: flex; gap: var(--ahi-space-2)">
    <ahi-button type="submit" variant="primary">Submit</ahi-button>
    <ahi-button type="reset" variant="default">Reset</ahi-button>
  </div>
</form>

<script type="module">
  const form = document.querySelector('.input__type-example');

  // Wait for controls to be defined before attaching form listeners
  await Promise.all([
    customElements.whenDefined('ahi-button'),
    customElements.whenDefined('ahi-input')
  ]).then(() => {
    form.addEventListener('submit', event => {
      event.preventDefault();
      alert('All fields are valid!');
    });
  });
</script>

Custom error messages

To create a custom validation error, pass a non-empty string to the setCustomValidity() method. This will override any existing validation constraints. The form will not be submitted when a custom validity is set and the browser will show a validation error when the containing form is submitted. To make the input valid again, call setCustomValidity() again with an empty string.


Submit Reset
<form class="input__custom-example">
  <ahi-input label="Type “azalea”" required></ahi-input>
  <br />
  <div style="display: flex; gap: var(--ahi-space-2)">
    <ahi-button type="submit" variant="primary">Submit</ahi-button>
    <ahi-button type="reset" variant="default">Reset</ahi-button>
  </div>
</form>

<script type="module">
  const form = document.querySelector('.input__custom-example');
  const input = form.querySelector('ahi-input');

  // Wait for controls to be defined before attaching form listeners
  await Promise.all([
    customElements.whenDefined('ahi-button'),
    customElements.whenDefined('ahi-input')
  ]).then(() => {
    form.addEventListener('submit', event => {
      event.preventDefault();
      alert('All fields are valid!');
    });

    input.addEventListener('ahi-input', () => {
      if (input.value === 'azalea') {
        input.setCustomValidity('');
      } else {
        input.setCustomValidity("Hey, you're supposed to type 'azalea' before submitting this!");
      }
    });
  });
</script>

Showing your own error message

When your app decides what is wrong with a field — its own validation rules, or a rejection from the server — set the control’s error property to the message. The control becomes invalid, shows that message in its error row right away, without waiting for the user to touch the field, and shows it instead of the browser’s message, so a required field left empty shows one error, in your words and your language. Clear error to make the control valid again. Use the error slot when the message needs markup.

ahi-input, ahi-textarea, ahi-select, ahi-combobox, ahi-input-mask (and so ahi-phone-input, ahi-zip-code-input and ahi-ssn-input), ahi-memorable-date and ahi-radio-group all support it.

<ahi-phone-input label="Mobile phone" value="229555" error="Enter all 10 digits, like (229) 555-0142."></ahi-phone-input>

Don’t show the same message in help-text as well: once the browser also considers the field invalid, the control shows its own error, and the field ends up with two.

Styling validation states

Form controls expose their validation state through custom states, which you can target in CSS with the :state() pseudo-class:

State Description
required The control is required.
optional The control is not required.
valid The control currently satisfies its validation constraints.
invalid The control currently fails its validation constraints.
user-valid The control is valid and the user has interacted with it.
user-invalid The control is invalid and the user has interacted with it (or a submit was attempted).

Azalea Kit UI’s built-in error styles key off user-invalid, so errors only appear after the user has interacted with a control or tried to submit the form — never while they’re still filling it in. Use the same state for custom styling:

ahi-input:state(user-invalid)::part(input-wrapper) {
  outline: 2px solid var(--ahi-border-critical);
}

Getting associated form controls

Because Azalea Kit UI form controls are form-associated custom elements, they appear in HTMLFormElement.elements alongside native controls, in DOM order — no special API is needed:

const form = document.querySelector('#my-form');

console.log([...form.elements]); // e.g. [input, ahi-input, ...]

The getFormControls() utility remains available for backwards compatibility and returns the same controls as an array:

import { getFormControls } from '@azaleahealth/azalea-kit-ui/utilities/form.js';

const formControls = getFormControls(document.querySelector('#my-form'));

Browser autofill & PHI

Azalea Kit UI form controls forward the standard autocomplete attribute to their underlying native input. Because Azalea products run on shared clinical workstations, be deliberate about which fields the browser (or a password manager) is allowed to remember and pre-fill.

Guidelines for handling protected health information (PHI):

  • Fields holding PHI should not be autofilled. Set autocomplete="off" on controls that collect identifiers or sensitive values you don’t want cached in the browser profile. Note that browsers and password managers may ignore off for credential-like fields, so treat it as a strong hint rather than a guarantee, and rely on your workstation/browser policy as the real control.
  • <ahi-ssn-input> defaults to autocomplete="off". Social Security Numbers are never autofilled unless a consumer explicitly overrides the attribute for a specific, reviewed reason.
  • Opt in to autofill only where it helps the patient, not the clinician. Patient-facing self-entry flows (for example, a patient completing their own registration) benefit from autofill of their own details; the same fields inside a clinician charting on many patients should generally stay off.
  • Enterprise policy is the backstop. Deployments should pair these attribute defaults with browser/OS policies that disable form and password saving on shared machines.