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.| Name | What 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.Components | Every 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.Theme | The 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.
In depth: Server Components
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.