@arag 0.1.0

Getting started

Install is a copy or an npm install, and consuming it is one <link>. There is nothing to compile, nothing to configure and no CLI. A semantic HTML document is styled the moment the stylesheet loads.

Install

Two paths. Pick whichever matches how the project already handles vendor code.

Via npm

The package is @arag/css. It has no dependencies and no install scripts.

npm install @arag/css

Then link the entry point straight out of node_modules, or copy it to wherever the project serves static assets from.

<link rel="stylesheet" href="node_modules/@arag/css/src/index.css">

Vendored

Copy src/ into the project and link it. This is the right choice for the kiosk app and for print-adjacent work, where there is no package manager in the loop at all.

cp -r arag/src public/vendor/arag

<link rel="stylesheet" href="/vendor/arag/index.css">

Either way: no build step, no config file, no CLI, no watcher, no class scanner. The files you link are the files that were written. What you read in src/ is exactly what the browser parses, so devtools shows you real source lines and not a compiled artefact.

This is a complete document. There is not one class on anything in it, and it is correctly styled — typography, spacing, colour, focus ring, dark mode and the Cyrillic font stack all come from the single link.

Account balance

Payment details were updated successfully.

Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.

Body of the panel above
<h1>Account balance</h1>
<p>Payment details were updated successfully.</p>
<p lang="mn">Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.</p>
<button type="button" lang="mn">Илгээх</button>

The panel above is rendered inside this page, so its heading is an h3 rather than the h1 a standalone document would use. Everything else is identical. The whole file:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Balance</title>
<link rel="stylesheet" href="node_modules/@arag/css/src/index.css">
</head>
<body>
  <h1>Account balance</h1>
  <p>Payment details were updated successfully.</p>
  <p lang="mn">Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.</p>
  <button type="button" lang="mn">Илгээх</button>
</body>
</html>

Two things in that head are load-bearing and easy to drop. The viewport meta is what makes the fluid type scale resolve against the real device width instead of a fictional desktop one. viewport-fit=cover is the part of it that is easiest to leave off, because nothing breaks visibly until you test on an actual phone: it is what lets the page paint into the notch and home-indicator area instead of stopping short of it, and it is what turns on the four env(safe-area-inset-*) reads behind --safe-block-start, --safe-block-end, --safe-inline-start and --safe-inline-end in tokens.css. Leave it off and those four tokens are permanently 0px, and a bottom-sheet dialog or a bottom toast sits flush under the home indicator instead of clear of it. The package uses those tokens to keep its own components — the sheet, the full-screen dialog, the bottom toast placement — off the unsafe edge, but it cannot do that for chrome that is yours: any bar or button you anchor to a screen edge needs to read the same safe tokens itself, or it inherits the same problem this meta exists to fix. This docs site does not set viewport-fit=cover on its own pages: without it the browser letterboxes the page inside the safe area, so the sticky masthead and sidebar never reach a display cutout in the first place, and setting viewport-fit=cover is exactly what would create that obligation. And lang matters: it is on the root element for the document as a whole, and repeated as lang="mn" on each Mongolian run, which is what the font stack and the line-breaking key off. See bilingual & Cyrillic.

What you get immediately

base.css styles bare HTML elements. It adds no classes and asks for none, so a semantic document is already a styled document. Everything below is covered with zero markup changes.

Here is a fragment using several of those at once. Again, no classes.

Transfers
ReferenceAmount
MNT-44711250000
MNT-447298400
Хэрэглэгчийн тохиргоо

Гуйвуулга баталгаажлаа

Markup of the panel above
<table>
  <caption>Transfers</caption>
  <thead>
    <tr><th scope="col">Reference</th><th scope="col" data-numeric>Amount</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">MNT-4471</th><td data-numeric>1250000</td></tr>
  </tbody>
</table>

<details>
  <summary lang="mn">Хэрэглэгчийн тохиргоо</summary>
  <p lang="mn">Гуйвуулга баталгаажлаа</p>
</details>

<label for="phone" lang="mn">Утасны дугаар</label>
<input id="phone" type="tel" inputmode="tel">

What is deliberately not covered

Scrollbars are left alone. Every custom scrollbar we have shipped has been worse than the native one on at least one of the four target browsers, and on the kiosk build the native one is what the touch layer expects. There is no scrollbar-width, scrollbar-color or ::-webkit-scrollbar rule anywhere in the package.

There are also no component classes yet. src/components/ and src/utilities.css are empty — the layers are declared and reserved, but nothing is in them. Until they land, build components in your own app CSS, unlayered, and let them sit above the system. That is the same mechanism described in cascade layers, so nothing changes when the real components arrive except that you get to delete code.

Importing individual files

Every source file wraps its own content in its own @layer block. reset.css declares @layer reset, tokens.css declares @layer tokens, base.css declares @layer base. None of them relies on index.css having run first, so each file is correct when linked on its own.

The realistic use case: an application that already has its own component CSS and does not want element defaults imposed on it, but does want the @arag palette, spacing scale and type scale so it matches the rest of the product family. Link tokens.css alone and use the custom properties in the rules you already have.

<link rel="stylesheet" href="node_modules/@arag/css/src/tokens.css">
/* your existing CSS, unchanged apart from the values */
.card {
  padding: var(--space-5);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  color: var(--color-text);
}

Because tokens.css is also where color-scheme and the light-dark() pairs live, this gets you working dark mode without adopting anything else.

The subpaths are declared in package.json, so bundlers and bare @import specifiers both resolve them:

"exports": {
  ".":            "./src/index.css",
  "./reset.css":  "./src/reset.css",
  "./tokens.css": "./src/tokens.css",
  "./base.css":   "./src/base.css",
  "./src/*":      "./src/*"
}
What each specifier gets you.
SpecifierResolves toUse when
@arag/csssrc/index.css You want the system. This is the default and what almost everything should use.
@arag/css/tokens.csssrc/tokens.css You want the values only, for your own component CSS.
@arag/css/reset.csssrc/reset.css You want a token-free reset, standalone.
@arag/css/base.csssrc/base.css Rarely on its own — its var() references need the tokens file to resolve.

Your first override

All six of the system's layers sit below unlayered CSS. Your application stylesheet is unlayered unless you deliberately put it in a layer, so it already outranks the whole package. Overriding is therefore just writing the rule you would have written anyway.

Say base.css gives buttons a neutral surface, and the portal needs its primary action brand-filled. Before, in the package:

/* @arag/css/src/base.css — inside @layer base */
button {
  background-color: var(--color-surface);
  color: var(--color-text);
}

After, in your own stylesheet, loaded after the @arag link:

/* app.css — unlayered */
button {
  background-color: var(--color-brand-solid);
  color: var(--color-text-on-solid);
}

Both rules are a bare type selector — specificity (0,0,1) on each side. Yours wins anyway, because unlayered declarations outrank every layered one no matter what the specificity says. You did not need an extra class, a doubled selector, an :is() wrapper or a parent scope, and you did not need to know anything about how base.css was written.

Why this matters

This is the whole reason the package contains no priority-raising declarations: there is nothing here for you to fight, so you should never need one either. If you find yourself reaching for !important against @arag, treat that as a bug report rather than a solution. Cascade layers covers the mechanism, the one trap that has actually bitten this codebase, and the recipes for changing tokens rather than rules.

File map

What is in src/. Layers apply in the order listed.
FileLayerDoes
src/index.css Declares the layer order, then imports the rest. The entry point. Has no declarations of its own.
src/reset.cssreset Removes browser default rendering. No var(), no colours, no sizes — token-free by design, so it is correct standalone.
src/tokens.csstokens Every literal value in the system, as custom properties on :root, plus color-scheme and the [data-theme] switches.
src/base.cssbase Token-aware element defaults — body, headings, links, lists, tables, form controls, focus, selection, details, dialog.
src/components/components Empty. One file per component, next session. The layer is already declared and reserved.
src/utilities.cssutilities Empty. A static, hand-picked class list, next session. No generator, no arbitrary-value syntax.
overrides Declared, never written to by the package. Reserved for the consuming application's escape hatch.

Verify your install

Load the page, open devtools, and paste this into the console. It asks the browser what a token actually resolved to, which is a stronger check than seeing that the page looks roughly right.

getComputedStyle(document.body).getPropertyValue('--color-brand-600').trim()

A working install returns an oklch(...) string. On this page, right now, that value is . An empty string means the custom property is not defined on the document at all, and that has exactly two causes: the stylesheet never loaded, or it loaded from the wrong path. Check the Network panel for a 404 on index.css, and remember that the @import rules inside it resolve relative to index.css itself, not to your HTML file — so moving index.css out of src/ without moving its siblings breaks it quietly.

To check the whole chain rather than one property, read one token from each part of the scale at once:

['--color-brand-600', '--space-4', '--text-base', '--radius-md']
  .map(function (t) {
    return t + ': ' + getComputedStyle(document.body).getPropertyValue(t).trim();
  })
  .join('\n')

Any empty value in that list is the same diagnosis. If all four resolve, the tokens layer is live and everything above it is reading from it.