Surface
The layer primitive. A Surface paints a scope base and re-themes everything inside it — set an elevation and the surfaces, borders, and controls within re-derive to that level, so a panel's relationship to its contents holds at every depth.
Elevation
elevation establishes a scope and stamps data-elevation — elevated, menu, or sunken — and every role inside re-derives to that level. Omit it for the root canvas. The same panel and the same secondary button repeat at each level below; the level changes, the panel-to-control relationship holds.
import { Button, Surface } from "@stridge/noctis";
export default function SurfaceElevation() {
return (
<div className="grid w-full max-w-md grid-cols-2 gap-3">
<Surface bordered className="flex flex-col gap-3 rounded-md p-4">
<span className="font-mono text-mini tracking-wide text-subtle uppercase">root</span>
<Button variant="secondary" size="sm" fullWidth>
Action
</Button>
</Surface>
<Surface elevation="elevated" bordered className="flex flex-col gap-3 rounded-md p-4">
<span className="font-mono text-mini tracking-wide text-subtle uppercase">elevated</span>
<Button variant="secondary" size="sm" fullWidth>
Action
</Button>
</Surface>
<Surface elevation="menu" bordered className="flex flex-col gap-3 rounded-md p-4">
<span className="font-mono text-mini tracking-wide text-subtle uppercase">menu</span>
<Button variant="secondary" size="sm" fullWidth>
Action
</Button>
</Surface>
<Surface elevation="sunken" bordered className="flex flex-col gap-3 rounded-md p-4">
<span className="font-mono text-mini tracking-wide text-subtle uppercase">sunken</span>
<Button variant="secondary" size="sm" fullWidth>
Action
</Button>
</Surface>
</div>
);
}
Shade & shadow
shade picks which ramp tier to paint — base (the control-safe default), sunken, surface, or raised — independent of the elevation scope. Pair it with bordered for the standard 1px outline and shadow (card, popover, or modal) to lift the panel off the page.
import { Surface } from "@stridge/noctis";
export default function SurfaceShadeAndShadow() {
return (
<div className="grid w-full max-w-md grid-cols-2 gap-4">
<Surface shade="sunken" className="rounded-md p-4 text-small text-muted">
Sunken well
</Surface>
<Surface shade="raised" bordered className="rounded-md p-4 text-small text-muted">
Raised tier
</Surface>
<Surface shade="surface" shadow="card" className="rounded-md p-4 text-small text-muted">
Card shadow
</Surface>
<Surface shade="surface" shadow="popover" className="rounded-md p-4 text-small text-muted">
Popover shadow
</Surface>
</div>
);
}
Escape hatch
When you can't wrap an element in a <Surface> — a list item, a foreign component's root — spread Surface.props({ shade, bordered, shadow, elevation }) onto it (the D12 escape hatch). It returns a { "data-slot": "noctis-surface", ...dataAttrs } bag the precompiled surface.css keys off, so the element styles as a surface without the wrapper.
- First row
- Second row
- Third row
"use client";
import { Surface } from "@stridge/noctis";
export default function SurfaceEscapeHatch() {
return (
<ul className="flex w-full max-w-sm flex-col gap-2">
{["First", "Second", "Third"].map((label) => (
<li
key={label}
{...Surface.props({ shade: "surface", bordered: true })}
className="rounded-md p-3 text-small text-muted"
>
{label} row
</li>
))}
</ul>
);
}
Anatomy
Surface is a single polymorphic part — no compound subparts. By default it renders a <div>; pass as to change the tag or render to compose it with another component entirely.
Surface— the layer element. Props:elevation(the scope it establishes),shade(the ramp tier, defaultbase),bordered(the 1px outline),shadow(the drop-shadow tier, default none), plusas/renderfor the rendered element.Surface.props(...)— the D12 escape hatch: a spreadabledata-slot+ data-attribute bag for styling a foreign element as a surface in place.
The rendered element carries data-slot="noctis-surface" for host-side styling and the stable data-surface marker every surface.css rule keys off (present independent of any data-slot override). The scope and look read off data-elevation, data-shade, data-bordered, and data-shadow.
On surfaces
OnSurfaces is the matrix the component pages use to prove a control adapts: it stacks a registered preview down the elevation scopes, each on its own full-width row as a Surface that establishes the scope and paints its base, so a surface-adaptive control re-tunes per layer with no per-component knowledge. Point it at a registry id; the control reads on every layer.
import { OnSurfaces } from "#/components/mdx";
export default function Example() {
return <OnSurfaces component="slider" />;
}
Design tokens
Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. Surface consumes the neutral ramp roles directly rather than minting its own knobs; retune the surface family by overriding those roles on an ancestor — e.g. .brand { --noctis-color-surface: …; } recolors every card in that region. See Customization for the full override ladder and Tokens for the whole graph.
API reference
Generated from the component's types — every prop, type, default, and description comes straight from the source. Surface is polymorphic, so the native attributes of its rendered tag pass through alongside the layer props; expand a row for the full type and description.