Skip to main content

Paper

betav0.1.0

Neutral, 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

The four elevation levelselevation maps 1:1 onto theme.css's own .layer-0….layer-3 context classes, and changes nothing but depth. Only elevation 1 has a directly Figma-observed instance (RFC §3.1, arbitration #1); 0/2/3 rest on the pre-existing, already Figma-sourced token family, and Sandy's visual checkpoint is their practical verification until Figma grows the variants.
elevation={0}Secondary text on this surface.
elevation={1}Secondary text on this surface.
elevation={2}Secondary text on this surface.
elevation={3}Secondary text on this surface.
Nesting — the measured panel-hosting-a-card patternA real usage family from the cross-product audit (RFC §2): an elevation-1 panel hosting a more prominent surface. Mirrors Material's dark-theme convention of lightening rather than shadowing a nested surface (RFC §6.2) — there is no shadow at any level (arbitration #2).
Panel (elevation 1)Panels commonly host a nested, more elevated surface — e.g. a highlighted summary card.
Highlighted card (elevation 3)Nested Paper — elevation differentiates it from its elevation-1 parent.
Polymorphism — the clickable card (as="a")The measured clickable-card pattern (RFC §2.3/§4.2 — 4/113 real sites doing this through MUI’s component={Link}). Paper adds NO focus-ring scaffolding of its own (RFC §5.3), so rendering it as a focusable element makes the standard ring the consumer’s responsibility — spelled out here rather than shipping a silently inaccessible example. `as` targets the surface, including when a title/action header is present.Clickable card (as="a")The whole surface is a real, focusable, navigable link.

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

PropType
elevation0 | 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).
padding0 | 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.
titleReactNode — 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.
actionReactNode — 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]".
asReact.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.
classNamestring — Additional classes, merged via tailwind-merge. Applied to the surface, and it still wins over `padding` (e.g. an off-scale p-3).
childrenReactNode — Surface content, free-form

Accessibility

2.1 AAWCAG pass

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.