⌘K
(opens in new tab)
Liquidify
Documentation
Getting started
Concepts
Liquid GlassProvidersThemingCompositionControlled stateFormsAccessibilityBrowser supportFidelity notesSSR and React Server Components
Guides
Packages
Design tokens
Examples
AI resourcesDocumentation versions
Community

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.

Controlled stateAccessibility