FormLayout

A page that is one form: header, fields, then the actions.

Overview

Creating or editing a record on a page of its own.

Form

Do

New client

Who is the quote for?

Client

import { Button, Card, Field, FormLayout, Input, Stack } from "@hackcove/ui";

<FormLayout
  title="New client"
  subtitle="Who is the quote for?"
  onSubmit={(event) => event.preventDefault()}
  actions={
    <>
      <Button type="submit">Save client</Button>
      <Button variant="ghost">Cancel</Button>
    </>
  }
>
  <Card title="Client">
    <Stack>
      <Field label="Full name" htmlFor="do-form-name">
        <Input id="do-form-name" placeholder="Naledi Mokoena" />
      </Field>
      <Field label="Email" htmlFor="do-form-email">
        <Input id="do-form-email" type="email" placeholder="[email protected]" />
      </Field>
    </Stack>
  </Card>
</FormLayout>
The fields go in children; Save and Cancel go in actions, at the end of the form.
Don't

New client

Who is the quote for?

Client

import { Button, Card, Field, Input, ListLayout, Stack } from "@hackcove/ui";

<ListLayout
  title="New client"
  subtitle="Who is the quote for?"
  actions={<Button>Save client</Button>}
>
  <Card title="Client">
    <Stack>
      <Field label="Full name" htmlFor="dont-form-name">
        <Input id="dont-form-name" placeholder="Naledi Mokoena" />
      </Field>
      <Field label="Email" htmlFor="dont-form-email">
        <Input id="dont-form-email" type="email" placeholder="[email protected]" />
      </Field>
    </Stack>
  </Card>
</ListLayout>
ListLayout is not a form: Save sits in the header, away from the fields, and nothing submits on Enter. Use FormLayout.

Full page

New client

Who is the quote for?

Client

import {
  AppShell,
  Button,
  Card,
  Field,
  FormLayout,
  Input,
  NavTabs,
  Stack,
} from "@hackcove/ui";

<AppShell
  brand={<a href="#">Hack Cove</a>}
  nav={
    <NavTabs
      label="Main"
      items={[
        { label: "Quotes", href: "#quotes", active: true },
        { label: "Clients", href: "#clients" },
      ]}
    />
  }
>
  <FormLayout
    title="New client"
    subtitle="Who is the quote for?"
    onSubmit={(event) => event.preventDefault()}
    actions={
      <>
        <Button type="submit">Save client</Button>
        <Button variant="ghost">Cancel</Button>
      </>
    }
  >
    <Card title="Client">
      <Stack>
        <Field label="Full name" htmlFor="full-page-name">
          <Input id="full-page-name" placeholder="Naledi Mokoena" />
        </Field>
        <Field label="Email" htmlFor="full-page-email">
          <Input id="full-page-email" type="email" />
        </Field>
      </Stack>
    </Card>
  </FormLayout>
</AppShell>
A whole form page: the shell with its brand and nav, then the layout with the fields in children and Save and Cancel in actions.

Usage

  • The fields are the children, grouped in Cards; the layout spaces them in a reading-width column.
  • Save and Cancel go in actions, at the end of the form: a type="submit" Button first, then a ghost Cancel. The header has no actions here.
  • Handle the submit with onSubmit: other props go on the <form> itself.
  • A form in a small panel is a Dialog or FieldDialog; a sign-in is CenteredLayout.
  • Do not put Save in the header of a ListLayout: that is not a form.

Accessibility

  • The root is a real <form>: Enter in a field submits it, and a type="submit" Button submits it.
  • The title is the page's h1.
  • Give each control a Field with htmlFor, so every input has a label.

API

FormLayout

A form page: header (no header actions), then a max-w-3xl column that starts at the Container's left edge (it is not re-centred), with an actions row at the end. Other props go on the <form>.


actions
Type
ReactNode

breadcrumbs

A Breadcrumbs above the title.

Type
ReactNode

subtitle
Type
ReactNode

tabs

A NavTabs (or similar) under the title.

Type
ReactNode

title required
Type
ReactNode