design token

Surfaces & Elevation

How panels pick their surface, text and border tokens from the terrain ramp.

Every container in Pindoba (cards, popovers, dialogs, sidebars, banners) is a Panel. A Panel doesn’t take a background color. It takes a position on a ramp, and the tokens for its fill, text, borders and hover states follow from that position.

The concept: terrain, not paint

Surfaces are named like terrain. Peak is the highest step and ground the lowest. Higher surfaces sit closer to the reader. The page itself stands mid-ladder on base: cards rise to peak above it, and wells, rails and selected rows sink toward ground below it.

Altitude here is structural depth, not shadow. Shadows are a separate axis (shadow="xs"…"xl") that you can add to any surface. A popover is a peak surface with a shadow. A sunken sidebar rail is a low step with no shadow.

Play with the props below. The readout shows the ramp and token names the Panel resolves to, and the contrast of its text measured on the painted fill.

background · the step on the ramp
emphasis · which ramp
tone · which palette
shadow · elevation, independent of the surface
modifiers
ramp
primary.surface.*
fill
primary.surface.peak
hover
primary.surface.hill
press
primary.surface.base
panel.text
primary.text
shadow
shadows.sm
contrast
depends on what shows through

secondary · primary

Text reads panel.text, so it follows this surface.

Hover or press me to step the ramp.

Nested card

its own surface

A child panel resolves its tokens again from its own props.

stage: neutral.surface.ground
<Panel
  background="surface.peak"
  emphasis="secondary"
  tone="primary"
  interactive
  shadow="sm"
>

The terrain ramp

Each semantic group (neutral, primary, success, warning, danger) has a five-stop micro-ramp between shade 50 and shade 100:

peakcards, sheets, popovers
hillraised tier
basethe page floor
valleyrails, wells
grounddeepest wells, chrome
  • surface.peak: shade 50 in light mode, 100 in dark. Cards, reading sheets, and popovers or menus with a shadow.
  • surface.hill: a quarter of the way down. An intermediate raised tier.
  • surface.base: the midpoint. The page floor (the default body background), and recessed regions inside a peak card.
  • surface.valley: three quarters of the way down. Rails, insets and wells cut into the page.
  • surface.ground: shade 100 in light mode, 50 in dark. The deepest wells on the page and window chrome. Not a floor: nothing can sink below it.
  • transparent: no fill, so the page shows through. Hover still tints the page.

An interactive panel moves down the ramp on hover (+1 step) and on press (+2 steps). From valley and ground there is no room to go lower, so they step back up instead. A transparent panel (a ghost button, a nav row, a list row) has no step of its own, so its states step off the surface underneath it. See The page floor.

The page floor

The page stands on base, the middle of the ramp. It is the only step with room in both directions. Cards rise two steps to peak, and wells, columns and selected rows sink two steps toward ground. One neutral step (about 1.02:1) is below what the eye notices, so two steps is the smallest move that reads, and only base leaves two steps on both sides.

  • App floor: base. It’s the preset default: body is painted neutral.surface.base. Dashboards, boards, settings and inboxes put their content on peak cards above it.
  • Reading-first content: a peak panel, not a peak page. An article, a docs page or an editor puts its text on one peak sheet standing on the base floor. The text gets the top step’s contrast, code blocks and callouts sink from it, and the chrome around it stays on the floor. These docs are built this way.
  • ground is chrome, not a floor. It separates cards the most, but nothing can sit below it. Wells and kanban columns would have to rise instead, so keep it for window chrome and the deepest wells.

Light and dark agree. Down the ramp is darker in both modes, so the same floor works in both.

Ghost states follow the floor

A transparent panel’s hover, press and current (“selected”) fills are steps on the ramp, picked relative to the surface underneath. Every solid Panel publishes its step to the transparent panels inside it, and body publishes the page’s. So the same nav row reads correctly in a card, on the page and on a sunken rail:

floorhoverpresscurrentcurrent + hovercurrent + press
peak (inside cards)basevalleyvalleygroundbase
hillvalleygroundgroundvalleybase
base (the page)peakhillgroundvalleyhill
valley (rails)hillbasepeakhillbase
groundbasevalleypeakhillbase

On the page, hover lifts a row to the card surface and the current row sinks, so hovering never looks like a selection. Where there is no room below (valley, ground), every state rises.

A floor you paint yourself, without a Panel, has to say which step it is, or ghost rows on it step off the page’s base. Add data-surface-floor:

<aside
  class={css({ bg: "neutral.surface.valley" })}
  data-surface-floor="valley"
>
  <!-- nav rows, ghost buttons… -->
</aside>

With a Panel (<Panel background="surface.valley">) this happens automatically.

Emphasis × tone

Two props decide which ramp the background step is taken from:

  • emphasis picks the ramp. primary uses the solid accent band (<tone>.accent.surface.*), secondary uses the tinted surfaces (<tone>.surface.*), and tertiary uses the colorless neutral.surface.* but keeps the palette’s accent text.
  • tone picks the palette: it sets colorPalette, so every colorPalette.* token below the panel follows it.
neutralprimarysuccesswarningdangerprimaryaccent
Aa
Aa
Aa
Aa
Aa
secondarytinted
Aa
Aa
Aa
Aa
Aa
tertiaryneutral
Aa
Aa
Aa
Aa
Aa

The text follows the fill. A filled primary panel (solid step, not translucent) redirects panel.text.* to <tone>.text.contrast.*, which is solved against the accent band. Every other combination uses the palette’s own <tone>.text.*. On a coloured surface, write color: "panel.text" rather than a fixed group so your content flips with the panel.

  • emphasis="primary": the ramp is <tone>.accent.surface.*. When filled, panel.text is <tone>.text.contrast.
  • emphasis="secondary": the ramp is <tone>.surface.*, and panel.text is <tone>.text.
  • emphasis="tertiary": the ramp is neutral.surface.*. panel.text is <tone>.text, and the panel’s own text color is <tone>.text.accent.

Nesting

Panels stack. Each one resolves its tokens again from its own props, so a child can sit higher or lower than its parent. A common stack climbs from the floor to the card on top. With radius="inner", each child’s corner stays concentric with its parent’s.

surface.ground · app floor
surface.valley · inset
surface.base · region
surface.hill · raised
surface.peak + shadow.sm · the card on topAltitude comes from the surface; the shadow is a separate axis.

Use tone="inherit" on a child to keep the parent’s palette, for example a neutral panel inside a success region.

Translucency

translucent paints the chosen step at 35% opacity with a backdrop blur, so a panel over imagery reads as glass rather than a tinted slab. There is no single contrast ratio for glass, because it depends on what sits behind it. For that reason a translucent primary panel keeps the palette’s regular text, not the contrast text, and switches to text.bold on its two darkest steps.

solidsecondary · neutral
translucentsecondary · neutral
translucentprimary · primary

When to use which

  • The page: surface.base, the preset’s body default. Don’t repaint it.
  • Cards and reading sheets: surface.peak, emphasis="secondary", tone="neutral" (the defaults most components start from). Long-form text goes on a peak sheet, not a peak page.
  • Sunken rails and wells on the page: surface.valley, or surface.ground for the deepest. Declare data-surface-floor if you paint them by hand.
  • Inside a peak card: selected rows and recessed regions on surface.base; wells, code blocks and disabled fills on surface.valley.
  • Ghost rows (nav items, ghost buttons, list rows): background="transparent". Their states follow the floor, so don’t hand-tint them.
  • Floating layers (popovers, menus, dialogs): surface.peak plus a shadow. Elevation comes from the shadow, not from a different surface.
  • Status callouts: emphasis="secondary" with a tone. Use emphasis="primary" only for a strong, filled call to action.
  • Neutral surface with a hint of color: emphasis="tertiary". The surface stays gray and the text carries the palette.
  • Over images or video: translucent.

For the full prop reference, including borders, active states, padding and radius, see Panel. The underlying color tokens are documented on Colors.