Installing Yeti

Yeti is one stylesheet. Everything else here is optional and most pages need none of it.

The examples on these pages are live: each is a demo, a box you can drag from its bottom corner to watch the component change shape, with the code beneath it. The label in the corner names the width stop the box is at.

Start a page

Yeti ships two files to begin a site from: index.html, a complete page that is correct by construction, and theme.css, the essential settings with their defaults, every one commented out. Both ship in the package under dist/starter/. Copy both. They are a place to delete from, not a showcase.

The page is the skeleton most sites share: a skip link, a sticky nav that folds behind a toggle on a phone, a hero, a grid of cards, a little running text, and a footer.

<body class="shell" data-gap="xl">
    <a href="#content">Skip to content</a>
    <!-- nav.nav with data-sticky and a sticky offset of 0: the brand, the toggle, the links, one button -->
    <main id="content" class="stack" data-gap="3xl">
        <section class="center" aria-labelledby="headline">
            <!-- div.hero: the headline, a lede, two buttons, and a figure -->
        </section>
        <section class="center" aria-labelledby="features-heading">…</section>
        <section class="center" data-max="md" aria-labelledby="about-heading">…</section>
    </main>
    <footer class="box" data-surface="raised">…</footer>
</body>

It links ../yeti.css, so it opens straight from the package; moved anywhere else, point the link at the CDN copy its comment gives. Edit the values in theme.css before you write any CSS of your own: the hues, the scale, the fonts and the corners are most of what makes a site look like itself, and the theming guide explains each.

Getting the files

Yeti is in alpha, and each alpha is published under the next tag, so ask for that tag to get the newest one. There are three ways to have it. The examples on the rest of this page use the npm paths.

With npm:

npm install @foundation/yeti@next

The package ships dist/: the bundled yeti.css and its minified twin yeti.min.css, the same source tree unbundled under css/, the ten modules under js/ and all of them in one yeti.js with yeti.min.js beside it, the two example themes under themes/, the starter page and theme under starter/, and the machine-readable files described further down.

Without Node, for a PHP site or anything else with no node_modules, copy the files into the project as one folder. Take llms.txt with them, so the docs on disk always match the stylesheet they describe, and the licence, which has to travel with any copy:

mkdir -p public/assets/yeti && cd public/assets/yeti
base=https://cdn.jsdelivr.net/npm/@foundation/yeti@next
for file in dist/yeti.min.css dist/yeti.min.js dist/yeti.min.js.map dist/llms.txt LICENSE; do
    curl -fsSLO "$base/$file"
done

To update, run it again and let the new files replace the old. The first line of yeti.min.css and of llms.txt says which version you have. A folder like this needs no build, and it is the module and stylesheet sections below with your own path in place of node_modules/@foundation/yeti/dist/.

From the CDN, to try something on a page with nothing to install:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@foundation/yeti@next/dist/yeti.min.css">

next moves with every release, so pin a version such as @7.0.0-alpha.1 on anything that stays up.

To work on Yeti itself, clone the repository and run npm run build. That writes the same dist/ the package ships.

The stylesheet

One link, before your own styles:

<link rel="stylesheet" href="node_modules/@foundation/yeti/dist/yeti.css">

Yeti's rules live in cascade layers, so anything you write outside a layer wins over them without a specificity fight. A theme is a second stylesheet of token values that loads after the first:

<link rel="stylesheet" href="node_modules/@foundation/yeti/dist/yeti.css">
<link rel="stylesheet" href="node_modules/@foundation/yeti/dist/themes/soft.css">

The minified copies

dist/yeti.min.css is the same stylesheet with the whitespace and the comments taken out, and dist/yeti.min.js is the same module bundle minified, names shortened and whitespace gone, with yeti.min.js.map beside it so a stack trace still points at a line of yeti.js — in both, save for the one-line licence banner every shipped file carries. Link them instead when you are counting bytes:

<link rel="stylesheet" href="node_modules/@foundation/yeti/dist/yeti.min.css">
<script type="module" src="node_modules/@foundation/yeti/dist/yeti.min.js"></script>

The package's style field and its . export still point at the readable yeti.css, because that is the one worth stepping through in devtools; name the minified file yourself when you want it.

Minified is not transpiled. light-dark(), @starting-style, container queries, oklch() and anchor positioning are left exactly as they are written, because Yeti's floor is Baseline 2025 and every browser at that floor already has them. The minified file and the readable one are the same CSS.

A custom build

yeti.css is every part of Yeti in one file. A site that never shows a demo, a carousel or a tooltip can leave them out. dist/css/ is the same stylesheet as separate files, and dist/css/yeti.css is a list of @import lines, one per file, in the order the full build uses. Copy that file into your project, delete the lines you don't want, and point the paths at node_modules/@foundation/yeti/dist/css/:

/* site.css: Yeti without the demo, the carousel or the tooltip */
@import "../node_modules/@foundation/yeti/dist/css/layers.css";
@import "../node_modules/@foundation/yeti/dist/css/tokens/scale.css";
/* ...the rest of the list, as it came... */
@import "../node_modules/@foundation/yeti/dist/css/components/dialog/dialog.css";
/* @import ".../components/tooltip/tooltip.css"; */
/* @import ".../components/carousel/carousel.css"; */
/* @import ".../components/demo/demo.css"; */
@import "../node_modules/@foundation/yeti/dist/css/utilities/attention/attention.css";

Four groups always stay, whatever you remove:

  • layers.css, first. It fixes the order of Yeti's cascade layers before any rule is read, which is what lets the rest of the list arrive in any order and still resolve the same way.
  • Every file under tokens/. The tokens are the theme; a part you removed leaves a few unused custom properties behind, which cost nothing.
  • Every file under base/: the reset, type, flow and form controls that everything else stands on.
  • layouts/attributes.css. It maps the shared attributes, data-gap, data-width, data-variant, data-size and the rest, for layouts and components alike, so a page with any layout or component needs it.

Everything else stands alone: each layout, recipe, component and utility is one file, and removing it removes that part and nothing more. Where one part's file mentions another, it is for the two used together, a spinner inside a button or a dropdown inside a nav, and those rules simply match nothing once the other part is gone. Don't reorder the lines you keep: the layers settle which rules win between groups, but inside a group a later file still wins a tie.

You can link site.css as it is, and it works: the browser follows each @import itself. That is fine while you are trying things, but it is one request per file, so bundle the list into one file before it ships. You need Node, and one command, run from the folder site.css is in:

npx esbuild site.css --bundle --minify --outfile=site.min.css

npx fetches esbuild the first time and runs it; there is nothing to configure. Link site.min.css in place of yeti.css. The bundle keeps Yeti's layers and their order, because they are written in the CSS itself, so a theme linked after it still lands where the theming guide says.

Scripts need no build: load only the modules for the components you kept, one script tag each, as a module below describes. To ship them as one file instead, list them in a site.js, using the package's own paths:

// site.js: the modules for the dialog, the tabs and the contents list
import "@foundation/yeti/js/dialog.js";
import "@foundation/yeti/js/tabs.js";
import "@foundation/yeti/js/toc.js";

and bundle it with the same tool:

npx esbuild site.js --bundle --minify --outfile=site.min.js

Load site.min.js with <script type="module">, as you would yeti.js. The paths are @foundation/yeti/js/…, not @foundation/yeti/dist/js/…: the package exports its modules under js/, and a bundler that honors the exports refuses the other.

Bare HTML

Most of Yeti is opt-in, but the base layer is not: it styles plain HTML the moment the stylesheet loads. Headings take the type scale, running text takes a measure, siblings in flow take the spacing rhythm, form controls and tables come out tidy, definition lists read as pairs, a quotation's caption reads as an attribution, and a details opens with a chevron and a panel that grows. A page with no Yeti markup in it at all still reads as a designed page. The base guide shows every element of it, live, with the tokens behind each group.

One base rule is a convention rather than an element. The first link in the body, if it points at a fragment of the same page, is treated as a skip link: it is out of sight until it is focused, and then it is a box pinned at the top start corner of the window with the focus ring on it.

<body>
    <a href="#content">Skip to content</a>
    <header>…</header>
    <main id="content">…</main>
</body>

There is no class to remember, because that is where a skip link goes anyway. The cost runs the other way: if the first link in your body points at a fragment and is not a skip link, it is invisible until someone focuses it. Put anything at all before it — a header, a div, the site's logo — and it is an ordinary link again.

A module

Every component works with no script. Some do more with one: the alert's close button, the tabs' roving focus, the dialog's backdrop and focus return, a dropdown that opens on hover, the carousel's dots, a range's filled track, a form's validation messages, a toc that follows the reader, and a demo's frame built from the code beneath it. Each is a module you load once, anywhere in the page, with nothing to call:

<script type="module" src="node_modules/@foundation/yeti/dist/js/dialog.js"></script>

A module finds its own elements and is safe on a page that has none of them. Leave it out and the component is still there, minus what the module adds; the components guide says what that is for each. A page that would rather not pick loads all ten at once, about four kilobytes compressed in yeti.min.js:

<script type="module" src="node_modules/@foundation/yeti/dist/yeti.js"></script>

Events

A module says what it did. Each dispatches one CustomEvent on the component's own element, bubbling and composed, so a single listener on document hears every instance on the page:

Event Dispatched on Detail
yeti:close the .alert, before it is removed none
yeti:open the dialog, once it is open none
yeti:close the dialog, when it closes none
yeti:select the .tabs { tab, panel }
yeti:slide the .carousel { index, slide }
yeti:invalid the form, when a submit is refused { controls }
yeti:current the .toc, when the mark moves { link, heading }
<script type="module">
    document.addEventListener('yeti:select', (event) => {
        history.replaceState(null, '', `#${event.detail.tab.id}`);
    });
</script>

None of them is cancelable: by the time one is dispatched the module has already acted. To stop something happening, prevent the platform event that caused it — the click, the submit, the command — which reaches your own listener first.

Editor completion

Yeti configures components through data-* attributes with fixed value lists, and the package ships those lists in the two formats editors read, generated from the same manifest the validator uses.

VS Code, Cursor and Windsurf read a custom-data file. One setting, in .vscode/settings.json:

{ "html.customData": ["./node_modules/@foundation/yeti/dist/yeti.html-data.json"] }

PhpStorm and WebStorm read web-types and find the file through package.json on their own. Install the package and the completions are there.

One limit is worth knowing. Neither format can tie a completion to a class, and a Yeti component's identity is its class, so every attribute is offered on every element: typing inside a p will offer data-rows. Each description opens with the components that accept the attribute, so the list explains itself, and a wrong value is still a validator error rather than a silent nothing.

For a TypeScript project the package ships types for the two JSON files and the vocabularies they are built from:

import manifest from '@foundation/yeti/manifest';
import type { YetiGap, YetiManifest } from '@foundation/yeti';

Both resolve with no configuration beyond the default in a modern project. They exist for tools built on those files; a page needs nothing from them.

Check your pages

The same manifest validates rendered HTML. bin/validate-html.js in the Yeti repository reads a folder of built pages and checks every element carrying a Yeti class the way the examples and guides are checked: an attribute a component does not take, a value outside its list, a bare attribute that needs one, a set with one member (an accordion with one panel, a timeline with one entry), a required accessibility attribute missing. Point it at the manifest the package ships:

node path/to/yeti/bin/validate-html.js ./build --manifest node_modules/@foundation/yeti/dist/yeti.manifest.json

It exits non-zero on any problem and names the file and line of each, so it belongs in the site's build after the pages are rendered. A card grid whose data-rows has no value collapses its rows silently in the browser and is one line of output here; run it before looking for the fault by hand.

For a language model

llms.txt and llms-full.txt are in the package and on the docs site at /yeti/llms.txt and /yeti/llms-full.txt. The first lists every component with its class, attributes, legal values and defaults; the second adds each component's guidance, accessibility notes and the token catalogue. They are generated from the manifest, so they describe exactly the surface the validator enforces, and nothing else. Point an assistant at the first and it can write valid Yeti; give it the second and it can explain why.

Every built stylesheet and bundle names llms.txt on its first line, at its own version on the CDN, so an assistant that opens a copied file can find the docs for exactly what it has. A project should say so outright, and point at its own copy, because that one always matches the installed version. Add this to its AGENTS.md or CLAUDE.md. The paths are for a copied folder; with npm, use node_modules/@foundation/yeti/dist/ instead of public/assets/yeti/:

## Yeti

This site is built on Yeti, a plain-CSS framework, in `public/assets/yeti/`.
The first line of `yeti.min.css` gives the version.

- Read `public/assets/yeti/llms.txt` before writing markup or CSS. Search
  `https://foundationcss.com/yeti/llms-full.txt` for anything it leaves out.
- Use what Yeti provides before writing CSS: most layouts, entrances and
  color changes are an attribute or a token.
- Set only public `--yeti-*` tokens. Never set `--_yeti-*`: those are private
  and may change in any release.