Once UI Blog
Posts
Once UI
Colour arrives in pairs
Once UI 2.0 alpha: Measured, not assumed
Paint in pairs: background and onBackground
Once UI 1.8: Polish as a feature
Once UI 1.7: Form with Intent
From products to systems: a new service model
Magic Journal: A different medium for long-form writing
Supa Social: The next milestone of our journey
Once UI is open source. Here's how we plan to keep it that way.
Designing a timeless, AI-native brand
Once UI 1.5: Curiosity in code
Once UI 1.4 migration guide: The breakpoint object
Launching in the AI-native era: A guide for indie builders
The first design system for indie builders
Builder Stories
SatisPro and Once UI: Scaling smarter review management
IQON and Once UI: Scaling with a design system
OsmyReal: Launching a platform for mobile gaming with minimal coding
JExcellence and Once UI: Turning experiments into enterprise apps
Dev: Blending science and code with Once UI
TrademarkTrademark
Ctrl k
Search…
Sign up
Once UI Blog
Posts
Once UI
Colour arrives in pairs
Once UI 2.0 alpha: Measured, not assumed
Paint in pairs: background and onBackground
Once UI 1.8: Polish as a feature
Once UI 1.7: Form with Intent
From products to systems: a new service model
Magic Journal: A different medium for long-form writing
Supa Social: The next milestone of our journey
Once UI is open source. Here's how we plan to keep it that way.
Designing a timeless, AI-native brand
Once UI 1.5: Curiosity in code
Once UI 1.4 migration guide: The breakpoint object
Launching in the AI-native era: A guide for indie builders
The first design system for indie builders
Builder Stories
SatisPro and Once UI: Scaling smarter review management
IQON and Once UI: Scaling with a design system
OsmyReal: Launching a platform for mobile gaming with minimal coding
JExcellence and Once UI: Turning experiments into enterprise apps
Dev: Blending science and code with Once UI
Sponsored
Aveiro
aveiro.app
Sponsored
Aveiro
aveiro.app
TrademarkTrademark
Once UIDocumentationBlog
© Once UI. All rights reserved.
Built with Aveiro
Guides

Colour arrives in pairs

Once UI colour is four pairings, not a palette you pick from: background with onBackground, solid with onSolid, alpha over whatever sits behind, and surface when you want the theme to decide. Use the pairs; leave sand-700 alone.
Updated 1h ago
What made Once UI 2.0 possible
Once UI colour is not a palette you pick from. It is four pairings. Reach for the pair that matches the job, and leave base tokens like sand-700 alone — they are the foundation layer, not something a product page should name. Colour arrives on the container. Set the fill and the text that sits on it once, and every label inside inherits it. That is why the props come in pairs rather than as a freestyle colour picker on every child.

background and onBackground

Use this for a panel, a section, a page area — anything that is a surface, not a control. background also accepts page, surface, overlay, and transparent. Those follow the theme without naming a scheme. The main site background is usually page; raised layout chrome such as a header or sidebar is usually surface.

solid and onSolid

Use this when the fill has to read as pressable — a button, a checkbox, a filled badge. solid is the high-contrast fill; onSolid is the only safe text colour on top of it. Putting onBackground on a solid fill is the usual contrast bug. The pair names exist so you do not have to remember which weight clears a brand fill in light mode and again in dark mode.

Alpha over whatever sits behind

Alpha weights (brand-alpha-medium, neutral-alpha-weak) tint whatever is behind them instead of replacing it. That is what keeps a glow, a soft highlight, or a scrim working over both an image and a plain surface. Do not stack a low opacity on top of an already-weak colour. Prefer the alpha token. And do not use *-background-weak as an accent: it is nearly the page colour, so it disappears.

surface when you do not want to pick a side

background="surface" is the neutral raised panel. It follows the theme without naming neutral, brand, or anything else. Pair it with a quiet border and you have the static panel recipe most product UIs live on:
<Column
  background="neutral-medium"
  onBackground="neutral-strong"
  padding="24"
  radius="l"
  gap="8"
>
  <Heading variant="heading-strong-m">Panel title</Heading>
  <Text variant="body-default-s" onBackground="neutral-weak">
    Supporting copy stays on the same surface.
  </Text>
</Column>
<Row solid="brand-strong" onSolid="brand-strong" paddingX="16" paddingY="8" radius="m">
  <Text variant="label-default-s">Primary action</Text>
</Row>
<Column
  background="surface"
  border="neutral-alpha-weak"
  radius="l"
  padding="24"
  gap="16"
  fillWidth
>
  <Heading variant="heading-strong-m">Settings</Heading>
  <Text variant="body-default-s" onBackground="neutral-weak">
    Body copy on a theme-owned surface.
  </Text>
</Column>
Save Card for surfaces that actually navigate or select. A static box belongs on Column with the surface recipe.

What not to do

  • Do not paint with base tokens (sand-700, hex, rgb). Semantic tokens move with the theme; base tokens do not.
  • Do not set text colour on every child when the container already carries onBackground or onSolid.
  • Do not mix solid fills with onBackground text and hope contrast holds in both themes.
The longer reference lives in the colour docs. This post is the decision tree: four pairs, one job each.