Browse docs
Browse docs
The surface around content. <Box> collapses MUI's Box + Paper + Card + Joy's Surface into a single component with five visual variants and seven intent colors. Need a card? <Box variant="outlined">. Need a callout? <Box variant="soft" color="warning">. Need a hero banner? <Box variant="solid" color="primary">. Same component, configured.
import { Box, Typography } from '@dashforge/tw';
<Box variant="outlined" rounded="xl" p={6}>
<Typography variant="h3">Workspace settings</Typography>
</Box>{/* Plain — a typed div with padding. The baseline. */}
<Box p={6}>Basic container</Box>
{/* Outlined — light border + subtle bg. The classic "card". */}
<Box variant="outlined" rounded="lg" p={6}>
Card-like surface
</Box>
{/* Elevated — floats with a shadow. */}
<Box variant="elevated" elevation={3} rounded="xl" p={8}>
Floating panel
</Box>Box has one job: be the surface around content. It's not a flex container, not a grid container, not a paragraph. The strict separation between "surface" (Box) and "layout" (Stack, Grid) is intentional — when every <div> in an app gravitates back to Box, the distinction collapses and code becomes harder to read.
| If you need... | Use | NOT |
|---|---|---|
| A container with padding, border, shadow, or background | <Box> | — |
| To arrange children in a row or column with gap | <Stack> | <Box> |
| To arrange children in a 2D grid with columns | <Grid> | <Box> |
| Styled text | <Typography> | <Box> |
| Absolute positioning, overflow control, animation | sx prop on any of the above | shorthand props on Box |
Box's job stops where layout starts. This is why Box has no display, no flex*, no grid* props — and never will.
solid · primary
outlined · neutral
plain · no chrome
import { Box, Typography, Stack } from '@dashforge/tw';
<Stack direction="row" gap={3} wrap justify="center">
<Box variant="solid" color="primary" p={4} rounded="md">
<Typography variant="body2">solid · primary</Typography>
</Box>
<Box variant="outlined" p={4} rounded="md">
<Typography variant="body2">outlined · neutral</Typography>
</Box>
<Box variant="plain" p={4} rounded="md">
<Typography variant="body2" color="muted">plain · no chrome</Typography>
</Box>
</Stack><Box variant="plain" p={4}>plain — just padding and radius</Box>
<Box variant="outlined" color="neutral" rounded="lg" p={4}>outlined neutral</Box>
<Box variant="elevated" elevation={2} rounded="lg" p={4}>elevated, shadow=2</Box>
<Box variant="soft" color="info" rounded="lg" p={4}>soft info — semi-transparent intent</Box>
<Box variant="solid" color="primary" rounded="lg" p={4}>solid primary — filled CTA</Box>| Variant | When to use |
|---|---|
plain (default) | Bare container — padding/radius wrapper inside another card |
outlined | "Card lite" — settings panel, form section, sidebar group |
elevated | Floating UI — modals, dropdowns, hover cards |
soft | Callouts, info boxes, badges — semi-transparent tint |
solid | CTA banners, hero panels, brand surfaces — filled |
{/* outlined per intent */}
<Box variant="outlined" color="primary" p={4}>Primary outlined</Box>
<Box variant="outlined" color="success" p={4}>Success outlined</Box>
<Box variant="outlined" color="warning" p={4}>Warning outlined</Box>
<Box variant="outlined" color="danger" p={4}>Danger outlined</Box>
{/* soft per intent */}
<Box variant="soft" color="primary" p={4}>Primary soft</Box>
<Box variant="soft" color="success" p={4}>Success soft</Box>
<Box variant="soft" color="warning" p={4}>Warning soft</Box>
<Box variant="soft" color="danger" p={4}>Danger soft</Box>
{/* solid per intent */}
<Box variant="solid" color="primary" p={4}>Primary solid</Box>
<Box variant="solid" color="success" p={4}>Success solid</Box>
<Box variant="solid" color="warning" p={4}>Warning solid</Box>
<Box variant="solid" color="danger" p={4}>Danger solid</Box>All 21 surface × intent combos are pre-resolved at the variant level — dark-mode pairs (lighter shade for primary, darker bg for surface) are wired without you thinking about it.
elevation 1
elevation 3
elevation 5
import { Box, Typography, Stack } from '@dashforge/tw';
<Stack direction="row" gap={4} wrap justify="center" align="center">
<Box variant="outlined" elevation={1} p={4} rounded="md">
<Typography variant="body2">elevation 1</Typography>
</Box>
<Box variant="outlined" elevation={3} p={4} rounded="md">
<Typography variant="body2">elevation 3</Typography>
</Box>
<Box variant="outlined" elevation={5} p={4} rounded="md">
<Typography variant="body2">elevation 5</Typography>
</Box>
</Stack><Box variant="elevated" elevation={0} rounded="md" p={4}>elevation 0 — no shadow</Box>
<Box variant="elevated" elevation={1} rounded="md" p={4}>elevation 1 — shadow-sm</Box>
<Box variant="elevated" elevation={2} rounded="md" p={4}>elevation 2 — shadow</Box>
<Box variant="elevated" elevation={3} rounded="md" p={4}>elevation 3 — shadow-md</Box>
<Box variant="elevated" elevation={4} rounded="md" p={4}>elevation 4 — shadow-lg</Box>
<Box variant="elevated" elevation={5} rounded="md" p={4}>elevation 5 — shadow-xl</Box>Elevation lives on a separate axis from variant so a "subtle elevated card" (elevation=1) and a "modal-like elevated card" (elevation=4) share the same variant="elevated".
<Box p={4}>uniform padding 4</Box>
<Box px={6} py={2}>asymmetric — px=6, py=2</Box>
<Box m={2}>uniform margin 2</Box>
<Box mx="0.5">tight horizontal margin</Box>Accepted steps (mirror @dashforge/tw-tokens spacing scale):
0 · '0.5' · 1 · 2 · 3 · 4 · 6 · 8 · 12 · 16 · 24
Note '0.5' is a string because 0.5 would lose precision in object keys.
<Box fullWidth p={4}>Stretches to container width</Box>
<Box fullHeight p={4}>Stretches to container height</Box>For custom dimensions (w-64, h-screen, max-w-3xl), use sx — keeping the shorthand props limited to the two patterns used in ~80% of layouts.
The canonical pattern:
<Box variant="outlined" rounded="xl" p={6}>
<Stack gap={3}>
<Typography variant="h3">Workspace settings</Typography>
<Typography variant="body2" color="muted">
Control access and notifications.
</Typography>
</Stack>
</Box>Box paints the surface. Stack handles the gap. Typography styles the text. Three concerns, three components, zero overlap.
<Box variant="solid" color="primary" rounded="2xl" p={12} fullWidth>
<Typography variant="h1" color="inherit" gutterBottom>
Get started today
</Typography>
<Typography variant="body1" color="inherit">
Two minutes from install to your first component on screen.
</Typography>
</Box>Solid variants set text-white on the container; nested Typography with color="inherit" reads cleanly without per-component prop.
as and asChild{/* `as` — change the tag but keep all Box styles */}
<Box as="section" variant="outlined" p={6}>
Semantic section with card chrome
</Box>
{/* `asChild` — paint Box styles onto an existing element (no extra DOM) */}
<Box variant="elevated" elevation={2} rounded="lg" p={6} asChild>
<article>
<Typography variant="h3">Article-shaped card</Typography>
</article>
</Box>Both as and asChild passed → asChild wins (same rule as Typography).
access and visibleWhenSprint 4.4 added Dashforge's universal bridge contract to Box — the same access and visibleWhen props you'd find on Button, Menu, Chip, IconButton, Card and the form atoms. Use them when a whole surface should appear, disappear, or grey out based on the viewer's role or a reactive form value.
{/* Role-based card — only visible to admins.
The Box (and everything inside) returns null for non-admins. */}
<Box
variant="outlined"
rounded="xl"
p={6}
access={{ requires: 'workspace.admin', when: 'denied:hide' }}
>
<Stack gap={3}>
<Typography variant="h3">Danger zone</Typography>
<Typography variant="body2" color="muted">
Workspace-level destructive actions live here.
</Typography>
<Button color="danger">Delete workspace</Button>
</Stack>
</Box>
{/* Reactive surface — only mounts when the form field "country" is "US".
Engine-aware; no useState/useEffect bookkeeping on the consumer side. */}
<Box
variant="soft"
color="info"
rounded="lg"
p={4}
visibleWhen={(engine) => engine.values.country === 'US'}
>
<Typography variant="body2">
Tax ID required for US-based workspaces.
</Typography>
</Box>The three access.when modes work identically to Button — 'denied:hide' (default, drops the subtree), 'denied:disable' (renders dimmed + non-interactive), 'denied:readonly' (renders normally but click handlers short-circuit). The retrofit is additive: existing <Box> usages without these props compile and render unchanged.
sxFor utility classes Box deliberately doesn't expose as props (overflow, position, cursor, transitions):
<Box variant="elevated" elevation={2} rounded="lg" p={6}
sx="overflow-hidden cursor-pointer transition-shadow hover:shadow-xl">
Hover me — Box shorthand handles the surface, sx handles the interaction
</Box>tailwind-merge resolves precedence; your sx always wins over the variant defaults when they conflict.
Configure <Box> variant defaults application-wide through the theme's components field. Consumers who want every Box to default to variant="outlined" or rounded="lg" don't need to touch each instance — they set it once on the theme.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Box: {
defaults: {
variant: 'outlined',
color: 'neutral',
rounded: 'lg',
elevation: 1,
},
},
},
});Configurable axes (BoxVariantProps):
| Axis | Type | Notes |
|---|---|---|
variant | 'plain' | 'outlined' | 'elevated' | 'soft' | 'solid' | Surface treatment. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | Intent role. Applied by outlined / soft / solid; ignored by plain / elevated. |
elevation | 0 | 1 | 2 | 3 | 4 | 5 | Shadow scale, primarily for variant="elevated". |
rounded | 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | Border-radius identity. |
fullWidth | boolean | Stretch to container width. |
fullHeight | boolean | Stretch to container height. |
Not theme-configurable: spacing (p, px, py, m, mx, my) — spacing is a per-instance layout choice, not a component identity. Same for access, visibleWhen, event handlers, ARIA.
Precedence chain (lowest → highest):
defaultVariants from the internal boxVariants recipe (fallback).theme.components.Box.defaults (application-wide override).<Box variant="solid" />) — wins over theme.sx — wins over the resolved variant class via tailwind-merge.TypeScript: theme configuration is fully type-checked. theme.components.Box.defaults autocompletes to the six axes above.
Reactivity: useComponentDefaults('Box') inside <Box> subscribes to the theme store, so a patchTheme call at runtime re-renders every mounted Box with the new defaults.
| Prop | Type | Default | Description |
|---|---|---|---|
access | AccessRequirement | — | RBAC requirement — added in 1.1.0 (Sprint 4.4 surface alignment). Controls Box visibility / disabled-state via the centralized @dashforge/rbac policy engine.- onUnauthorized: 'hide' → Box does not render - onUnauthorized: 'disable' → adds aria-disabled + dimmed visual; interactive children should derive their disabled state from data-disabled on the Box or pass their own access - onUnauthorized: 'readonly' → adds aria-readonly; mostly used when Box wraps form sections that propagate readonly to childrenUse for permission-gated dashboard cards, admin-only sections, tenant-aware layouts. The hide path is the common case. |
as | ElementType | — | Override the rendered HTML tag. Defaults to 'div'. Useful when the surface should also carry semantic meaning — <Box as="section"> for a page section, <Box as="article"> for a card-shaped article.Ignored when asChild is true. |
asChild | boolean | — | Render via Radix Slot — the Box styles paint onto the single React child instead of wrapping it in our own element. Useful for <Box asChild><Link>...</Link></Box> to get a styled router link with no extra DOM wrapper.Mutually exclusive with as (when both are passed, asChild wins — see Box.tsx for the reasoning). |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'neutral' | Intent role. Applied by outlined / soft / solid; ignored by plain / elevated where color is not part of the surface identity. |
elevation | 0 | 1 | 2 | 3 | 4 | 5 | 0 | Shadow scale — primarily useful with variant='elevated'. 0 removes the shadow entirely. |
fullHeight | boolean | false | Stretch to fill the container's height (h-full). |
fullWidth | boolean | false | Stretch to fill the container's width (w-full). |
m | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Uniform margin — spacing token step. |
mx | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Horizontal margin — spacing token step. |
my | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Vertical margin — spacing token step. |
p | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Uniform padding — spacing token step. |
px | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Horizontal padding — spacing token step. |
py | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Vertical padding — spacing token step. |
rounded | 'md' | 'sm' | 'lg' | 'none' | 'xl' | '2xl' | 'full' | 'none' | Border-radius identity. |
sx | string | — | Utility classes appended to the variant chain. Resolved via tailwind-merge so the consumer's classes always win over the variant defaults. Use for one-off overrides AND for utility dimensions Box deliberately doesn't expose as props (overflow, position, animation, etc.). |
variant | 'outlined' | 'soft' | 'solid' | 'plain' | 'elevated' | 'plain' | Surface treatment: plain (transparent), outlined (bordered), elevated (shadow), soft (tinted bg), solid (filled). |
visibleWhen | (engine: Engine) => boolean | — | Reactive visibility predicate — added in 1.1.0 (Sprint 4.4 surface alignment). When the predicate returns false, the component renders null. Inside a <DashForm> it subscribes reactively to engine state changes; outside a form, it's evaluated as a plain closure.Use for state-driven hiding of dashboard cards, conditional sections, gated content, etc. For permission-driven hiding, prefer access with onUnauthorized: 'hide' — it's semantically the correct path and surfaces better in RBAC dev tools. |
...rest | Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'color'> | — | Additional native attributes forwarded via Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'color'>. |
SpacingStep: 0 \| '0.5' \| 1 \| 2 \| 3 \| 4 \| 6 \| 8 \| 12 \| 16 \| 24 (mirror of the @dashforge/tw-tokens spacing scale).
| Concern | Use instead |
|---|---|
display, flex*, grid*, gap | <Stack> or <Grid> |
position, top, right, bottom, left, z-index | sx |
overflow, cursor | sx |
transition, transform, animation | sx |
Width/height beyond fullWidth/fullHeight | sx (e.g. sx="max-w-3xl w-96") |
<Box> has no slots. It renders a single DOM element (defaulting to <div>, overridable via as or asChild) — there is no anatomy to target separately.
The slotProps API pattern only exists on compound components with multiple visually distinct regions. Examples: <Menu>, <DataGrid>, <Autocomplete>, <Card>.
For <Box>, use sx (documented in the Props table above) for any style override. When you need a richer style surface, wrap Box in your own domain component — the boxVariants export makes that trivial.
import { boxVariants } from '@dashforge/tw';
// boxVariants({ variant: 'outlined', color: 'primary', p: 4 }) → className stringUseful for app-level extensions (e.g. a <StatsCard> that wraps Box with a fixed variant chain).
variant × color compound: 21 visually-distinct combos resolved at the TV level (7 intents × 3 colored variants — outlined/soft/solid). plain and elevated ignore color visually.dark: counterpart baked in (lighter shade for solid bg, darker bg for soft surface). No double-prop dance.elevated is color-agnostic: it always paints a neutral surface (bg-white dark:bg-neutral-900) + shadow. If you need an "elevated + primary tint", use variant="outlined" color="primary" and stack a shadow via sx.<Typography> not <Box>. Box has no text-* defaults.asChild wrapping a Button if you really need a custom shape).