Paper
betav0.1.0Neutral, non-interactive surface/container with 4 elevation depths (0-3) mapped onto theme.css's existing `.layer-0`…`.layer-3` token family — the styled-container equivalent of MUI's Paper. Pure presentation: no Radix primitive exists for a static surface (none was expected — Paper.rfc.md's own front matter), no shadow (none observed in Figma; §3.4/§9 arbitration #2), and a single always-on look (94% of measured usage already pinned MUI's variant="outlined", so there is no separate `variant`/`square` prop — §2.3/§4). Ships with a decorative, non-gating border reproducing the 10%-diluted border color read from Figma (§3.4/§9 arbitration #3) and a `padding` prop on an 8px pixel scale — 0/8/16/24/32, default 24, the annotated Figma value (§3.3/§4.5). `title` and `action` render an optional header row ABOVE the surface, outside its border and padding, where all 281 measured product sites already put it (§4.6). No colour prop: the surface paints the shared `--bg-elevation-default` alias, so a product recolours it by redeclaring `--bg-elevation-default-layer-N`, exactly as it recolours the Navbar and the Header (§6.5).
Variants
Examples
<Paper>Default surface — elevation 1, div.</Paper><Paper elevation={0}>Flush with the page background.</Paper><Paper padding={16}>16px — the value 31/69 measured MUI sites picked via sx={{ p: 2 }}.</Paper><Paper
title="Scenario settings"
action={<Button priority="tertiary">Edit</Button>}
padding={16}
>
<Text variant="content-compact">Free children. The header row renders ABOVE the surface,
outside its border and padding — omit both slots and Paper is a single element, as before.</Text>
</Paper>{/* A heading level is never invented: pass the element when you need the semantics. */}
<Paper title={<h2 className="m-0">Injects</h2>}>Body</Paper><Paper elevation={3} className="max-w-sm">
<Text variant="content-base-bold">Highest elevation</Text>
<Text variant="content-compact" className="text-default-secondary">
Use sparingly — reserve level 3 for the most prominent nested surface.
</Text>
</Paper><Paper
as="a"
href="/report/42"
elevation={2}
className="focus-visible:outline-none focus-visible:ring-2 ring-focus focus-visible:ring-offset-2 focus-visible:ring-offset-focus"
>
Whole-surface clickable card — migrated from MUI's <Paper component={Link} to="...">. The
focus ring above is required, not optional: Paper adds none of its own (RFC §5.3),
so any `as`-rendered focusable element must supply it.
</Paper>Props
| Prop | Type |
|---|---|
elevation | 0 | 1 | 2 | 3 — surface depth, maps 1:1 to theme.css's .layer-0….layer-3 token family. Default 1 (the one Figma-observed level). |
padding | 0 | 8 | 16 | 24 | 32 — surface padding in PIXELS, default 24 (the annotated Figma value, so omitting it changes nothing). Px rather than none/sm/md/lg/xl because every migrating site is reading a literal MUI sx={{ p: 2 }} = 16px (§4.5). All five classes are safelisted in src/tokens/index.css, so they exist in the shipped dist/index.css whatever the rest of the library uses — before that, p-8 was measurably absent and className="p-8" silently rendered at 24px. An off-scale value still works through className (tailwind-merge lets it win), but only these five carry the emission guarantee. |
title | ReactNode — optional heading content, rendered in a header row ABOVE the surface (outside its border and padding), where all 281 measured product sites put it today. NO heading level is applied: a string renders in a plain div with the library's title typography, because inventing an <h2> is a WCAG 1.3.1 heading-order regression rather than a neutral default — pass the element when you need the semantics (title={<h2>…</h2>}). BREAKING: `title` used to fall through to the native HTML title attribute, i.e. a browser tooltip over the whole panel. |
action | ReactNode — optional trailing content for the same header row (buttons, a menu, a chip), right-aligned by justify-between. Works with or without `title`. BREAKING: `action` used to reach the DOM as action="[object Object]". |
as | React.ElementType — element/component to render, default "div". Never affects appearance (Text.tsx precedent — one polymorphism convention across the library). Targets the SURFACE, including when a title/action header is present, so <Paper as="a" href="…" title="…"> keeps the surface as the link. Migrating from MUI: component → as. |
className | string — Additional classes, merged via tailwind-merge. Applied to the surface, and it still wins over `padding` (e.g. an off-scale p-3). |
children | ReactNode — Surface content, free-form |
Accessibility
RFC §9 arbitration #3. The border is DECORATIVE and non-gating, by explicit arbitration: a static, non-interactive container is not a WCAG 1.4.11 "UI component" until a consumer renders it interactive (precedent: Button's redundant border, Chip's decorative icon) — border-* classes are outside check-contrast.ts's Rule 5 scope entirely (bg-/text-/ring- only), consistent with treating it as non-gating. Usage rules (RFC §6) state the border must never be the sole differentiator between adjacent surfaces — pair elevation changes with spacing/layout. No shadow at any level (RFC §3.4/§9 arbitration #2). Elevation levels 0/2/3 are not individually Figma-verified at the component level (only level 1 has a directly observed Figma instance across all 3 given nodes) — the underlying theme.css token family is still Figma-sourced (generated from Figma variables); it is specifically the 4-level Paper composition that has not been drawn as Figma variants yet (RFC §9 arbitration #1). All 8 pairs above compute well above their 4.5 minimum in both modes (pnpm check:contrast). The optional title/action header row renders OUTSIDE the surface, so its --text-default-primary sits on whatever is behind Paper — the page background, i.e. --bg-elevation-default at layer 0. That exact pair is already the first entry above, so the row introduces no undeclared combination; it introduces no new color token at all. The row applies NO heading level: a string title renders in a plain div, because inventing an <h2> would be a WCAG 1.3.1 heading-order regression rather than a neutral default (same reasoning as Paper claiming no ARIA role of its own, §5.1) — a consumer needing the semantics passes the element. Host theming (§6.5) changes the surface color at runtime, so a product that redeclares --bg-elevation-default-layer-N owns re-verifying contrast against ITS value: the gate resolves this library's theme.css, not a customer's override. Stated here because it is exactly the kind of scope claim a green gate is otherwise read as covering.