Forms

Fields with a label, a hint and an error, text inputs, lists, checkboxes, radios and switches.

Edit page

Field

Field draws the label, the hint and the error, and links them to the input with id and aria-describedby, so a screen reader reads all three.

We never share it.

Example.jsx
import { Field } from './components/Field'

<Field label='Email' hint='We never share it.'>
  {(props) => <input className='input' type='email' {...props} />}
</Field>

Errors and states

States are attributes, not classes. Pass error to Field and the input gets aria-invalid; a disabled input is disabled.

Example.jsx
<Field label='Email' error='Enter a valid email address'>
  {(props) => <input className='input' {...props} />}
</Field>
<input className='input' disabled />

Select and textarea

Example.jsx
<select className='select'>…</select>
<textarea className='input' rows='4' />

Check and switch

check works for both checkboxes and radios: the shape follows the type. switch is a checkbox drawn as a toggle, with role='switch'.

Example.jsx
<label className='check'>
  <input type='checkbox' /> Remember me
</label>
<label className='check'>
  <input type='radio' name='plan' /> Monthly
</label>
<label className='switch'>
  <input type='checkbox' role='switch' /> Notifications
</label>

Sending a form

hooks/useSendForm.js sends the fields as JSON to FORMS_ENDPOINT and returns the errors by field name, so each one lands under its input. A hidden website input catches bots: people never fill it in.

Contact.jsx
const { send, status, errors } = useSendForm('contact')

const onSubmit = (event) => {
  event.preventDefault()
  send(Object.fromEntries(new FormData(event.target)))
}

<form onSubmit={onSubmit} noValidate>
  <Field label='Email' error={errors.email}>
    {(props) => <input className='input' name='email' type='email' {...props} />}
  </Field>
  <input name='website' tabIndex='-1' autoComplete='off' hidden />
  <button className='button dark' disabled={status === 'sending'}>Send</button>
</form>

On Cloudflare Pages the endpoint is part of the site. functions/v1/forms/[formId].js checks the fields against the schema of the form, limits each visitor to five messages in ten minutes and saves them to a D1 database, keeping them for 90 days. The database and the table are described in wrangler.toml and migrations/.

Terminal
wrangler d1 create avior
wrangler d1 migrations apply avior --remote
yarn build && wrangler pages deploy
wrangler d1 execute avior --remote --command "SELECT * FROM messages"

Put the id of your own database into wrangler.toml. On another host, point FORMS_ENDPOINT to a forms service with the same contract, or set it to an empty string and the hook returns the status unavailable.

StatusWhen
idlenothing sent yet
sendingthe request is on its way
sentthe service accepted the form
invalidthe service returned errors by field
failednetwork or server error
unavailableno forms service configured

Classes

ClassWhat it is
fielda column of label, control, hint and error
field-labelthe label
field-hinta muted note under the control
field-errorthe error, red in both modes
inputa text input or a textarea, 40px tall
selecta native select with its own arrow
checka checkbox or a radio with its label
switcha checkbox drawn as a toggle
minia 32px input or select

Variables

VariableDefaultWhat it sets
$field-gap8pxspace between label, control and hint
$field-space20pxspace below a field
$input-height40pxheight of input and select
$input-padding0 12pxinner padding
$input-radius$radiuscorner radius
$input-linevar(--line)border colour
$input-focusvar(--accent)border and ring on focus
$check-size16pxsize of a checkbox or radio
$check-activevar(--accent)colour when checked
$switch-width32pxwidth of a switch
$switch-height20pxheight of a switch
$switch-onvar(--accent)colour of a switch when on