Forms
Use TextField, Toggle, Picker, and Slider in real form flows with controlled React state.
Liquidify's interactive controls follow React's controlled/uncontrolled split (see
Controlled state). In a form, the reliable
pattern across every control is to own each value in React state and read it on
submit. TextField additionally supports native name-based participation, so a
plain FormData read works for text inputs; Toggle, Picker, and Slider do
not emit a native form value and must be gathered from state.
Controlled gather
This is the portable pattern — it works identically for text, boolean, choice, and numeric controls, and it is deterministic under SSR because the same state seeds the server and first client render.
import { useState } from "react"
import { Button, Picker, Slider, TextField, Toggle } from "@liquidify/react"
import "@liquidify/react/styles.css"
export function AccountForm() {
const [name, setName] = useState("")
const [notify, setNotify] = useState(true)
const [plan, setPlan] = useState("free")
const [volume, setVolume] = useState(0.5)
const save = () => console.log({ name, notify, plan, volume })
return (
<form onSubmit={(event) => event.preventDefault()}>
<label htmlFor="account-name">Display name</label>
<TextField
id="account-name"
name="name"
value={name}
onChange={setName}
placeholder="Ada Lovelace"
/>
<Toggle
aria-label="Email notifications"
checked={notify}
onChange={setNotify}
/>
<Picker
aria-label="Plan"
items={[
{ value: "free", label: "Free" },
{ value: "pro", label: "Pro" },
]}
value={plan}
onChange={setPlan}
/>
<Slider
aria-label="Alert volume"
value={volume}
onChange={setVolume}
min={0}
max={1}
step={0.1}
/>
<Button variant="borderedProminent" onPress={save}>
Save
</Button>
</form>
)
}Each onChange reports a plain, typed value — TextField and Picker emit a
string, Toggle a boolean, Slider a number — so state stays simple and
the payload never needs to be unwrapped from an event.
Button renders a native <button type="button"> and never submits a form on
its own, so drive the submit from onPress (as save above) rather than relying
on implicit form submission.
Native submission for text fields
TextField forwards name to its underlying input, so a native FormData read
picks up text values without a controlled binding. Combine this with controlled
state for the non-text controls when you want a single submit handler:
import { useRef } from "react"
import { Button, TextField } from "@liquidify/react"
export function ContactForm() {
const formRef = useRef<HTMLFormElement>(null)
const send = () => {
if (!formRef.current) return
const data = new FormData(formRef.current)
console.log(data.get("email"))
}
return (
<form ref={formRef} onSubmit={(event) => event.preventDefault()}>
<label htmlFor="contact-email">Email</label>
<TextField
id="contact-email"
name="email"
inputMode="email"
autoComplete="email"
/>
<Button variant="borderedProminent" onPress={send}>
Send
</Button>
</form>
)
}Toggle, Picker, and Slider do not expose a name and are absent from
FormData; keep them in controlled state and merge on submit.
Visual grouping
Form and its Form.Section slot are display containers — they add the grouped
card surface and hairline separators but hold no value or state of their own. Wrap
row controls in a Section for the iOS grouped-list look; the native <form>
element still owns submission.
import { Form, TextField, Toggle } from "@liquidify/react"
export function GroupedForm() {
return (
<form onSubmit={(event) => event.preventDefault()}>
<Form>
<Form.Section title="Profile">
<label htmlFor="grouped-name">Name</label>
<TextField id="grouped-name" name="name" />
<Toggle aria-label="Public profile" defaultChecked />
</Form.Section>
</Form>
</form>
)
}Accessibility
Associate a visible <label> with text fields through htmlFor/id, and give
non-text or icon-only controls an accessible name with aria-label. Place
validation text next to its field and connect it through the control's documented
API. See Accessibility for the full checklist.