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
Ten 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". |
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.configure() | Tell the engine how to name classes, where to write rules and what the base class transitions. Call it once, before the first render. |
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. |
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.
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.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 })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.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.