Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

FormsReact

Form

A native form element with consolidated error handling.

Rendered by React 19.3.1

import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { Switch } from '@hamur/react/switch'

export default function FormExample() {
  const [values, setValues] = React.useState<unknown>(null)
  return (
    <Form className="stack" onFormSubmit={(next: unknown) => setValues(next)}>
      <Field.Root name="name" className="field">
        <Field.Label>Name</Field.Label>
        <Field.Control className="input" required placeholder="Ada Lovelace" />
        <Field.Error className="error" />
      </Field.Root>
      <Field.Root name="news" className="field">
        <Field.Label className="row">
          <Switch.Root className="switch">
            <Switch.Thumb className="thumb" />
          </Switch.Root>
          Send me the newsletter
        </Field.Label>
      </Field.Root>
      <button className="button" type="submit">
        Subscribe
      </button>
      {values ? <pre className="output">{JSON.stringify(values)}</pre> : null}
    </Form>
  )
}
100Tests passing in React · 0 failing
3.1 KiBSize
Not measured yetMemory
Not measured yetSpeed
productdevbook and 9 upstream authors11 commits · last

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Form />

Examples

Action

The form submits through a server function with the action state.

FormAction.tsx
import * as React from 'react'
import { Button } from '@hamur/react/button'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'

interface FormState {
  serverErrors?: Record<string, string>
}

export default function FormActionExample() {
  const [state, formAction, loading] = React.useActionState<FormState, FormData>(submitForm, {})
  return (
    <Form className="stack" errors={state.serverErrors} action={formAction}>
      <Field.Root name="username" className="field">
        <Field.Label>Username</Field.Label>
        <Field.Control
          className="input"
          type="text"
          autoComplete="username"
          required
          defaultValue="admin"
          placeholder="e.g. alice132"
        />
        <Field.Error className="error" />
      </Field.Root>
      <Button className="button" type="submit" disabled={loading} focusableWhenDisabled>
        Submit
      </Button>
    </Form>
  )
}

// A server function in a framework that has them (`'use server'` in Next.js).
async function submitForm(_previousState: FormState, formData: FormData): Promise<FormState> {
  // Mimic a server response.
  await new Promise((resolve) => setTimeout(resolve, 1000))
  const username = formData.get('username') as string | null
  if (username === 'admin') {
    return { serverErrors: { username: "'admin' is reserved for system use" } }
  }
  // 50% chance the username is taken.
  if (Math.random() > 0.5) {
    return { serverErrors: { username: `${username} is unavailable` } }
  }
  return {}
}

Zod

A Zod schema's field errors are passed to errors by field name.

FormZod.tsx
import * as React from 'react'
import { Button } from '@hamur/react/button'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'

// The checks of a schema library such as Zod, with its flattened field errors:
// z.object({ name: z.string().min(1), age: z.coerce.number().positive() })
function fieldErrors(values: Record<string, unknown>) {
  const errors: Record<string, string[]> = {}
  if (String(values.name ?? '').length < 1) errors.name = ['Name is required']
  const age = Number(values.age)
  if (Number.isNaN(age)) errors.age = ['Age must be a number']
  else if (!(age > 0)) errors.age = ['Age must be a positive number']
  return errors
}

async function submitForm(values: Record<string, unknown>) {
  return { errors: fieldErrors(values) }
}

export default function FormZodExample() {
  const [errors, setErrors] = React.useState({})
  return (
    <Form
      className="stack"
      errors={errors}
      onFormSubmit={async (values: Record<string, unknown>) => { const response = await submitForm(values) setErrors(response.errors) }}
    >
      <Field.Root name="name" className="field">
        <Field.Label>Name</Field.Label>
        <Field.Control className="input" placeholder="Enter name" />
        <Field.Error className="error" />
      </Field.Root>
      <Field.Root name="age" className="field">
        <Field.Label>Age</Field.Label>
        <Field.Control className="input" placeholder="Enter age" />
        <Field.Error className="error" />
      </Field.Root>
      <Button className="button" type="submit">
        Submit
      </Button>
    </Form>
  )
}

API reference

4 props across 1 parts, in React syntax. className, style, render are listed once, under Shared props.

FormForm

A native form element with consolidated error handling. Renders a <form>.

Form props
PropTypeDefault
errorsValidation errors returned externally, typically after submission by a server or a form action.Errors—
actionsRefA ref to imperative actions.React.RefObject<FormActions | null>—
onFormSubmitEvent handler called when the form is submitted.((formValues: FormValues, eventDetails: { reason: "none"; event: Event; }) => void)—
validationModeDetermines when the form should be validated.FormValidationMode'onSubmit'

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, State>

Keyboard and accessibility

Keyboard

Form adds no keys; Enter in a field submits it, as a native form does.

Accessibility

What the parts set
PartSetsSo that
Formrenders a <form>the browser's own submit, reset and autofill work
Formmoves focus to the first invalid field when a submit failsthe person lands on the error to fix
Formhands each entry of errors to the field with the same namea server's error is read with its field, not elsewhere on the page

Focus order

  1. Form adds no tab stop; its fields keep document order.
  2. A submit with errors moves focus to the first invalid field in document order.