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>Showcase
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).
Skeleton
Animated placeholder for loading states. Wave shimmer (default) and pulse variants, five rounded options, and composable with any layout.
Progress
Communicate operation status with a linear bar or circular ring. Four variants (default/success/warning/error), three sizes, five rounding options, indeterminate animation, optional label with inside/outside positioning, and LTR/RTL support.