Skip to content
#ask-design-system

Foundation

Surface

Beta

The 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

ToneBackground tokenWhen to use
default--color-background-surface-neutral-defaultStandard light content area (page bg, cards, sections).
inverse--color-background-surface-inverse-defaultDark/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 tone enum — className is 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 / AreaStatus
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-tone attribute can be used by analytics or e2e tooling.

React Native

  • Renders a View with no accessibilityRole.
  • Tone propagates through context to Text, which sets accessibilityRole="header" on heading variants.

Surface Changelog

2026-05-27 — Initial implementation (PR #52)

Added

  • Surface component with tone="default" | "inverse" rendered as a div. 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-tone attribute on the rendered element so non-React-context consumers (analytics, e2e selectors, plain-CSS overrides) can still detect the tone.
  • className forwarding for padding, gap, border-radius, and layout. Background colour stays bound to the tone token.
  • bg-surface-default / bg-surface-inverse Tailwind utilities backed by --color-background-surface-{neutral,inverse}-default.
  • Storybook stories: Default, Inverse, Nested.

Notes

  • Web Button is not yet context-aware — pass inverse explicitly when rendering inside a <Surface tone="inverse">. Native Button does 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

  • Surface component with tone="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.
  • Button updated to read useSurfaceTone() and switch to inverse colours automatically when wrapped in <Surface tone="inverse">. The explicit inverse prop still wins if passed.
  • Storybook stories: Default, Inverse, Nested, ExplicitOverride.
  • Tests: tone-context propagation, nesting, fallback outside a Surface (8 tests).