npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@appshell/tokens

v1.0.0-alpha.97

Published

The Appshell design token contract, its presets, and contrast validation

Downloads

10,207

Readme

@appshell/tokens

The design token contract an Application supplies and every package it composes consumes.

A catalogue of components that cannot be themed to their host is not composable — visual coherence is the point. This package is the vocabulary that makes it possible: 41 CSS custom properties, a set of presets, and the contrast validation that keeps a theme legible.

For a package author

Author against the names, always with a fallback so the package still renders standalone:

.button {
  background: var(--appshell-primary, #0284c7);
  color: var(--appshell-on-primary, #fff);
  border-radius: var(--appshell-radius-md, 0.5rem);
  padding: var(--appshell-space-sm, 0.5rem) var(--appshell-space-md, 1rem);
}
.button:hover  { background: var(--appshell-primary-hover); }
.button:focus-visible { outline: 2px solid var(--appshell-focus-ring); outline-offset: 2px; }

Never write a literal colour you expect to be themed. That is the one rule.

The contract

Colour is semantic; dimensions are a scale. --appshell-surface says what a colour means; --appshell-space-md is honestly just a size. Nobody wants --appshell-space-card-padding.

Every colour that carries text has an on- partner. Write background: var(--appshell-primary); color: var(--appshell-on-primary) and you cannot produce an illegible control whatever palette the Application supplies. It is also what makes dark mode a change of values rather than a change of logic.

| Group | Roles | | --- | --- | | Surface | surface, on-surface, surface-raised, on-surface-raised | | Text & line | text-muted, border | | Action | primary, on-primary, secondary, on-secondary | | Derived | primary-hover, primary-active, secondary-hover, secondary-active, focus-ring | | Status | danger, warning, success and their on- partners | | Type | font-body, font-mono, font-size-h1…h6, -body, -small, line-height-tight, -normal | | Dimension | space-xs…xl, radius-sm/md/lg |

Type sizes are named for their role rather than a sm/md/lg scale on purpose. A numeric scale reintroduces the divergence the contract exists to prevent — one author maps h1 to 2xl, another to xl, and headings stop matching across a composed page.

Derived roles cost a theme nothing. Hover and active are a color-mix from their accent, and the focus ring is picked per base and accent. A theme may override any of them; none has to be supplied. So the contract publishes 41 names while an Application authors 36 at most — and usually none, because it picks presets.

Presets: base × accent

Themes are two choices, not 41 values:

import { toCss } from '@appshell/tokens';

toCss({ base: 'midnight', accent: 'ice' });

Bases carry surfaces, text, border and status, as a light/dark pair: neutral, midnight. Accents carry the two action colours: ice, ember, emerald, violet, steel. Either slot also accepts raw token values, and overrides adjusts a preset without forking it.

Every combination is verified — see below — and midnight/ice is derived from the first application scheduled to migrate.

Contrast is the validation that matters

Syntax checking earns little. It catches --appshell-primary: bananas; it misses --appshell-on-primary: #fff on --appshell-primary: #fff, which is perfectly valid CSS and invisible text.

import { composeTheme, validateTheme, describeFinding } from '@appshell/tokens';

validateTheme(composeTheme({ base: 'neutral', accent: 'ice' }, 'light')).map(describeFinding);

Text pairs are checked at WCAG AA (4.5:1), the focus ring at 3:1 against its surface (1.4.11, non-text). A value that will not parse is reported rather than thrown, so one bad token does not hide the rest.

Two things this caught while the presets were being chosen, both of which would otherwise have shipped:

  • Tailwind's indigo-500 fails against both black and white — 4.28 and 4.22. There is no legible text colour for it. The violet accent uses indigo-600 instead.
  • A focus ring that is simply the accent disappears on 7 of the 20 preset combinations. The rings are derived instead: hue and chroma are kept, lightness moves only as far as it must to clear 3:1.

The presets are held to the same standard as everyone else's themes — the test suite composes all 20 combinations and asserts no findings.

Colour format

Values are oklch. Tailwind v4 consumes them directly and uses color-mix for its opacity modifiers. Tailwind v3 implements bg-primary/50 as rgb(var(--x) / <alpha-value>) and needs channel-separated values, so a v3 package either forgoes opacity modifiers on themed colours or derives channel-split locals in its own stylesheet. The contract is not deformed for the older version.

Modes

Three states, not two — an explicit choice in either direction plus the system default:

:root                                        { /* light */ }
@media (prefers-color-scheme: dark) {
  :root:not([data-appshell-theme="light"])   { /* dark, unless light was chosen */ }
}
:root[data-appshell-theme="dark"]            { /* dark, chosen */ }

Only the roles that actually differ are restated in the dark blocks. Type and dimensions never vary, and repeating them would triple the size of something the registry inlines into every page it serves.