Foundation
Surface
BetaThe base layout primitive for areas with a defined background tone. Renders a token-driven background and pushes its tone into a React context so descendant components (Text, and later Card, Input…) adapt automatically. Available on web (React) and native (React Native).
Tones
Default
Standard light content area — page background, cards, sections.
Plumber needed
Body copy on the default surface.
<Surface tone="default" className="rounded-lg p-6 space-y-2">
<Text variant="header-md">Plumber needed</Text>
<Text variant="body-md">Body copy on the default surface.</Text>
</Surface>Inverse
Dark or branded promotional areas. Text inside automatically flips to inverse colours.
Promoted area
Body copy on the inverse surface.
<Surface tone="inverse" className="rounded-lg p-6 space-y-2">
<Text variant="header-md">Promoted area</Text>
<Text variant="body-md">Body copy on the inverse surface.</Text>
</Surface>Nested surfaces
An inner Surface overrides the outer tone for the region it wraps. Useful for inverse callouts inside a default page.
Outer default surface.
Inverse callout nested inside.
<Surface tone="default" className="rounded-lg p-6 space-y-3">
<Text variant="body-md">Outer default surface.</Text>
<Surface tone="inverse" className="rounded-lg p-4">
<Text variant="body-md">Inverse callout nested inside.</Text>
</Surface>
</Surface>Tone-aware children
Text reads useSurfaceTone() and flips automatically — no inverse prop needed. Button is not yet context-aware on web; pass its inverse prop explicitly.
Need a tradesperson?
We’ll match you with vetted local pros.
<Surface tone="inverse" className="rounded-lg p-6 space-y-3">
<Text variant="header-md">Need a tradesperson?</Text>
<Text variant="body-md">We'll match you with vetted local pros.</Text>
<Button label="Get quotes" inverse />
</Surface>When to use Surface directly
Surface is a building block, not a consumer primitive. Most product code should reach for semantic components (Card, Banner, Section, Sheet, Modal) — those consume Surface internally so consumers never see it.
// What product code writes
<Card variant="elevated">
<Text variant="header-md">Plumber needed</Text>
<Button label="View quotes" />
</Card>
// What Card renders internally
function Card({ tone, children, ... }) {
return <Surface tone={tone} className={cardStyles}>{children}</Surface>;
}Reach for bare <Surface> only when:
- Setting a page-level root tone (often inside a layout shell or app provider).
- Prototyping before a semantic component exists for the region.
- Building a truly custom region the design system doesn’t model yet.
If you find yourself wrapping <Surface> with the same shape repeatedly in product code, that’s a signal to extract a semantic component into the design system.
Promoted
Get matched with vetted local pros.
Do
Use Surface as the root of a themed region and let tone-aware children pick up the colour automatically.
Background should come from the tone, not a className override.
Don't
Don't override the surface background with custom classes — the tone token is the only intended source of colour.
Tones
| Tone | Background token | When to use |
|---|---|---|
default | --color-background-surface-neutral-default | Standard light content area (page bg, cards, sections). |
inverse | --color-background-surface-inverse-default | Dark/navy promotional or callout areas. |
Default tone is 'default'. Additional tones (e.g. accent, muted) will be added as the design tokens grow.
When not to use
- Card-like elevation, borders, or padding shapes that recur. Build a semantic component (Card, Banner, Section) that consumes Surface internally and exposes a tight API to product code.
- Per-instance background colour. Use the
toneenum —classNameis for layout, not colour.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| tone | 'default' | 'inverse' | 'default' | Sets the background colour and provides the tone via context for descendant tone-aware components. |
| className | string | - | Web — merged with the surface utility class. Use for padding, gap, border-radius, and layout. Do not override the background colour. |
| style | StyleProp<ViewStyle> | - | Native — forwarded to the underlying View for layout (padding, gap, border-radius). |
| children | ReactNode | - | Content rendered inside the surface. |
| data-testid | string | - | Web — forwarded to the underlying div. |
| testID | string | - | Native — forwarded to the underlying View. |
Import
Web
import { Surface, useSurfaceTone } from '@checkatrade/components-web';Native
import { Surface, useSurfaceTone } from '@checkatrade/components-native';Basic usage
<Surface tone="default" className="rounded-lg p-6">
<Text variant="header-md">Section heading</Text>
<Text variant="body-md">Body copy on a default surface.</Text>
</Surface>
<Surface tone="inverse" className="rounded-lg p-6">
<Text variant="header-md">Promoted area</Text>
<Text variant="body-md">Body copy on a dark surface.</Text>
</Surface>Reading the tone from your own component
import { useSurfaceTone } from '@checkatrade/components-web';
function MyAdaptiveLabel() {
const tone = useSurfaceTone(); // 'default' | 'inverse'
// Pick colours / icons / etc. based on tone.
}Returns 'default' when called outside any <Surface> — safe to use anywhere.
Detecting the tone outside React
The rendered element carries a data-surface-tone="default | inverse" attribute so non-React consumers (analytics, e2e selectors, plain-CSS overrides) can still detect the tone.
Platform status
| Platform / Area | Status |
|---|---|
| Design (Figma) | Beta |
| Web (React) | Beta |
| Native (React Native) | Beta |
| iOS (Swift) | Planned |
| Android (Kotlin) | Planned |
| Accessibility audit | Planned |
Accessibility
Surface is a purely visual container — it does not add roles or labels of its own. The semantic structure should come from the components rendered inside (Text for headings, Button for actions, etc.).
Web
- Renders a
<div>with no implicit role. - Tone propagates through context to
Text, which renders the appropriate heading or paragraph element. data-surface-toneattribute can be used by analytics or e2e tooling.
React Native
- Renders a
Viewwith noaccessibilityRole. - Tone propagates through context to
Text, which setsaccessibilityRole="header"on heading variants.
Surface Changelog
2026-05-27 — Initial implementation (PR #52)
Added
Surfacecomponent withtone="default" | "inverse"rendered as adiv. Paints the matching token-driven background via Tailwind and pushes its tone into a React context.useSurfaceTone()hook so descendants can read the nearest tone. Returns'default'outside any Surface — safe to use anywhere.data-surface-toneattribute on the rendered element so non-React-context consumers (analytics, e2e selectors, plain-CSS overrides) can still detect the tone.classNameforwarding for padding, gap, border-radius, and layout. Background colour stays bound to the tone token.bg-surface-default/bg-surface-inverseTailwind utilities backed by--color-background-surface-{neutral,inverse}-default.- Storybook stories:
Default,Inverse,Nested.
Notes
- Web
Buttonis not yet context-aware — passinverseexplicitly when rendering inside a<Surface tone="inverse">. NativeButtondoes pick up the surface context.
Surface Changelog
2026-05-27 — Building-block clarification (PR #48)
Changed
- Docs (
Surface.md,Surface.llms.txt) now make clear that Surface is a building block for semantic components (Card, Banner, Section, Sheet, Modal) rather than a primitive for everyday product code. Bare<Surface>is reserved for app-level roots, prototyping, and custom regions the system doesn’t model yet.
2026-05-27 — Initial implementation (PR #46)
Added
Surfacecomponent withtone="default" | "inverse". Paints the matching token-driven background and pushes its tone into a React context.useSurfaceTone()hook so descendants can read the nearest tone. Returns'default'outside any Surface — safe to use anywhere.Buttonupdated to readuseSurfaceTone()and switch to inverse colours automatically when wrapped in<Surface tone="inverse">. The explicitinverseprop still wins if passed.- Storybook stories:
Default,Inverse,Nested,ExplicitOverride. - Tests: tone-context propagation, nesting, fallback outside a Surface (8 tests).