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.
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.
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.
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>
)
}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.