formulaire-ui
v0.4.0
Published
A modern take on FormKeep's Gridforms: a CSS core for plain HTML, plus React components built on Base UI.
Maintainers
Readme
Formulaire
A modern take on FormKeep's Gridforms: the densest forms you'll actually enjoy filling in. Use one stylesheet for plain HTML, or React components built on Base UI.
Formulaire is French for form, the paper kind you fill out at the post office.
Live demo and docs: formulaire.mattrothenberg.com
Install
npm install formulaire-ui @base-ui/reactReact 19 and Base UI 1.x are peer dependencies. The stylesheet is plain CSS in @layer formulaire, so any unlayered rule of yours overrides it.
Usage
import { GridForm } from 'formulaire-ui';
import 'formulaire-ui/styles.css';
<GridForm.Root mode="edit" density="comfortable" onFormSubmit={save}>
<GridForm.Section legend="Customer">
<GridForm.Row>
<GridForm.Field name="name" label="Full name" span={2}>
<GridForm.Input required />
</GridForm.Field>
<GridForm.Field name="stock" label="Film stock">
<GridForm.Select items={stocks} />
</GridForm.Field>
</GridForm.Row>
</GridForm.Section>
</GridForm.Root>;Ideas
- Container-adaptive, no breakpoints. A cell asks for
span × --gf-min-col, and a row wraps when the container can't fit it. - Edit and filled modes.
mode="filled"renders the same form read-only, like a completed document, with dashes for empty fields. - Errors inside the cell. Short validation messages sit beside the label, so the layout never shifts.
- Base UI underneath. Field, Form, Fieldset, Select, Radio and Checkbox handle labelling, validation and focus state.
- Plain HTML works too.
.gf > .gf-section > .gf-row > .gf-field[data-span]. - Themeable. Everything is a
--gf-*custom property inside@layer formulaire.
Styling
Formulaire ships one look and keeps every visual choice in a --gf-* custom property. Set tokens on the form, a section or a single legend, from your own CSS (it sits outside @layer formulaire, so it wins) or from React:
/* Section titles as headings over a heavy rule, instead of the tinted band. */
.heading {
--gf-legend-font: var(--gf-font);
--gf-legend-size: calc(var(--gf-input-size) * 1.0625);
--gf-legend-weight: 650;
--gf-legend-tracking: normal;
--gf-legend-case: none;
--gf-legend-bg: var(--gf-paper);
--gf-legend-rule: 3px;
}<GridForm.Root className="heading">…</GridForm.Root>
// Or one legend: legendProps go to Base UI's Fieldset.Legend.
<GridForm.Section
legend="Shipping"
legendProps={{ style: { '--gf-legend-rule': '3px' } as React.CSSProperties }}
/>More recipes, including a rebuild of the original Gridforms look, are on the Styles page.
Input masks
Formulaire doesn't bundle a mask library. GridForm.Input passes its ref straight to the <input>, so any ref-based masker works. Maskito handles the cursor position, pasting and undo well, and @maskito/phone formats numbers by country:
import { maskitoPhone } from '@maskito/phone';
import { useMaskito } from '@maskito/react';
import metadata from 'libphonenumber-js/min/metadata';
// Module scope, so the mask isn't rebuilt on every render.
const phoneMask = maskitoPhone({ countryIsoCode: 'US', metadata, format: 'NATIONAL' });
function PhoneInput(props: InputProps) {
const ref = useMaskito({ options: phoneMask });
return <GridForm.Input ref={ref} type="tel" autoComplete="tel" {...props} />;
}Call useMaskito in the same component as the input. If the hook lives in a parent and the input remounts, Maskito drops the mask.
