React address form

A React shipping address form with the autocomplete tokens browsers fill from, a searchable country select and errors read aloud.

Install

One command copies the files into your project, as code you then own. It needs a components.json, and the shadcn registry page has the one-time setup.
Terminal
npx shadcn@latest add https://www.box-kite.dev/r/address-form.json
Open in v0The item JSON
1 file · installs @box-kite/react, store-design

Light and dark

The same component twice, each half in a theme of its own — nothing in it is written for either. Open it full screen to see it in yours.
light
Shipping address
Optional.
Country
United States
Only for the courier, if they cannot find you.
dark
Shipping address
Optional.
Country
United States
Only for the courier, if they cannot find you.

Source

The files the command writes, exactly as the preview above runs them. Every style is a prop, so there is no stylesheet to copy with them.
box-kite/store/address-form.tsx
JSX
'use client';
import Box from '@box-kite/react';
import Dropdown from '@box-kite/react/components/dropdown';
import Flex from '@box-kite/react/components/flex';
import Grid from '@box-kite/react/components/grid';
import { Span } from '@box-kite/react/components/semantics';
import Textbox from '@box-kite/react/components/textbox';
import FormField from './form-field';

const COUNTRIES = ['United States', 'Canada', 'United Kingdom', 'Ireland', 'Germany', 'France', 'Netherlands', 'Moldova', 'Australia'];

export interface AddressFormProps {
  /** The fieldset's caption, and the prefix every field submits under: `shipping.city`, `billing.city`. */
  legend?: string;
  prefix?: string;
}

/**
 * A postal address as a `<fieldset>`: name, two street lines, city, postcode, country and a phone. Every
 * field carries the `autocomplete` token a browser fills it from, which on a phone is most of the typing.
 * Drop two into one form, with different prefixes, for a shipping and a billing address.
 */
export default function AddressForm({ legend = 'Shipping address', prefix = 'shipping' }: AddressFormProps) {
  const section = prefix === 'billing' ? 'billing' : 'shipping';
  const field = (name: string) => `${prefix}.${name}`;

  return (
    <Box tag="fieldset" b={0} p={0} m={0} minWidth={0}>
      <Box tag="legend" fontSize={16} fontWeight={500} mb={5}>
        {legend}
      </Box>
      <Flex d="column" gap={4}>
        <Grid gridTemplateColumns={1} sm={{ gridTemplateColumns: 2 }} gap={4}>
          <FormField label="First name">
            {(control) => <Textbox name={field('firstName')} props={{ ...control, autoComplete: `${section} given-name` }} required />}
          </FormField>
          <FormField label="Last name">
            {(control) => <Textbox name={field('lastName')} props={{ ...control, autoComplete: `${section} family-name` }} required />}
          </FormField>
        </Grid>
        <FormField label="Street address">
          {(control) => <Textbox name={field('line1')} props={{ ...control, autoComplete: `${section} address-line1` }} required />}
        </FormField>
        <FormField label="Apartment, suite, floor" hint="Optional.">
          {(control) => <Textbox name={field('line2')} props={{ ...control, autoComplete: `${section} address-line2` }} />}
        </FormField>
        <Grid gridTemplateColumns={1} sm={{ gridTemplateColumns: 2 }} gap={4}>
          <FormField label="City">
            {(control) => <Textbox name={field('city')} props={{ ...control, autoComplete: `${section} address-level2` }} required />}
          </FormField>
          <FormField label="Postcode">
            {(control) => <Textbox name={field('postcode')} props={{ ...control, autoComplete: `${section} postal-code` }} required />}
          </FormField>
        </Grid>
        <Dropdown<string>
          name={field('country')}
          label={
            <Span fontSize={14} fontWeight={500}>
              Country
            </Span>
          }
          labelProps={{ width: 'fit', gap: 1.5 }}
          defaultValue={COUNTRIES[0]}
          isSearchable
          searchPlaceholder="Search countries…"
        >
          {COUNTRIES.map((country) => (
            <Dropdown.Item key={country} value={country}>
              {country}
            </Dropdown.Item>
          ))}
        </Dropdown>
        <FormField label="Phone" hint="Only for the courier, if they cannot find you.">
          {(control) => <Textbox name={field('phone')} type="tel" props={{ ...control, autoComplete: `${section} tel` }} />}
        </FormField>
      </Flex>
    </Box>
  );
}
It stands on store-design, which the command installs with it. The design the store blocks and pages share: the components restyled with Box.createComponents(), the catalogue, the drawn product art, and the header and footer. The design is on the template page.