Surface
Declares an "on-color" tone for a colored or dark region so every nested button-family component automatically switches to high-contrast, surface-adaptive styling — set the surface once, no prop-drilling.
Playground
Overview
Surface solves a common contrast problem: a button that looks correct on the page background can become unreadable on a brand-colored hero, a dark section, or an inverted card. Rather than detecting the painted background at runtime (unreliable and SSR-unsafe), Tessina UI uses an explicit, inheritable tone — the same approach taken by Material 3, Radix, and Adobe Spectrum.
Wrap a colored region in <Surface> and every nested Button, IconButton, Fab, SplitButton, ToggleButton, and ButtonGroup inherits tone="on-color". A per-component tone prop always overrides the inherited value.
Installation
pnpm add @tessinaui/uiUsage
import { Surface } from "@tessinaui/ui";
import { Button } from "@tessinaui/ui";{/* Recommended — `background` paints a paired bg + on-* ink and auto-enables on-color */}
<Surface background="primary" className="rounded-xl p-6">
<Button>Get started</Button> {/* solid inverse chip */}
<Button variant="secondary">Docs</Button>
<Button variant="ghost">Learn more</Button>
</Surface>
{/* Per-button override — opt one button back out */}
<Surface background="primary">
<Button>Inherited on-color</Button>
<Button tone="default">Forced default</Button>
</Surface>
{/* Raw mode — bring your own background + a readable ink color */}
<Surface tone="on-color" className="bg-[#3b0764] p-6 text-white">
<Button variant="outline">Outline</Button>
</Surface>Examples
Default
A colored surface where nested buttons inherit tone="on-color" automatically.
Backgrounds
The background sugar paints a paired fill and on-* ink — primary, error, warning, success, info, and foreground.
Rounded
Corner radius via the rounded prop — none, sm, md, lg, and full.
Per-button override
An explicit tone="default" opts a single button back out of the inherited surface tone.
Raw mode
Skip the background sugar — set tone="on-color" with your own background and a readable ink color.
How on-color styling works
Under tone="on-color", each variant renders a contrast-safe treatment derived from the surface ink (currentColor):
| Variant | On-color treatment |
|---|---|
primary (intent none) | Solid neutral inverse chip (bg-foreground / text-background) — a self-contained, AA-guaranteed prominent action. |
primary (semantic intent) | Keeps its solid semantic fill (error, warning, …) — those already carry guaranteed on-* contrast on any surface. |
secondary | Quiet: transparent rest with a faint ink border; ink-tinted hover/active. |
outline | Full ink border (border-current), transparent rest, ink-tinted hover. |
ghost | Transparent rest, ink-tinted hover only. |
Quiet variants rest transparent so their label contrast equals the surface's own on-*/surface ratio — they never render below what the palette can support. Interaction tints use color-mix(in srgb, currentColor X%, transparent), so they adapt to any surface color.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "default" | "on-color" | "default" (or "on-color" when background is colored) | Tone propagated to descendant button-family components. |
background | "none" | "primary" | "error" | "warning" | "success" | "info" | "foreground" | "none" | Convenience: paints a paired bg-* + on-* ink and auto-enables on-color tone. |
render | React.ReactElement | — | Render into a custom element instead of a div (polymorphic). |
className | string | — | Additional classes on the surface element. |
Hooks & helpers
| Export | Description |
|---|---|
useSurfaceTone() | Returns the current SurfaceTone from the nearest <Surface> (or "default"). For building tone-aware components. |
resolveTone(prop, ctx) | Resolves a component's effective tone — explicit prop wins, else inherited surface tone. |
Notes
- Recommended path: use the
backgroundsugar — it pairs the background with its matchingon-*ink so the inheritedcurrentColoris guaranteed legible in both light and dark themes. - Raw mode: when you supply your own background via
className, also set a readable text color (e.g.text-white) so the quiet variants'currentColor-derived fills and labels read correctly. - No auto-detection:
Surfacedoes not inspect the painted background; it propagates an explicit, deterministic, build-time-verifiable tone (audited against WCAG AA).