# React ecommerce storefront template

_Box Kite 2.2.4 · a markdown copy of https://www.box-kite.dev/templates/react-ecommerce-storefront/_

A store front page in React: a drawn hero, a product grid with filters and sorting, and a story band, in light and dark with no stylesheet.

## Light and dark

[Live demo](https://www.box-kite.dev/templates/react-ecommerce-storefront/demo.md)

## Install

One command writes the page, its design and its 1 section into your project, as code you then own. It needs a `components.json`, and [the shadcn registry page](https://www.box-kite.dev/registry.md) has the one-time setup.

```shell
npx shadcn@latest add https://www.box-kite.dev/r/ecommerce-storefront.json
```

## Sections

Each section is a block of its own, with its own page and its own install command.

- [React product grid with filters](https://www.box-kite.dev/blocks/react-product-grid.md)

## Source

The page itself. Every style is a prop, so there is no stylesheet to copy with it, and each section it imports is on its own page above.

**box-kite/store/storefront-page.tsx**

```jsx
'use client';
import Box from '@box-kite/react';
import Flex from '@box-kite/react/components/flex';
import Grid from '@box-kite/react/components/grid';
import { H1, H2, Link, P, Section, Span } from '@box-kite/react/components/semantics';
import ProductArt from './product-art';
import ProductGrid from './product-grid';
import { countOf, FREE_SHIPPING_FROM, formatPrice, productBySlug, sampleCart } from './store-data';
import { eyebrow, heading, muted, useStoreLinks } from './store-design';
import StoreShell from './store-shell';

const promises = [
  { title: `Free shipping over ${formatPrice(FREE_SHIPPING_FROM)}`, detail: 'Packed in paper and card, never plastic.' },
  { title: 'Made in small numbers', detail: 'By four workshops we visit every year.' },
  { title: 'Repaired, not replaced', detail: 'Spare parts for every object, for ten years.' },
];

const textLink = {
  fontSize: 15,
  fontWeight: 500,
  color: 'stone-900',
  textDecoration: 'underline',
  css: { textUnderlineOffset: '6px' },
  hover: { color: 'stone-500' },
  theme: { dark: { color: 'stone-100', hover: { color: 'stone-400' } } },
} as const;

/** The store's front page: the hero, three promises, the collection with its filters, and the shop's story. */
export default function StorefrontPage({ theme }: { theme?: string }) {
  const links = useStoreLinks();
  const lamp = productBySlug('table-lamp');

  return (
    <StoreShell cartCount={countOf(sampleCart)} theme={theme}>
      <Section props={{ 'aria-labelledby': 'hero-heading' }}>
        <Grid gridTemplateColumns={1} lg={{ gridTemplateColumns: 2 }} gap={10} xl={{ gap: 20 }} ai="center">
          <Box py={4} lg={{ py: 0 }}>
            <Span display="block" {...eyebrow} {...muted}>
              Autumn objects · 2026
            </Span>
            <H1
              id="hero-heading"
              mt={6}
              fontSize={48}
              lineHeight={52}
              md={{ fontSize: 72, lineHeight: 76 }}
              textWrap="balance"
              {...heading}
              letterSpacing={-2}
            >
              Fewer, better things.
            </H1>
            <P mt={6} maxWidth={110} fontSize={18} lineHeight={30} {...muted}>
              A lamp, a clock, a mug that stacks. Everyday objects in five quiet colours, made to be used for a long time and noticed only
              when they are needed.
            </P>
            <Flex gap={8} mt={10} ai="center" flexWrap="wrap">
              <Link component="button" props={{ href: '#collection' }} textDecoration="none" py={3.5} px={8}>
                Shop the collection
              </Link>
              <Link props={{ href: '#studio' }} {...textLink}>
                About the shop
              </Link>
            </Flex>
          </Box>

          <Box>
            <Link props={{ href: links.product(lamp.slug) }} display="block" aspectRatio="square" borderRadius={6}>
              <ProductArt shape={lamp.shape} finish="clay" label={`${lamp.name} in Clay`} stroke={0.5} radius={6} />
            </Link>
            <Flex jc="space-between" mt={3} fontSize={14} {...muted}>
              <Span>{lamp.name}, Clay</Span>
              <Span>{formatPrice(lamp.price)}</Span>
            </Flex>
          </Box>
        </Grid>
      </Section>

      <Grid
        gridTemplateColumns={1}
        md={{ gridTemplateColumns: 3 }}
        gap={8}
        mt={20}
        pt={8}
        bt={1}
        borderColor="stone-200"
        theme={{ dark: { borderColor: 'stone-800' } }}
      >
        {promises.map((promise) => (
          <Box key={promise.title}>
            <Span display="block" fontWeight={500}>
              {promise.title}
            </Span>
            <Span display="block" mt={1} fontSize={14} {...muted}>
              {promise.detail}
            </Span>
          </Box>
        ))}
      </Grid>

      <Box id="collection" mt={24} css={{ scrollMarginTop: '6rem' }}>
        <ProductGrid />
      </Box>

      <Studio />
    </StoreShell>
  );
}

function Studio() {
  return (
    <Section
      id="studio"
      props={{ 'aria-labelledby': 'studio-heading' }}
      mt={28}
      pt={16}
      bt={1}
      borderColor="stone-200"
      css={{ scrollMarginTop: '6rem' }}
      theme={{ dark: { borderColor: 'stone-800' } }}
    >
      <Grid gridTemplateColumns={1} md={{ gridTemplateColumns: 2 }} gap={12} xl={{ gap: 20 }} ai="center">
        <Box aspectRatio="square">
          <ProductArt
            shape="vase"
            finish="sand"
            also={['sage']}
            view="set"
            label="Two bud vases, one in Sand and one in Sage"
            stroke={0.5}
            radius={6}
          />
        </Box>
        <Box>
          <Span display="block" {...eyebrow} {...muted}>
            About
          </Span>
          <H2 id="studio-heading" mt={5} fontSize={36} lineHeight={42} textWrap="balance" {...heading} letterSpacing={-1}>
            Made slowly, in small numbers
          </H2>
          <P mt={6} fontSize={17} lineHeight={28} {...muted}>
            Every object starts as a drawing, goes through a dozen prototypes, and is made by one of four workshops we know by name. We
            would rather make one thing well than ten things quickly, so the collection changes once a year, not once a week.
          </P>
          <Box tag="blockquote" mt={10} mx={0} mb={0} ps={5} bs={2} borderColor="stone-900" theme={{ dark: { borderColor: 'stone-100' } }}>
            <P fontSize={18} lineHeight={28}>
              “The clock has hung in our kitchen for six years. I have never once had to think about it.”
            </P>
            <Span display="block" mt={3} fontSize={14} {...muted}>
              Ana, Chișinău
            </Span>
          </Box>
        </Box>
      </Grid>
    </Section>
  );
}
```

## Design

What every page of this template shares: the built-in components restyled with `Box.createComponents()`, applied to the subtree with `<Box.Components>`, so it stops at the edge of the store and the rest of your app keeps its own look.

### Store design

**box-kite/store/store-design.tsx**

```jsx
'use client';
import Box from '@box-kite/react';
import { createContext, type ReactNode, useContext } from 'react';

/**
 * The store's design: the built-in components restyled for this one brand, in light and dark. It registers
 * nothing — `Box.createComponents()` returns a design, and `StoreSurface` applies it to its subtree only, so
 * the store can sit inside an app that looks like something else.
 */
export const storeDesign = Box.createComponents({
  // Square, solid and one colour: the only filled shape on a page of hairlines, so the action is never in doubt.
  button: {
    styles: {
      bgColor: 'stone-900',
      color: 'white',
      borderRadius: 999,
      px: 6,
      fontWeight: 500,
      transition: 'colors',
      hover: { bgColor: 'stone-700' },
      active: { bgColor: 'stone-800' },
      focus: { outlineColor: 'stone-400' },
      disabled: { bgColor: 'stone-200', color: 'stone-500', hover: { bgColor: 'stone-200' } },
      theme: {
        dark: {
          bgColor: 'stone-100',
          color: 'stone-950',
          hover: { bgColor: 'white' },
          active: { bgColor: 'stone-200' },
          focus: { outlineColor: 'stone-500' },
          disabled: { bgColor: 'stone-800', color: 'stone-500', hover: { bgColor: 'stone-800' } },
        },
      },
    },
    variants: {
      secondary: {
        bgColor: 'transparent',
        color: 'stone-900',
        b: 1,
        borderColor: 'stone-300',
        hover: { bgColor: 'transparent', borderColor: 'stone-900' },
        active: { bgColor: 'stone-100' },
        focus: { borderColor: 'stone-900', outlineColor: 'stone-300' },
        theme: {
          dark: {
            bgColor: 'transparent',
            color: 'stone-100',
            borderColor: 'stone-700',
            hover: { bgColor: 'transparent', borderColor: 'stone-200' },
            active: { bgColor: 'stone-900' },
            focus: { borderColor: 'stone-300', outlineColor: 'stone-600' },
          },
        },
      },
      ghost: {
        bgColor: 'transparent',
        color: 'stone-700',
        hover: { bgColor: 'stone-900/5' },
        active: { bgColor: 'stone-900/10' },
        disabled: { bgColor: 'transparent', color: 'stone-400' },
        theme: {
          dark: {
            bgColor: 'transparent',
            color: 'stone-300',
            hover: { bgColor: 'white/10' },
            active: { bgColor: 'white/15' },
            disabled: { bgColor: 'transparent', color: 'stone-600' },
          },
        },
      },
    },
  },
  textbox: {
    styles: {
      width: 'fit',
      borderRadius: 2.5,
      borderColor: 'stone-300',
      bgColor: 'white',
      color: 'stone-900',
      hover: { borderColor: 'stone-500' },
      focus: { borderColor: 'stone-900', outlineColor: 'stone-200' },
      ariaAttr: { invalid: { borderColor: 'red-700', focus: { outlineColor: 'red-200' } } },
      theme: {
        dark: {
          bgColor: 'stone-950',
          color: 'stone-100',
          borderColor: 'stone-700',
          hover: { borderColor: 'stone-500' },
          focus: { borderColor: 'stone-200', outlineColor: 'stone-800' },
          ariaAttr: { invalid: { borderColor: 'red-400', focus: { outlineColor: 'red-900' } } },
        },
      },
    },
  },
  checkbox: {
    styles: {
      borderRadius: 1,
      borderColor: 'stone-400',
      outlineColor: 'stone-300',
      hover: { borderColor: 'stone-900' },
      checked: { bgColor: 'stone-900', borderColor: 'stone-900' },
      theme: {
        dark: {
          borderColor: 'stone-500',
          outlineColor: 'stone-700',
          hover: { borderColor: 'stone-200' },
          checked: { bgColor: 'stone-600', borderColor: 'stone-200' },
        },
      },
    },
  },
  radioButton: {
    styles: {
      borderColor: 'stone-400',
      outlineColor: 'stone-300',
      hover: { borderColor: 'stone-900' },
      checked: { bgColor: 'stone-900', borderColor: 'stone-900' },
      theme: {
        dark: {
          borderColor: 'stone-500',
          outlineColor: 'stone-700',
          hover: { borderColor: 'stone-200' },
          checked: { bgColor: 'stone-600', borderColor: 'stone-200' },
        },
      },
    },
  },
  // The caption over a row of options rather than beside it: the row wraps, and the caption takes the first line whole.
  radioGroup: {
    variants: { horizontal: { flexWrap: 'wrap' } },
    children: {
      label: { styles: { width: 'fit', fontSize: 14, fontWeight: 500, color: 'stone-900', theme: { dark: { color: 'stone-100' } } } },
    },
  },
  dropdown: {
    styles: {
      width: 'fit',
      borderRadius: 2.5,
      borderColor: 'stone-300',
      bgColor: 'white',
      color: 'stone-900',
      hover: { borderColor: 'stone-500' },
      focus: { borderColor: 'stone-900', outlineColor: 'stone-200' },
      theme: {
        dark: {
          bgColor: 'stone-950',
          color: 'stone-100',
          borderColor: 'stone-700',
          hover: { borderColor: 'stone-500' },
          focus: { borderColor: 'stone-200', outlineColor: 'stone-800' },
        },
      },
    },
    children: {
      items: {
        styles: {
          borderRadius: 3,
          borderColor: 'stone-200',
          color: 'stone-900',
          theme: { dark: { bgColor: 'stone-950', borderColor: 'stone-800', color: 'stone-100' } },
        },
      },
      item: {
        styles: {
          borderRadius: 2,
          hover: { bgColor: 'stone-100' },
          focus: { bgColor: 'stone-100' },
          selected: { bgColor: 'stone-100', hover: { bgColor: 'stone-200' } },
          theme: {
            dark: {
              hover: { bgColor: 'stone-900' },
              focus: { bgColor: 'stone-900' },
              selected: { bgColor: 'stone-900', hover: { bgColor: 'stone-800' } },
            },
          },
        },
        variants: { highlighted: { outlineColor: 'stone-900', theme: { dark: { outlineColor: 'stone-200' } } } },
      },
    },
  },
  progress: {
    styles: { height: 0.5, borderRadius: 999, bgColor: 'stone-200', theme: { dark: { bgColor: 'stone-800' } } },
    children: {
      fill: { styles: { borderRadius: 999, bgColor: 'stone-900', theme: { dark: { bgColor: 'stone-100' } } } },
    },
  },
  accordion: {
    children: {
      item: { styles: { borderColor: 'stone-200', theme: { dark: { borderColor: 'stone-800' } } } },
      trigger: {
        styles: {
          py: 4,
          fontSize: 15,
          fontWeight: 500,
          color: 'stone-900',
          hover: { color: 'stone-500' },
          ariaAttr: { expanded: { color: 'stone-900' } },
          focusVisible: { outlineColor: 'stone-900' },
          theme: { dark: { color: 'stone-100', hover: { color: 'stone-400' }, ariaAttr: { expanded: { color: 'stone-100' } } } },
        },
      },
      panel: { styles: { fontSize: 15, lineHeight: 24, color: 'stone-600', theme: { dark: { color: 'stone-400' } } } },
    },
  },
  tabs: {
    children: {
      tab: { styles: { focusVisible: { outlineColor: 'stone-900' } } },
      panel: { styles: { focusVisible: { outlineColor: 'stone-900' } } },
    },
  },
});

/** Spread on a heading: a medium weight and tight tracking, in whatever sans the system already has. */
export const heading = { fontWeight: 500, letterSpacing: -0.5 } as const;

/** The small spaced capitals over a section, beside a badge or under a price. */
export const eyebrow = { fontSize: 12, fontWeight: 500, letterSpacing: 1.5, textTransform: 'uppercase' } as const;

/** A panel on the page: a hairline and soft corners, nothing else. */
export const card = { b: 1, borderRadius: 4, borderColor: 'stone-200', theme: { dark: { borderColor: 'stone-800' } } } as const;

/** Secondary text, in both themes. */
export const muted = { color: 'stone-500', theme: { dark: { color: 'stone-400' } } } as const;

/**
 * Where the pages and blocks link to. A provider rather than constants, so the same pages work under any
 * router: wrap them in `<StoreLinks.Provider value={…}>` with your own routes.
 */
export interface StoreRoutes {
  shop: string;
  product: (slug: string) => string;
  cart: string;
  checkout: string;
}

export const StoreLinks = createContext<StoreRoutes>({
  shop: '/',
  product: (slug) => `/products/${slug}`,
  cart: '/cart',
  checkout: '/checkout',
});

export const useStoreLinks = () => useContext(StoreLinks);

/** The design and the page colours, with no theme of its own: it follows whatever theme is around it. */
export function StoreSurface({ children }: { children: ReactNode }) {
  return (
    <Box.Components value={storeDesign}>
      <Box bgColor="white" color="stone-900" theme={{ dark: { bgColor: 'stone-950', color: 'stone-100' } }}>
        {children}
      </Box>
    </Box.Components>
  );
}

/**
 * A whole store page: its own light/dark switch, remembered under its own key, around `StoreSurface`.
 * Without `theme` it opens in the reader's saved choice, then their system's.
 */
export function StoreTheme({ theme, children }: { theme?: string; children: ReactNode }) {
  return (
    <Box.Theme theme={theme} storageKey="still-theme">
      <StoreSurface>
        <Box minHeight="fit-screen">{children}</Box>
      </StoreSurface>
    </Box.Theme>
  );
}
```

The command installs the rest of it beside that file — `store-data.ts`, `product-art.tsx`, `quantity-stepper.tsx`, `form-field.tsx`, `store-shell.tsx` — and [the repository](https://github.com/box-kite/box-kite/tree/main/registry/designs/store) has every file.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

