Appearance

A design for the whole site, in light or dark. Kept on this device.

Design
Mode

Box functions

Everything Box carries that is not a prop: add props of your own, name a set of styles, register keyframes, sample a spring, switch themes and configure the engine — with the smallest real example of each.

The whole surface

Twelve names, and you will use two of them. Box.components() is how a design system is written here, and Box.extend() is how the prop set grows. The other eight are for the day you need them.
NameWhat it is for
Box.extend()Add CSS variables, props of your own, and extra values on props that already exist. Afterwards they are indistinguishable from built-ins.
Box.components()Name a set of styles — with variants, named parts and inheritance — and wear it with component="card". With { design: "ember" } it is a second design, chosen by data-design="ember" in CSS.
Box.palette()Point the two families the components are drawn in — accent and base — at another family or a ramp of your own, for an element and everything inside it.
Box.sizes()Say what compact, normal and large are in this project, once on the root: every component is drawn at one of the three, and { size: "compact" } draws a whole subtree at one.
Box.createComponents()The same shape as Box.components(), registered nowhere: a design for one subtree, so two designs can share a page.
Box.ComponentsEvery Box below resolves its component against that design instead of the app’s. Box.Theme takes it as components={…}.
Box.keyframes()Register an @keyframes sequence whose steps are Box props. Nothing is emitted until a rule names it.
Box.spring()Sample a damped oscillator into { easing, duration } — the two halves the timing-function and duration props take. No runtime.
Box.viewTransition()Run a DOM change inside a view transition where the browser has one, and plainly where it has not — the same three promises either way.
Box.ThemeThe provider that writes a theme class onto <html> or onto a wrapper of its own, follows the system preference and persists a choice.
Box.useTheme()Read the theme the nearest provider settled on, and set it. Passing null hands control back to the system preference.
Box.useDesign()Read the design the nearest provider draws in — a Box.components(…, { design }) name, written as data-design — and switch it. Passing null goes back to its design prop.
Box.configure()Tell the engine how to name classes, where to write rules and what the base class transitions, and whether components mark themselves with data-bk. Call it once, before the first render.
Box.onInteraction()Hear every interaction in one typed stream — a dialog opened and why, a tab chosen, a DataGrid sorted, a press on anything a component styled — as plain JSON, fire and forget, for analytics or an audit log. What a person typed is left out unless the listener passes { content: true }, and @box-kite/react/analytics sends the stream to a collector with a session id on every record.
Box.reportInteraction()Report a moment of the app's own — a hover, a page view, a custom step — to the same Box.onInteraction listeners, fire and forget.
Box.flushInteractions()Deliver what is queued now, rather than in the next background task: what an analytics sender calls before its last batch, and a test before it asserts.
Box.getVariableValue()The var(--…) reference behind a token, declared on first use — for handing a themed colour to something that takes a string.
useClassNames()Box props as a class attribute, for an element Box cannot render: a router’s NavLink, a motion.div, an icon from another library.
useBoxElement()Box’s render as a hook, for a component of your own that is a Box with defaults — one fiber rather than a wrapper around a second component.
useVisibility()Open/closed state that closes itself on an outside press, on Escape, and optionally on scroll or resize.
Everything is registered on one engine, shared by the whole app, so these are called once at module scope — not in a component, not in an effect. Register before the first render and nothing has to be re-emitted. The one design that stops at the edge of a subtree is Box.createComponents(), which registers nothing.

Box.components()

Box.components({ card: { styles, variants, children, extends } })
A named set of styles with variants, named parts and inheritance. An element wears it with component="card", and props written on that element still win — a default is a starting point, not a lock. It is how every pre-built component here is styled, so restyling button or datagrid is the same call.
Register a card, and two variants of it
JSX
// boxExtends.ts
import Box from '@box-kite/react';

export const components = Box.components({
  card: {
    styles: {
      p: 5,
      borderRadius: 3,
      b: 1,
      bgColor: 'white',
      borderColor: 'slate-200',
      theme: { dark: { bgColor: 'slate-800', borderColor: 'slate-700' } },
    },
    variants: {
      danger: { borderColor: 'rose-400', bgColor: 'rose-50' },
      flat: { b: 0, shadow: 'none' },
    },
    children: {
      title: { styles: { fontSize: 16, fontWeight: 600, mb: 2 } },
    },
  },
});
Wear it
JSX
<Box component="card" variant="danger" p={6}>
  <Box component="card.title">Payment failed</Box>
  We could not charge the card on file.
</Box>

A name is not a type until you say so

variant="danger" is a type error until a declare module hands TypeScript what you registered — the names exist at runtime only. One file, written once, and every later Box.components() call it exports from is picked up automatically.
box.d.ts — the one file that teaches the names
JSX
import '@box-kite/react';
import { ExtractComponentsAndVariants } from '@box-kite/core/types';
import { components } from './boxExtends';

declare module '@box-kite/core/types' {
  namespace Augmented {
    interface ComponentsTypes extends ExtractComponentsAndVariants<typeof components> {}
  }
}
In depth: Theme Setup

Box.createComponents()

const aurora = Box.createComponents({ button: { styles } }, { base })
Box.components() styles the whole app, so two designs cannot share a page with it: a later call merges into the earlier one. Box.createComponents() takes the same shape and registers nothing — it returns a design, and <Box.Components value> applies it to everything inside. A white-label app with a design per tenant, a redesign landing one screen at a time, a page of templates that each look different: one stylesheet, and each design ends where its subtree does.
Two designs, one page
JSX
import Box from '@box-kite/react';
import Button from '@box-kite/react/components/button';

const pill = Box.createComponents({ button: { styles: { borderRadius: 999, px: 6 } } });
const square = Box.createComponents({ button: { styles: { borderRadius: 0 } } }, { base: 'builtin' });

export default function Designs() {
  return (
    <>
      <Box.Components value={pill}>
        <Button>Pill</Button>
      </Box.Components>
      <Box.Theme theme="dark" components={square}>
        <Button>Square, dark</Button>
      </Box.Theme>
    </>
  );
}

Where a design starts, and what still wins

base is what the design is merged over: 'global' (the default) is the app's own Box.components(), followed as it changes; 'builtin' ignores the app's registrations; another registry chains onto that design. Props written on an element still beat the design, the way they beat Box.components(). A design is identified by its content, so building one inline costs nothing — but module scope is where it reads best. A Server Component cannot see the provider; a client one under it can.
box.d.ts — two designs that both name button
JSX
import '@box-kite/react';
import { MergeComponentsAndVariants } from '@box-kite/core/types';
import { aurora, harbor } from './designs';

declare module '@box-kite/core/types' {
  namespace Augmented {
    interface ComponentsTypes extends MergeComponentsAndVariants<[typeof aurora, typeof harbor]> {}
  }
}

Box.Components

<Box.Components value={aurora}>
The half that applies a design. It renders no element, only context, so it can wrap anything — a route, one card, a portal's contents. Box.Theme takes the same registry as components={…}, because a design in both light and dark is the usual case, and value={undefined} hands a subtree back to the app's own design.

Box.extend()

Box.extend(variables, newProps, newValues)
Three arguments, and each one is a different kind of growth: variables declares CSS custom properties, new props adds props the registry does not have, and new values teaches a prop that already exists to accept one more. All three land in the same pipeline as the built-ins — typed, nestable, shared, server-rendered.
A brand colour, a prop of your own, and a new value on an existing prop
JSX
// boxExtends.ts
import Box from '@box-kite/react';

export const { extendedProps, extendedPropTypes } = Box.extend(
  // 1. Variables: declared in :root the first time something uses one.
  { 'brand-500': '#4f46e5', 'grid-gutter': '1.5rem' },
  // 2. Props of your own.
  {
    columnRule: [
      {
        values: ['thin', 'thick'] as const,
        styleName: 'column-rule-width',
        valueFormat: (value: string) => (value === 'thin' ? '1px' : '4px'),
      },
    ],
  },
  // 3. More values on props that already exist.
  {
    bgColor: [{ values: ['brand-500'] as const, styleName: 'background-color', valueFormat: (value, getVariable) => getVariable(value) }],
  },
);
Use them like any other prop
JSX
<Box bgColor="brand-500" columnRule="thick" hover={{ bgColor: 'brand-500/80' }} md={{ columnRule: 'thin' }} />

A variable is a value, not a second system

A token registered here is accepted wherever the palette is, opacity modifier included — "brand-500/80" is a color-mix over the variable, so it stays themeable. The types need the same declare module the components do, with ExtractBoxStyles<typeof extendedProps> for your new props and ExtractBoxStyles<typeof extendedPropTypes> for the new values.
In depth: Style Grouping

Box.keyframes()

Box.keyframes({ slideIn: { from: {…}, to: {…} } })
An @keyframes sequence whose steps are Box props rather than CSS — so a step is written in the same scale, tokens and all. Registering costs nothing: the engine writes a sequence the first time a rule names it.
Register it, then name it
JSX
Box.keyframes({
  slideIn: {
    from: { opacity: 0, translateY: 4 },
    to: { opacity: 1, translateY: 0 },
  },
});
On an element
JSX
<Box animationName="slideIn" animationDuration={300} animationFillMode="both" />
In depth: Animation

Box.spring()

Box.spring({ stiffness, damping, mass, velocity })
Spring physics sampled into a linear() curve and a settling time — the two halves the timing-function and duration props already take. There is no runtime: the spring is a value, so it shares a class like any other and costs nothing per frame.
A spring of your own — the curve on one prop, the settling time on the other
JSX
const bouncy = Box.spring({ stiffness: 220, damping: 12 });

<Box transitionTimingFunction={bouncy.easing} transitionDuration={bouncy.duration} hover={{ scale: 1.05 }} />;
Two of the four named ones — hover either
Hover: spring-bouncy
Hover: spring-gentle
JSX
<Box transitionTimingFunction="spring-bouncy" transitionDuration="spring-bouncy" hover={{ scale: 1.08 }}>
  Hover: spring-bouncy
</Box>
<Box transitionTimingFunction="spring-gentle" transitionDuration="spring-gentle" hover={{ scale: 1.08 }}>
  Hover: spring-gentle
</Box>

Four are already named

spring, spring-gentle, spring-bouncy and spring-snappy are values on the timing-function and duration props, so the common case needs no call at all — and their durations are multiples of --transitionTime, which reduced motion sets to zero.
In depth: Animation

Box.Theme

<Box.Theme use="global" storageKey="theme" globalStyles={…}>
The provider. It reads prefers-color-scheme, follows it live, persists an explicit choice, and writes the theme class and data-theme onto <html> (use="global") or onto a wrapper of its own (use="local", the default). Switching costs no re-render — it is one class moving on an ancestor.
Once, at the root
JSX
<Box.Theme use="global" storageKey="theme" globalStyles={{ colorScheme: 'light dark' }}>
  <App />
</Box.Theme>

A theme name is any identifier

light and dark are only the two you get for free. A theme={{ midnight: … }} block on any element works the moment a provider above it is called midnight, and a nested provider owns its own subtree outright.
In depth: Theme Setup

Box.useTheme()

const [theme, setTheme] = Box.useTheme()
Read what the nearest provider settled on, and set it. Passing null clears the stored choice and hands control back to the operating system.
A theme toggle
JSX
function ThemeToggle() {
  const [theme, setTheme] = Box.useTheme();

  return <Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme === 'dark' ? 'Light' : 'Dark'}</Button>;
}
In depth: Theme Setup

Box.configure()

Box.configure({ classNames, sink, transition, componentAttribute })
How the engine names classes, where it writes rules, and what the base class transitions. The defaults suit a browser and a Node server alike, so most apps never call it — reach for it to make class names reproducible across processes, or to turn the base transition off.
Before the first render, once
JSX
Box.configure({
  // 'hashed' (default), 'readable' for tests, or 'stable' — content-hashed, so two processes agree.
  classNames: 'stable',
  // 'cssom' | 'textContent' | 'string' | 'element'. Defaults to the environment.
  sink: 'cssom',
  // What every Box transitions by default — a transition group, or false to declare nothing at all.
  transition: 'colors',
});

It re-emits everything

Changing the sink or the base transition after rules have been written drops the class-name cache and writes them all again, which is why this belongs at module scope and not in a component.

Box.getVariableValue()

Box.getVariableValue("sky-500")
The var(--…) reference behind a palette token, declaring it in :root the first time it is asked for. For handing a themed colour to something that takes a plain string — a canvas, a chart library, an SVG attribute somebody else renders.
A token as a string
JSX
const stroke = Box.getVariableValue('sky-500'); // 'var(--sky-500)'

For markup you do render, vars is the prop

vars={{ 'color-revenue': 'sky-500' }} declares --color-revenue on an element and everything inside it, so it nests in a theme and a breakpoint like any other prop. Reach for getVariableValue only when a string is genuinely what is wanted.

useClassNames()

const { className, styles } = useClassNames(props)
Box props as a className, for an element Box cannot render: a router’s NavLink, a motion.div, a component from another library that takes a class and nothing else. All the nesting works — hover, breakpoints, themes — because it is the same resolution Box does.
Styling somebody else's component
JSX
import { useClassNames } from '@box-kite/react';

function Crumb({ to, children }: { to: string; children: React.ReactNode }) {
  const { className, styles } = useClassNames({ color: 'sky-600', hover: { color: 'sky-400' } });

  return (
    <>
      {styles}
      <NavLink to={to} className={className}>
        {children}
      </NavLink>
    </>
  );
}

Render styles either way

styles is defined in element mode only, where the CSS travels as <style> elements React hoists. Everywhere else it is undefined and rendering it costs nothing — so that line is what to write in both.

useBoxElement()

return useBoxElement({ p: 4, ...props }, ref)
Box’s own render, as a hook. A component that is a Box with some defaults calls it from its own forwardRef instead of returning <Box>, so React sees one component rather than two — which is how Flex, H1 and Button are written. Defaults go first, so the caller’s props win.
A component that is a Box
JSX
import { forwardRef } from 'react';
import { BoxProps, useBoxElement } from '@box-kite/react';

const Card = forwardRef<HTMLDivElement, BoxProps>((props, ref) =>
  useBoxElement({ p: 4, borderRadius: 3, b: 1, borderColor: 'gray-200', ...props }, ref),
);

Wrap it in memo if it is a leaf

The pre-built components are each one memoized fiber, which is what lets a leaf whose props are all primitives skip a parent’s re-render entirely. A component of your own gets the same by wrapping the forwardRef in memo — worth it for anything rendered many times with unchanging props.

useVisibility()

const [visible, setVisible, ref] = useVisibility(options)
Open/closed state that closes itself: an outside press, Escape, and optionally a scroll or a resize. The ref goes on the element that counts as inside.
A panel that dismisses itself
JSX
const [isVisible, setVisible, ref] = useVisibility<HTMLDivElement>({ hideOnScroll: true });

<Box ref={ref}>
  <Button onClick={() => setVisible(!isVisible)}>Filters</Button>
  {isVisible && <Box p={4}>…</Box>}
</Box>;

For a real layer, reach past it

A dismissable panel that also needs focus return, layering or the top layer is Popover, and the primitive under it is useDismiss from @box-kite/react/a11y. useVisibility is the small case: a disclosure that owns nothing else.