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

@propellerads/tokens

v0.2.0

Published

Design tokens for the PropellerAds group's brands, as plain CSS. There is no build step and no source format: the published files are the tokens.

Readme

@propellerads/tokens

Design tokens for the PropellerAds group's brands, as plain CSS. There is no build step and no source format: the published files are the tokens.

Install

bun add @propellerads/tokens
import '@propellerads/tokens/common.css'
import '@propellerads/tokens/propellerads.css' // or monetag / propush / zeydoo

Two files: the shared one and one brand. Both define variables on :root, they define different names, and neither uses @import — so the order between them does not matter.

common.css holds what every brand shares: statuses, greys, overlays, the spacing and type scales, breakpoints and shadows. A brand file holds three things: its palette, its action colour and its typeface.

What components read

Semantic names only — --color-action, --color-rich, --color-danger, --space-16. Raw greys have no token of their own on purpose: if they did, changing one border colour in one brand would drag along every other place that grey is used.

Roles point at the brand palette through var(), so overriding --color-action also moves everything built on it. The same goes for the palette: --color-highlight and its pad follow --color-brand-primary.

In CSS and SCSS

Write the variable where the value goes. The browser resolves it against whatever :root the page has, so one rule follows both the brand and the theme:

.card {
  background: var(--color-light-inversion);
  color: var(--color-dark-inversion);
  border: 1px solid var(--color-rich);
  padding: var(--space-16);
  box-shadow: var(--shadow-middle);
}

In SCSS the same applies, with one exception: Sass colour functions such as color.adjust() run at build time and cannot see through var(). Use color-mix(), which runs in the browser.

Two things fail quietly:

@media (max-width: var(--breakpoint-mobile)) { }  /* a media query cannot read var() */
color: var(--over-fade);                        /* not a colour — see Overlays */

In styled-components

The same string inside the template literal. Don't import a JavaScript constant for a colour: it is baked in when the component renders and cannot follow a theme switch.

A component with several states declares its own variables once at the top, with the current value as a fallback, and reads only those below:

const Button = styled.button`
  --button-bg:       var(--color-action, hsl(210 100% 50%));
  --button-bg-hover: color-mix(in srgb, var(--over-deepen), var(--button-bg));
  --button-fg:       var(--color-light, hsl(0 0% 100%));

  background: var(--button-bg);
  color: var(--button-fg);

  &:hover { background: var(--button-bg-hover); }
`;

The hover is mixed from --button-bg rather than from --color-action, and that is the point: a button switched to the success tone darkens its own green. Mix against the element's variable, never against the token it happens to hold today.

There is no --color-action-hover, and no hover token for anything else. A state is not a colour of its own — it is the colour the element is already showing with an overlay baked in, and a custom property cannot take that colour as an argument. Two lines cover every case. An element that owns its surface deepens it:

--thing-bg:       var(--color-action);
--thing-bg-hover: color-mix(in srgb, var(--over-deepen), var(--thing-bg));

An element with no surface of its own — a ghost button, a link, a table row — has nothing to deepen, so it wears its own tone at --opacity-tint instead, a film over whatever it happens to be standing on:

--thing-bg:       transparent;
--thing-bg-hover: color-mix(in srgb, var(--thing-tone) var(--opacity-tint), transparent);

It cannot wash towards the page instead: over a panel or a photograph that wash comes out the colour of a background the element is not on.

Pressed takes no third colour: move it a pixel. A third shade on top of the hover only muddies it.

That block is also the component's override API, and needs no context — any ancestor can set it:

.checkout button { --button-bg: var(--color-success); }

A prop may still decide which token applies; what it must not do is carry the colour itself.

In plain JavaScript

Setting is straightforward — a custom property is a property:

document.documentElement.style.setProperty('--color-action', '#0080ff');
document.documentElement.dataset.theme = 'dark';   // delete it to follow the system

Reading has one trap. A custom property computes to its text with var() substituted and nothing else evaluated, so anything built with color-mix() reads back as the expression itself:

const styles = getComputedStyle(document.documentElement);
styles.getPropertyValue('--breakpoint-mobile');    // '481px'
styles.getPropertyValue('--color-danger-bg');
// 'color-mix(in srgb, hsl(0 0% 100%) 90%, hsl(0 85% 55%))'

When a real colour is needed — a canvas, a chart, an inline SVG — put the token on a real property first and read that back:

const probe = document.createElement('span');
probe.style.backgroundColor = 'var(--color-danger-bg)';
document.body.append(probe);
const colour = getComputedStyle(probe).backgroundColor;  // 'rgb(253, 234, 234)'
probe.remove();

Reach for this only when the value has to leave CSS. To style an element, a class and a rule that reads the token survives a theme switch; a value copied into JavaScript does not.

Colours

The two ends of the scale are the one place where a name says where a colour sits rather than what it does, because that is all a label on a filled button and the stuff of an overlay have in common. --color-light is always the light end — text and icons on a coloured surface, in both themes. --color-light-inversion is the page surface and goes dark. Same pair for black: --color-dark is for overlays and stays black, --color-dark-inversion is body text and flips.

| Token | Light | Dark | What it is for | |----------------------------------|-----------|-----------|---------------------------------------------------| | --color-light | #FFFFFF | #E6E6E6 | Anything that keeps its colour whatever the theme — a label on a filled button, and the button tone for a surface the system does not own | | --color-light-inversion | #FFFFFF | #212121 | The page background | | --color-dark | #000000 | #000000 | Anything that keeps its colour whatever the theme — what an overlay is made of | | --color-dark-inversion | #000000 | #F2F2F2 | Body text |

Everything that carries no status at all is a triple of its own, shaped like a status but with the colour taken out. What the grey scale has left after those three steps is two dividers, told apart by whether they are meant to be noticed.

| Token | Light | Dark | What it is for | |-----------------------|-----------|-----------|---------------------------------------------------| | --color-quiet | #B3B3B3 | #737373 | Icons and anything drawn like one | | --color-quiet-bg | #FAFAFA | #333333 | Neutral pads on the page background | | --color-quiet-text | #808080 | #999999 | Supporting text: the description and hints roles | | --color-formal | #F2F2F2 | #262626 | Formal dividers — the ones that only order a list | | --color-rich | #CCCCCC | #4D4D4D | Dividers that are meant to be seen |

The status colours are picked by what they say, never by their hue: --color-action for interactive elements — buttons, links, controls; --color-success for a scenario that worked; --color-warning for a warning, where nothing is broken yet; --color-danger for errors, scenarios that failed and actions that cannot be taken back. The action colour carries the same pair — --color-action-bg and --color-action-text — for what the interface is telling you rather than asking: a tip, a note, a highlighted row. That pad is not a button's surface; a button fills itself with the action colour itself.

None of them carries a -hover. A state is the colour the element is already showing with an overlay baked in, and which overlay it is depends on the element rather than on the colour: a form that owns its surface deepens that surface, a form that does not washes the tone into a tint instead. One token cannot answer both, so the rule lives with the component and this file only supplies what it mixes with.

A status is a triple — the colour itself, the pad it sits on (-bg) and the text on that pad (-text):

.error {
  background: var(--color-danger-bg);
  color: var(--color-danger-text);
}

| Status | Value | |-------------------|-----------| | --color-success | #00B359 | | --color-warning | #FFAA00 | | --color-danger | #EE2B2B | | --color-action | per brand |

--color-highlight takes the same three steps without being a status. It is the brand's own colour worn by the interface — a promo panel, a tip, a block the page wants noticed — so it points at the palette rather than at --color-action. Which of the two palette colours it takes is the brand's call: it is the first one by default, and Monetag and Zeydoo override it with the second in their own files — Monetag because its first colour is already the action colour and a highlight would be indistinguishable from it.

| Brand | --color-highlight | pad | text on the pad | dark pad | text there | |---|---|---|---|---|---| | PropellerAds | #D9D926 first | #FBFBE9 | 1.80 | #464622 | 5.19 | | Monetag | #A4D65E second | #F6FBEF | 2.00 | #3B452D | 4.78 | | Propush | #19D2FA first | #E8FAFE | 2.09 | #1F444C | 4.68 | | Zeydoo | #28C8D2 second | #E9FAFA | 2.34 | #224244 | 4.28 |

A status colour is picked to be read against; a palette colour is picked to be looked at, and the pad inherits that. All four hold in the dark theme and none in the light one, so this pad carries a shape, a heading or a short line, and a paragraph on it stays on --color-dark-inversion.

The pads are the colour with an overlay baked in through color-mix, and that overlay changes with the theme — so one formula covers both themes.

-text is the same colour darkened by a tenth. That lifts it off its own pad for a label or a heading, and it is not enough for a paragraph: against its own pad danger reaches 4.34, success 3.04 and warning 2.19, where body text asks for 4.5. A paragraph on a pad stays on --color-dark-inversion.

Anything drawn on a pad — an icon, a rule, a chart — is the status colour itself, and it answers to 3:1 rather than 4.5. On its own pad only danger reaches it, at 3.62; success at 2.49, warning at 1.78 and --color-quiet at 2.01 do not, so on those three a pad is a place for text and not for a shape that has to be made out.

Per brand, --color-action is what buttons and links use; the palette is for illustration and marketing surfaces. Only Monetag makes its brand colour the action colour — the Propush palette is a light cyan and a near-black, and neither survives as a button.

| Token | PropellerAds | Monetag | Propush | Zeydoo | |---------------------------|--------------|-----------|-----------|-----------| | --color-action | #0080FF | #4A3795 | #0080FF | #0080FF | | --color-brand-primary | #D9D926 | #4A3795 | #19D2FA | #5C10DD | | --color-brand-secondary | #14B082 | #A4D65E | #231E1E | #28C8D2 | | --font-family | Roboto | Gilroy | Roboto | Roboto |

Overlays

There are three, and they are what the pads and the states are built from.

--over-fullscreen is a colour with alpha, for the dimming behind a modal or a drawer:

.scrim { background: var(--over-fullscreen); }

--over-fade and --over-deepen are written as colour + amount, which is the form color-mix() takes, because baking an overlay into a colour is the only thing they are for. They are not colours — background: var(--over-fade) does nothing. Use them like this:

background: color-mix(in srgb, var(--over-deepen), var(--color-action));

They are named for what they do to a colour rather than for where it lands: fade pushes it towards the background of the current theme, deepen darkens it. Deepen builds both the text on a pad and the hover of a button, so a name taken from either would lie about the other.

Fullscreen is the one of the three that is a colour with alpha rather than a colour and an amount, because it is laid over the page rather than mixed into something; the amount itself is --opacity-fullscreen.

Fade turns over with the theme — white in the light one, black in the dark — and deepen is black in both. That is why no pad carries a dark value of its own: they are built on fade, and fade has already turned over.

Spacing and corners

Both scales are the 4 px grid. --space-4 is one step — Basic in the design file — and every other name is that step multiplied: --space-8 is 2X, --space-80 is 20X. Corners run on the same grid and stop at --radius-40.

.card {
  padding: var(--space-16);   /* 4X */
  gap: var(--space-8);        /* 2X */
  border-radius: var(--radius-8);
}

The token name carries the pixel value rather than the multiplier, so a value read off the design file is looked up without arithmetic, and the same name cannot come to mean a different number later.

Two names are on the list without being steps of the grid: --space-0 is a reset, and --space-1 is for hairlines and rules. A component may still use its own padding internally for optical alignment — the scale governs the space between things, not the inside of a control.

Transparency

Three amounts, smallest first, and not a scale: the distance between two of them means nothing, and none can stand in for another. What differs is the operation, not the unit — two are the share of a colour a mix keeps, the third is how much of an element is left, and opacity takes a percentage as readily as a number.

| Token | Value | What it is | |---|---|---| | --opacity-tint | 15% | How much of its own colour an element with no surface of its own shows under the pointer | | --opacity-disabled | 40% | How much of a control that is switched off is left — its contour and its focus ring fade with it | | --opacity-fullscreen | 50% | How much of the dark end covers the page under a modal — 80% in the dark theme |

.thing:disabled { opacity: var(--opacity-disabled); }
.quiet:hover { background: color-mix(in srgb, var(--color-action) var(--opacity-tint), transparent); }

--over-fullscreen is built from the last of these rather than carrying a number of its own, so the dark theme overrides the number and the overlay follows.

The tint is written from the colour's side on purpose. An element that owns no surface cannot wash itself towards the page, because the page may not be what it is standing on — over a panel or a photograph that wash comes out the wrong colour. Its own colour at a low opacity is right on anything: darker on a dark panel, paler on a light one.

Motion

Duration is chosen by how far the thing travels and by who started it, not by how important it is, and the name carries the value in milliseconds the way --space-16 carries pixels.

| Token | Use it for | |------------------|------------------------------------------------------------------------------------------------| | --duration-100 | What answers something already under way: the press of a button, anything that has to land at once | | --duration-200 | What the pointer changes — hover and focus colours — and small things appearing beside what opened them: hints, tooltips, dropdowns | | --duration-300 | Something large crossing the page: modals, drawers, the mobile menu, a bar sliding in from an edge |

A colour travels nowhere, and still takes --duration-200 when the pointer is what changed it: at 100ms a hover reads as a flicker rather than as the interface answering. The shortest step is for the other case — a press, where the finger is already down and a fifth of a second reads as hesitation.

Anything that needs longer than --duration-300 is a loading state, not a transition.

Direction is carried by the easing, not by a second set of durations. What arrives decelerates into place; what leaves accelerates out of the way and takes the step below:

.hint {
  transition:
    opacity var(--duration-200) var(--ease-flat),
    transform var(--duration-200) var(--ease-enter);
}

.hint[hidden] {
  transition-duration: var(--duration-100);
  transition-timing-function: var(--ease-exit);
}

--ease-enter and --ease-exit are named for what the element does, not for the CSS keywords, which invert: CSS ease-in starts slowly, which is what a leaving element wants. --ease-flat is linear, for colour and opacity — a curve on a colour reads as a stutter rather than as smoothness.

Two things a token cannot do for you. Do not transition all: with a duration token it will also animate what should not move. And a @keyframes loop, a spinner in particular, is outside this — prefers-reduced-motion cuts the durations in common.css, but an animation has to answer that query itself.

Dark theme

common.css switches on the operating system setting, and on data-theme:

<html data-theme="dark">  <!-- always dark, whatever the system says -->
<html data-theme="light"> <!-- always light -->
<html>                    <!-- follows the system -->

Only the greys, the overlays and the shadows move; the brand palette and the statuses are the same in both themes. The overrides are listed twice in the file, once per switch, because a media query and a plain selector cannot share a rule — change a dark value in both blocks.

Sizes

Type is named by role, not by size — H4, Text and Paragraph are all 14px and differ in weight and leading. A role is one token, for the font shorthand, and the whole style lands in one declaration:

.card__title {
  font: var(--font-h3);
}

There is no separate size, leading or weight token: the numbers only mean anything together, and a role you can take apart is a role that gets assembled wrong. The family is the one part that stays a variable, because it is the brand's.

font is a shorthand, so it also resets font-style, font-variant and font-stretch. That is what you want on a heading and a trap inside one: set the italic after the shorthand, never before.

| Role | Weight | Size | Line height | |---|---|---|---| | --font-hero-max | 700 | 100px | normal | | --font-hero-basic | 700 | 52px | normal | | --font-h1 | 700 | 28px, 20px below the mobile breakpoint | normal | | --font-h2 | 700 | 20px, 18px below the mobile breakpoint | normal | | --font-h3 | 700 | 16px | normal | | --font-h4 | 700 | 14px | normal | | --font-text | 400 | 14px | normal | | --font-paragraph | 400 | 14px | 20px | | --font-description | 400 | 12px | 16px | | --font-hints | 400 | 10px | normal |

normal is what the design file calls Auto. Sizes are in pixels, not em or rem, so a root font size cannot move them behind the page's back, and common.css applies the two mobile steps on its own — a role is one value, so the media query repeats the whole declaration rather than a size inside it.

| Group | Tokens | |--------------|--------------------------------------------------------------------------------------------| | Type role | --font-hero-max, -hero-basic, --font-h1…-h4, --font-text, -paragraph, -description, -hints | | Font family | --font-family — the brand's, and the only part of a role that is a variable | | Space | --space-0, -1, -2, -4, -8, -12, -16, -20, -24, -32, -40, -80 — the number is the pixel value | | Corner | --radius-2, -4, -8, -16, -24, -32, -40 — the number is the pixel value | | Shadow | --shadow-little, --shadow-middle, --shadow-big | | Breakpoint | --breakpoint-mobile (481px), --breakpoint-tablet (769px), --breakpoint-desktop (1025px) |

A breakpoint token is the first width above the range it names, so the queries in common.css read max-width: 480px. Media queries cannot read var(), so those numbers are written out there; the tokens exist for JavaScript.