Passer au contenu principal

FormRow

Implemented

Dashboard form row layout — flex-wrap row with a tokenized gap, replaces um-form-row CSS. Wraps when children no longer fit but does NOT stack on narrow viewports; use ResponsiveFieldGroup for mobile-first stacking.

Categorymolecules
Sourcesrc/components/molecules/FormRow/FormRow.tsx
AI HintDesktop dashboard form rows composed with FormField width slots. It wraps but never stacks on narrow viewports — for mobile-first forms (or any layout that must become a single column under a breakpoint) use ResponsiveFieldGroup instead. Ships no copy.

Dashboard form-row layout — a flex-wrap row (16px gap) that lays FormField columns side by side inside a FormSection, collapsing to a single-column stack (12px gap) below md (768px).

FormRow

Form layout molecule — form trio (FormSection · FormRow · FormField)

A plain layout <div data-component="form-row"> rendering flex flex-col gap-ep-3 md:flex-row md:flex-wrap md:gap-ep-4 with items-start (default) or items-center. Column sizing is delegated to each child’s FormField width prop (default / wide / narrow) — FormRow itself owns no column API.

Shown on the public event page

Visible on the storefront once saved

Where tickets are sent

This component supports light and dark mode.

Responsive behavior — read before using

FormRow collapses to one column below md (768px) — but does not shrink children

Use ResponsiveFieldGroup when each field must also be able to shrink (min-w-0).

FormRow renders flex flex-col gap-ep-3 md:flex-row md:flex-wrap md:gap-ep-4 — a single-column stack with a 12px gap below md (768px), and flex-wrap rows with a 16px gap from md up. This ports the legacy um-form-row mobile rule (flex-direction: column; gap: 12px inside the <768px media query, admin.css:3142-3148). The remaining caveat: FormRow does no child processing — above md each FormField keeps its own min-width (160px default), so intrinsically wide children can still push a row past the viewport, and the wrap point depends on content. If your fields must deterministically shrink, use ResponsiveFieldGroup: it wraps every child in a min-w-0 FieldWrapper — the deterministic fix for the 129–167% horizontal overflow on 375px viewports documented in ResponsiveFieldGroup.tsx:88-96.

FormRow vs ResponsiveFieldGroup vs raw grid

The design system ships two competing row molecules. Pick deliberately:

  • FormRow — desktop-first dashboard rows. Single-column stack below md (768px, 12px gap), flex-wrap rows with a fixed 16px gap above it; no child processing. Lighter and simpler; use it inside FormSection on admin/dashboard surfaces (TicketTypeForm, PackageManager, EventDetail…) where the md-collapse behavior is sufficient.
  • ResponsiveFieldGroup — mobile-first forms. Stacks below a configurable breakpoint (flex-col → {bp}:flex-row, default sm = 640px), wraps each child in a min-w-0 FieldWrapper so flex children can actually shrink, and offers asGroup/ ariaLabel for logical grouping. Its origin note ( ResponsiveFieldGroup.tsx:88-96) documents that bare flex flex-wrap with min-width: 160px children produced 129–167% horizontal overflow on 375px viewports — the wrap-only pattern FormRow renders above its own md breakpoint (FormRow’s sub-768px collapse now avoids it below md, but it still does no per-field shrink-wrapping). Newer storefront forms (EventForm, VenueForm, PassForm, ProductCatalogForm) consistently pick ResponsiveFieldGroup.
  • Raw grid grid-cols-2 — neither. A grid with no breakpoint variant forces two columns at every width (it cannot even wrap), and hand-rolled gap-3 bypasses DS spacing tokens. If you are typing a raw two-column field grid, you want one of the two molecules above.

The convergence question (deprecate FormRow, or keep it as the dashboard-specific wrapper now that its legacy sub-768px collapse is ported) is an open ADR — until it lands, this boundary is the contract: mobile-first → ResponsiveFieldGroup; desktop dashboard → FormRow.

Composition recipe

The form trio composes at the call site — no molecule imports another. A section stacks rows; a row lays out fields; a field owns label, hint, error, and its width slot:

import {
  FormSection,
  FormRow,
  FormField,
  Input,
} from '@epasslive/design-system'

<FormSection title="Contact" description="Who receives the tickets?">
  <FormRow>
    <FormField label="First name" htmlFor="first">
      <Input id="first" label="First name" labelHidden ariaLabel="First name"
             value={first} onChange={setFirst} />
    </FormField>
    <FormField label="Last name" htmlFor="last">
      <Input id="last" label="Last name" labelHidden ariaLabel="Last name"
             value={last} onChange={setLast} />
    </FormField>
  </FormRow>
  <FormRow>
    <FormField label="Description" width="wide" htmlFor="desc"
               hint="Shown on the public event page">
      <Input id="desc" label="Description" labelHidden ariaLabel="Description"
             value={desc} onChange={setDesc} />
    </FormField>
  </FormRow>
</FormSection>

Use FormSection as="fieldset" when the section is a single logical group that can be disabled as a unit; the fieldset/legend semantics belong to the section, never to the row.

Accessibility

FormRow is a layout-only <div> — it renders data-component="form-row" and no ARIA whatsoever, by design. A row of unrelated fields is not one group: grouping semantics belong to FormSection as="fieldset" (or, for a standalone row that IS one logical group, ResponsiveFieldGroup’s asGroup + ariaLabel props — an API FormRow deliberately does not offer). Do not add role="group" to a FormRow. Labels, hints, errors, and the aria-describedby wiring are owned by each FormField.

Internationalization

FormRow ships no copy — it renders no user-visible strings, so there is nothing to localize. All labels, hints, and placeholders in a row are consumer-owned and belong in your translation files.

Props

Prop
Type
Default
Description
children*
React.ReactNode
—

Row body — usually FormField slots, but any children work.

align
"start" | "center"
start

Cross-axis alignment of the row’s items. center matches the legacy "label + switch" rows; start (default) keeps stacked inputs top-aligned.

className
string
—

Extra classes appended to the root div.

Props

Preview

FormRow

Prop
Type
Required
Default
Description
children
React.ReactNode
Required
—

Row body — usually FormFields (width slots: default / 'wide' via FormField's width prop), but any children work.

align
'start' | 'center'
Optional
'start'

Vertical alignment of the row's items — 'center' matches the legacy label + switch rows.

className
string
Optional
—

Extra classes on the root.

Quick use:<FormRow />

Accessibility

Type

1 rule

NASA Rule 5

layout-only <div> — renders flex flex-wrap gap-ep-4 + items-{start|center} and nothing else; no breakpoint utilities

ARIA

1 rule

WAI-ARIA

carries no role — grouping semantics belong to FormSection as='fieldset' (or ResponsiveFieldGroup asGroup)

Usage

Import

tsxSnippet
import { FormRow } from "@epasslive/design-system"

Example 1

tsxSnippet
<FormRow><FormField label="Name" /><FormField label="Slug" width="wide" /></FormRow>

Example 2

tsxSnippet
<FormRow align="center"><FormField label="Active" /><Switch /></FormRow>

Anti-patterns

— Do not rely on a <768px collapse — this row only wraps; it has no stacking breakpoint

— Do not reach for raw grid grid-cols-2 — PromoCodeManager-style forced 2-col grids overflow at 375px

Design System v4.1.0 | ePassLive