Color

Color in Yeti is six words and a distance. A hue has a name, primary or warning, and the same name means the same thing on a badge, a button, an alert and a painted section. A grey has a number, how far it sits from the page toward the page's own text, and that number means the same thing in light mode and in dark. The browser picks the scheme; Yeti keeps every pair of colors readable in both. This page is how to use it. The theming guide is how to change it.

Six hues, one chroma

Everything starts from six hue angles, primary, secondary, success, warning, alert and neutral, and one chroma for how saturated the accents are. From those, Yeti derives every color at runtime, in both schemes at once, through one fixed ladder of lightness. Change a hue and the whole family moves with it; nothing is hand-picked.

Each hue gets five steps and a partner for text placed on it:

Token Light Dark For
--yeti-color-primary-subtle L 0.95 L 0.25 a tint behind body text: a callout's background, a striped row
--yeti-color-primary-soft L 0.85 L 0.35 a soft fill or a border with some weight
--yeti-color-primary L 0.52 L 0.70 the color itself: a button, a badge, a bar, a link
--yeti-color-primary-strong L 0.42 L 0.80 the same thing under the pointer
--yeti-color-primary-text L 0.35 L 0.85 words in the hue on the page, at reading contrast
--yeti-on-primary near white near black words placed on the base color

The page itself has its own roles at the neutral hue, low in chroma so they read as grey with a hint of the brand: --yeti-color-surface and its -raised and -sunken steps, --yeti-color-text and -text-muted, --yeti-color-border and -border-strong, a -scrim for washing over a picture, and -focus, which is the primary by default. Every one of these is a token you can read from your own CSS and a role a theme can set outright.

Text on color

Two of the six steps exist only so that words read. --yeti-on-primary is what goes on the base color: it is near white in light mode, where the base is dark enough to carry it, and near black in dark mode, where the base is lifted to carry that instead. --yeti-color-primary-text is the hue as text on the page, pushed far enough from the surface to be read at body size. Reach for on- when you paint something the hue and put words on it; reach for -text when the words themselves are the hue.

These are promises, not intentions. The browser tests check them at every hue angle in steps of thirty degrees, in both schemes:

Pair Holds at least
page text on the page surface 7:1
muted text on the page surface 4.5:1
on- text on every base hue 4.5:1
-text of every hue on the page surface 4.5:1
page text on every -subtle tint 7:1

A theme that sets a hue keeps every one of these, because the ladder is fixed and only the angle moved. A theme that sets a derived color directly, --yeti-color-primary: #0a7, takes the promise into its own hands for that role.

Color that means something

On a component, a hue is a status, not a decoration. data-variant takes the six names, and the component decides what to do with the color: a button fills with it, a badge tints itself with the subtle step and writes in the hue, a card tints its border, an alert washes its background with the subtle step behind a border in the hue, and a progress bar and a toc use it for the moving part. The word stays the same across all of them, so a reader learns success once.

danger is a seventh word on that list that is not a seventh hue. It is alert under its Foundation 6 name, kept so a callout that said danger still says something. New pages should say alert.

The words are the same as data-paint uses below, and that is the point: this is what success looks like as a status, and beside it as a fill.

View Code
<div class="stack" data-gap="md">
    <div class="cluster" data-gap="sm" data-align="center">
        <span class="badge" data-variant="success">Live</span>
        <button class="button" type="button" data-variant="success">Publish</button>
        <div class="alert" role="status" data-variant="success">
            <svg aria-hidden="true" viewBox="0 0 16 16"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
            <div><strong>Saved.</strong> Your changes are live.</div>
        </div>
    </div>
    <div class="box" data-paint="success">A section painted the same word: the base color, with the text made for it.</div>
</div>

A color by name

Anything can be painted. data-paint sets the background of any element, and the words on it, from one list: the six hues at their base step, three constants, and the eleven steps of the greyscale. data-text sets the words alone, from the same list, and on an element that is also painted it wins over the automatic color. Neither needs a class, and both outrank whatever background or color the element's own component gave it, so a card or a button can be painted too.

  • A hue paints the base color and puts its on- text on it, the same pair a button uses.
  • white, black and grey are the only three colors in Yeti that never move: white and black are what they say, and grey is 18% reflectance, the photographic middle. White takes black text, black takes white, and grey takes black. Reach for them when a thing must be that color on every page, a caption band over a photograph, and for the greyscale otherwise.
  • grey-0 to grey-100 , in tens, is the greyscale. grey-0 is the page surface and grey-100 is the page text, and each step is ten percent of the way from the one toward the other. A painted grey takes the page text up to grey-40 and the page surface from grey-50, because half-way toward the text is where the text stops reading. The middle steps, grey-40 to grey-60, suit a fill more than a block of text: no text color reaches full contrast on them.

A grey names a distance, not a color. In light mode the scale runs from white toward black; in dark mode from black toward white; and grey-20 is a quiet panel near the page in both. That is the whole reason the steps are numbers and not names. The frame below shows the scale the way the page's own scheme reads it, so on a page that follows your setting it reverses when you change it; the second row is the primary hue at the same eleven steps, which is a token rather than a paint value.

View Code
<div class="stack" data-gap="sm">
    <div class="cluster" data-gap="xs">
        <span class="box" data-gap="xs" data-paint="grey-0">0</span>
        <span class="box" data-gap="xs" data-paint="grey-10">10</span>
        <span class="box" data-gap="xs" data-paint="grey-20">20</span>
        <span class="box" data-gap="xs" data-paint="grey-30">30</span>
        <span class="box" data-gap="xs" data-paint="grey-40">40</span>
        <span class="box" data-gap="xs" data-paint="grey-50">50</span>
        <span class="box" data-gap="xs" data-paint="grey-60">60</span>
        <span class="box" data-gap="xs" data-paint="grey-70">70</span>
        <span class="box" data-gap="xs" data-paint="grey-80">80</span>
        <span class="box" data-gap="xs" data-paint="grey-90">90</span>
        <span class="box" data-gap="xs" data-paint="grey-100">100</span>
    </div>
    <div class="cluster" data-gap="xs" aria-hidden="true">
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-0)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-10)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-20)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-30)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-40)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-50)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-60)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-70)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-80)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-90)"></span>
        <span class="box" data-gap="sm" style="background-color: var(--yeti-color-primary-100)"></span>
    </div>
</div>

Inside a painted element, a plain link and a caption take the element's color too. Without that, a link inside a primary band would be primary on primary. A link that is a button keeps its own colors, because it is a button, and a caption that names its own data-text keeps that. A painted element also keeps its background on paper: browsers drop backgrounds when printing unless told otherwise, and a band's words were chosen for that background, so Yeti asks for them to stay; the reader's own print setting can still take them away.

View Code
<div class="stack" data-gap="md">
    <section class="box" data-paint="primary" data-gap="lg">
        <h2>A painted band</h2>
        <p>Its words are the color made for it, and so is <a href="#">a link inside it</a>. A heading, a paragraph and a link, all readable, nothing set by hand.</p>
    </section>
    <section class="box" data-paint="grey-80" data-gap="lg">
        <p>Past the middle of the scale the words flip to the page surface. This is grey-80, a dark panel on a light page and a pale one on a dark page.</p>
    </section>
    <p data-text="grey-60">Muted words, with no background painted at all.</p>
</div>

Light and dark

Yeti declares color-scheme: light dark on the root, so the page follows the reader's setting and every color above resolves for the scheme in use. To force one, set color-scheme: light or dark on any element and everything below it flips: the hues, the page roles, the greys and the tones. Force it on the root to opt a whole site out of dark mode; force it on a section to keep a photograph's caption band dark on a light page.

Two things to know. The inputs, the hues and the chroma, only take effect on the root, because the ladder is computed there; so are the greys and the tones. A derived color, --yeti-color-primary or --yeti-color-surface, can be set on any element, and every role that reads it there follows; the greys do not, because they were mixed at the root from the poles the root had. If a section needs its own greys, force its scheme rather than its surface.

In your own CSS

Every color on this page is a public token, and a class of your own can read any of them: the ladder steps, the page roles, --yeti-white, --yeti-black and --yeti-grey, the greyscale --yeti-grey-0 to --yeti-grey-100, and the same eleven steps at every hue, --yeti-color-primary-0 to --yeti-color-primary-100 and so on for the other five. A tone sits at exactly the lightness of the grey with its number, so a hued panel and its text can be picked from opposite ends of one scale:

.promo {
    background-color: var(--yeti-color-primary-20);
    color: var(--yeti-color-primary-90);
}

That pair flips with the scheme, follows the brand hue, and keeps its contrast, because the two numbers are seventy steps apart on the same ladder. The tokens page lists every name with its default; the theming guide says how to move the inputs they all derive from.