Forms
Fields with a label, a hint and an error, text inputs, lists, checkboxes, radios and switches.
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.
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.
Enter a valid email address
<Field label='Email' error='Enter a valid email address'>
{(props) => <input className='input' {...props} />}
</Field>
<input className='input' disabled />Select and textarea
<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'.
<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.
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/.
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.
| Status | When |
|---|---|
idle | nothing sent yet |
sending | the request is on its way |
sent | the service accepted the form |
invalid | the service returned errors by field |
failed | network or server error |
unavailable | no forms service configured |
Classes
| Class | What it is |
|---|---|
field | a column of label, control, hint and error |
field-label | the label |
field-hint | a muted note under the control |
field-error | the error, red in both modes |
input | a text input or a textarea, 40px tall |
select | a native select with its own arrow |
check | a checkbox or a radio with its label |
switch | a checkbox drawn as a toggle |
mini | a 32px input or select |
Variables
| Variable | Default | What it sets |
|---|---|---|
$field-gap | 8px | space between label, control and hint |
$field-space | 20px | space below a field |
$input-height | 40px | height of input and select |
$input-padding | 0 12px | inner padding |
$input-radius | $radius | corner radius |
$input-line | var(--line) | border colour |
$input-focus | var(--accent) | border and ring on focus |
$check-size | 16px | size of a checkbox or radio |
$check-active | var(--accent) | colour when checked |
$switch-width | 32px | width of a switch |
$switch-height | 20px | height of a switch |
$switch-on | var(--accent) | colour of a switch when on |