Skip to content

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

Neutral

Standard light content area — page background, cards, sections.

Plumber needed

Body copy on the neutral surface.

<Surface tone="neutral" className="rounded-lg p-6 space-y-2">
<Text variant="header-md">Plumber needed</Text>
<Text variant="body-md">Body copy on the neutral 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>

Brand primary

Primary brand colour area. On-surface ink tokens flip to match.

Brand primary

Body copy on the brand-primary surface.

<Surface tone="brand-primary" className="rounded-lg p-6 space-y-2">
<Text variant="header-md">Brand primary</Text>
<Text variant="body-md">Body copy on the brand-primary surface.</Text>
</Surface>

Brand secondary

Secondary brand colour area. On-surface ink tokens flip to match.

Brand secondary

Body copy on the brand-secondary surface.

<Surface tone="brand-secondary" className="rounded-lg p-6 space-y-2">
<Text variant="header-md">Brand secondary</Text>
<Text variant="body-md">Body copy on the brand-secondary surface.</Text>
</Surface>

Emphasis

Strong emphasis

emphasis='strong' selects the strong background token for any tone. Independent of tone; defaults to 'default'. soft is not exposed yet.

Welcome

Strong brand-secondary surface.

<Surface tone="brand-secondary" emphasis="strong" className="rounded-lg p-6 space-y-2">
<Text variant="header-md">Welcome</Text>
<Text variant="body-md">Strong brand-secondary surface.</Text>
</Surface>

Nested surfaces

An inner Surface overrides the outer tone for the region it wraps. Useful for inverse callouts inside a neutral page.

Outer neutral surface.

Inverse callout nested inside.

<Surface tone="neutral" className="rounded-lg p-6 space-y-3">
<Text variant="body-md">Outer neutral 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 and Icon read useSurfaceInk() and flip 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
neutral--color-background-surface-neutral-defaultStandard light content area (page bg, cards, sections).
inverse--color-background-surface-inverse-defaultDark/navy promotional or callout areas.
brand-primary--color-background-surface-brand-primary-defaultPrimary brand colour areas.
brand-secondary--color-background-surface-brand-secondary-defaultSecondary brand colour areas.

Default tone is 'neutral'. On-surface ink colours (Text, Icon) resolve automatically for all four families via useSurfaceInk(role).

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 'neutral' | 'inverse' | 'brand-primary' | 'brand-secondary' 'neutral' Sets the background colour and provides the tone via context for descendant tone-aware components (Text, Icon).
emphasis 'default' | 'strong' 'default' Strength of the surface background; 'strong' uses the *-strong token.
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, useSurfaceInk } from '@checkatrade/components-web';

Native

import { Surface, useSurfaceTone, useSurfaceInk } from '@checkatrade/components-native';

Basic usage

<Surface tone="neutral" className="rounded-lg p-6">
  <Text variant="header-md">Section heading</Text>
  <Text variant="body-md">Body copy on a neutral 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>

<Surface tone="brand-primary" className="rounded-lg p-6">
  <Text variant="header-md">Brand primary area</Text>
  <Text variant="body-md">On-surface ink resolves automatically.</Text>
</Surface>

<Surface tone="brand-secondary" className="rounded-lg p-6">
  <Text variant="header-md">Brand secondary area</Text>
  <Text variant="body-md">On-surface ink resolves automatically.</Text>
</Surface>

Reading the tone from your own component

import { useSurfaceTone } from '@checkatrade/components-web';

function MyAdaptiveLabel() {
  const tone = useSurfaceTone(); // 'neutral' | 'inverse' | 'brand-primary' | 'brand-secondary'
  // Pick colours / icons / etc. based on tone.
}

Returns 'neutral' when called outside any <Surface> — safe to use anywhere.

Resolving on-surface ink (useSurfaceInk)

import { useSurfaceInk } from '@checkatrade/components-web';

function MyAdaptiveWidget() {
  const inkClass = useSurfaceInk('body'); // e.g. 'text-ink-body-default'
  const headingInk = useSurfaceInk('heading'); // e.g. 'text-ink-heading-default'
  // toneOverride can be passed to ignore the context: useSurfaceInk('body', 'inverse')
}

Text and Icon use useSurfaceInk internally — you only need this hook when building new tone-aware components.

Detecting the tone outside React

The rendered element carries a data-surface-tone attribute (neutral | inverse | brand-primary | brand-secondary) and a data-surface-emphasis attribute (default | strong) so non-React consumers (analytics, e2e selectors, plain-CSS overrides) can still detect the tone and emphasis.

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).