Cookbook / Form Validation

Form Validation

form

Live validation — form() exposes value/validity, derived() computes the error message, touched gates when errors show.

id
form-validation
since
0.5.0
file
form-validation.aihu
stateformderivedactionifgroupon:inputon:blurinterpolation

Run it

source · static

This recipe doesn't have a hydrated island in the demo gallery yet. An in-browser, WASM-compiled playground (edit this source and re-render live) is planned as a follow-up — read the real source below.

Source

form-validation.aihu
governedform-validation.aihuaihu1.7 kB
// form-validation — form() with value+validity for form-associated custom element

@state {
  let value = state('')
  let touched = state(false)

  form({
    value: () => value,
    validity: () => ({ valueMissing: !value.trim(), tooShort: value.trim().length < 2 }),
  })

  const errorMessage = derived(() => {
      if (!touched) return ''
      if (!value.trim()) return 'This field is required.'
      if (value.trim().length < 2) return 'Must be at least 2 characters.'
      return ''
    })
  const isValid = derived(() => value.trim().length >= 2)

  const onInput = action((e: Event) => {
      value = (e.target as HTMLInputElement).value
      touched = true
    })
  const onBlur = action(() => { touched = true })
}

@template {
  <label class="form-field">
    <span class="field-label">Name <span aria-hidden="true">*</span></span>
    <input
      type="text"
      class={['field-input', touched && !isValid && 'invalid']}
      value={value}
      on:input={onInput}
      on:blur={onBlur}
      required
      minlength="2"
    />
    <group if={errorMessage}>
      <span class="error-msg" role="alert">{errorMessage}</span>
    </group>
    <group if={isValid}>
      <span class="valid-msg">Looks good!</span>
    </group>
  </label>
}

@style {
  .form-field { display: flex; flex-direction: column; gap: 0.3rem; max-width: 20rem; }
  .field-label { font-size: 0.9rem; font-weight: 600; }
  .field-input { padding: 0.5rem; border: 1px solid var(--border, #ccc); border-radius: 4px; font-size: 1rem; }
  .field-input.invalid { border-color: var(--error, #c00); }
  .error-msg { font-size: 0.8rem; color: var(--error, #c00); }
  .valid-msg { font-size: 0.8rem; color: var(--success, #080); }
}

Requires

concerns
statea11y

Anti-patterns

  • Do not surface errors on every keystroke before first interaction — track touched so messages appear only after blur or input.

Related recipes