FormRow
ImplementedDashboard 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.
src/components/molecules/FormRow/FormRow.tsxDashboard 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 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, defaultsm= 640px), wraps each child in amin-w-0FieldWrapper so flex children can actually shrink, and offersasGroup/ariaLabelfor logical grouping. Its origin note (ResponsiveFieldGroup.tsx:88-96) documents that bareflex flex-wrapwithmin-width: 160pxchildren 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-rolledgap-3bypasses 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
children*React.ReactNodeRow body — usually FormField slots, but any children work.
align"start" | "center"startCross-axis alignment of the row’s items. center matches the legacy "label + switch" rows; start (default) keeps stacked inputs top-aligned.
classNamestringExtra classes appended to the root div.
Props
FormRow
childrenReact.ReactNode—Row body — usually FormFields (width slots: default / 'wide' via FormField's width prop), but any children work.
align'start' | 'center''start'Vertical alignment of the row's items — 'center' matches the legacy label + switch rows.
classNamestring—Extra classes on the root.
<FormRow />Accessibility
Type
1 ruleARIA
1 ruleUsage
Import
import { FormRow } from "@epasslive/design-system"Example 1
<FormRow><FormField label="Name" /><FormField label="Slug" width="wide" /></FormRow>Example 2
<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