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

mates-css

v0.5.0

Published

CSS library for all frameworks like React, Vue, Mates, Angular, Solid, Svelte, Lit. Even plain JS. It even comes with a design system. A compact pure-CSS no-JS library with design tokens, automatic dark mode, flex & grid utilities, and CSS-only charts.

Readme

Mates CSS

We don't flash, we deliver!

CSS library for all frameworks like React, Vue, Mates, Angular, Solid, Svelte, Lit. Even plain JS. It even comes with a design system.

A compact pure-CSS UI kit — 75.0kb raw (12.4kb gzipped). No JS, no SCSS. Framework-agnostic: drop the stylesheet into any HTML, React, Vue, Svelte, or plain page.

Styled via native tags when possible (button, label, input, table…). Classes are variants (.btn-primary) or the same look on a non-native host (<a class="btn">). Overlay open state is the .open class — your app toggles it.

Sources live in src/. npm run build bundles them into mates.min.css at the repo root. import "mates-css" resolves to that single minified file.

Use it

CDN

<link rel="stylesheet" href="https://unpkg.com/[email protected]/mates.min.css">

npm

npm install mates-css
import "mates-css";
<link rel="stylesheet" href="node_modules/mates-css/mates.min.css">

Unminified bundle: mates-css/css or mates.css. Copy that file if you don't want a bundler.

Use with AI

mates-css is the CSS library an agent should load and build against. It is not the Mates JS framework. If a widget is missing, the agent should compose native HTML and --m-* tokens — not invent classes or pull in another CSS kit.

In Cursor / Claude / ChatGPT, attach or fetch:

  • Contract: llms.txthttps://unpkg.com/mates-css/llms.txt or https://raw.githubusercontent.com/thejsmaster/mates-css/main/llms.txt
  • Project rules: AGENTS.md
  • Live markup: docs.html

Paste this into custom instructions or a project prompt:

Build UI with mates-css. Always call it mates-css — never "Mates".
Mates is a separate JavaScript framework. mates-css is a framework-agnostic
CSS library: it pre-styles native HTML (button, input, table, details, …)
and offers optional CSS classes. It has no JavaScript runtime.

Install: npm install mates-css then import "mates-css"
Read the class contract: https://unpkg.com/mates-css/llms.txt

Use only classes listed there. Native HTML first; classes are variants
(.btn-primary) or the same look on a non-native host (<a class="btn">).
Overlays (.modal .menu .pop .drawer .sheet .search): add class .open
Current item: class .selected (not .active, except drag-over on .drop).

If mates-css has no component, build it from scratch with native HTML,
var(--m-*) tokens, and .m-* utilities. Do not invent class names.
Do not add Tailwind, Bootstrap, Bulma, or another CSS framework.

Copy .cursor/skills/mates-css/ into an app to auto-apply the same rules when the agent writes UI.

Dark mode

Follows the OS by default (prefers-color-scheme). Leave data-theme off unless you need to override.

<html data-theme="dark">
<html data-theme="light">

Toggle at runtime:

document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.removeAttribute('data-theme'); // back to OS

Open states

Menus, popovers, modals, drawers, sheets, and search panels stay closed until you add .open on the root. Nothing auto-opens on focus or via a hidden checkbox — toggle the class from your click, focus, or framework handler.

el.classList.add('open');
el.classList.remove('open');
el.classList.toggle('open');

Tokens

Everything is a --m-* CSS variable. Override on :root to rebrand:

:root {
  --m-primary: #0172ad;
  --m-primary-h: #016091;   /* hover */
  --m-primary-a: #014e75;   /* active */
  --m-primary-soft: rgba(1,114,173,.12);
  --m-primary-soft-2: rgba(1,114,173,.22);
  --m-primary-soft-3: rgba(1,114,173,.30);
  --m-on-primary: #ffffff;  /* text on primary fills */
  --m-r: 9px;               /* reshape everything */
  --m-font: "Inter", sans-serif;
  --m-fs: 16px;
}

Override type size with --m-fs (default 16px, set on :root so rem scales with it).

Stacking, low to high: --m-z-sticky (20) → --m-z-menu (40, also popover and search) → --m-z-drawer (90, also sheet) → --m-z-modal (100) → --m-z-toast (110) → --m-z-tooltip (120) → --m-z-skip (200).

Component index

| Component | How you get it | |---|---| | Buttons | bare <button> (.btn on links) | | Tags | .tag | | Forms | bare <label> / inputs / .switch / .fgroup | | Form extras | .field, .input-wrap, .chip, .rate, .seg, .drop, .search | | Files, number, OTP, cards | .files / .num / .otp / .pick | | Cards | .card + parts | | Tables | bare <table> (+ .table-wrap, .compact, .sorted) | | Tabs | .tabs + .tab (+ .tabs-v) | | Alerts & toasts | .alert / .toast / .toasts / .banner | | Skeleton, spinner, empty | .skel / .spin / .empty | | Result & count | .result / .count | | Tooltips | .tooltip + data-tip | | Modal | .modal + .open | | Menu | .menu + .open | | Drawer & sheet | .drawer / .sheet + .open | | Popover | .pop + .open | | Accordion | bare <details> | | Nav | .nav | | Breadcrumbs, steps | .crumbs / .steps | | Sidenav | .sidenav | | Bottom nav | .bottom-nav | | Subnav, TOC, footer, FAB | .subnav / .toc / .footer / .fab | | Avatar, status, badge | .avatar / .status / .badge | | Progress | bare <progress> | | Lists & timeline | .list / .timeline | | Stats & definition list | .stat / bare <dl> / .props | | Messages, quote, figure | .msg / .quote / .fig / .link-card / .tree | | Code & prose | bare <code> / <pre> / .prose | | Charts | .chart-bars, .donut, .ring, .spark | | Shell & layout blocks | .shell, .split, .aspect | | Layout utilities | .m-flex, .m-grid, .m-row, … |


Zero-class styling

These tags are styled with no class:

  • <button>, <input type="submit|button|reset">
  • <label> (field captions; checkbox/radio wraps stay inline)
  • <input>, <select>, <textarea>
  • <table>, <fieldset>, <legend>, <details>, <progress>
  • <code>, <pre>, <dl>
  • headings, links, hr, blockquote, kbd, mark, lists
<button>Default</button>
<button class="btn-primary">Primary variant</button>
<label for="email">Email</label>
<input type="text" placeholder="…">
<input type="checkbox"> Remember me

Use .btn / .input / .check / .label / .table / .acc / .progress when you need the same look on a non-native element (e.g. <a class="btn">).


Buttons

Required: bare <button> or class .btn on a link/span.

Optional variants (on the same element):

| Class | Role | |---|---| | .btn-primary .btn-dark .btn-light .btn-success .btn-warning .btn-danger | solid fills | | .btn-outline .btn-soft .btn-ghost .btn-link | quiet styles | | .btn-sm .btn-lg .btn-block | size / full width | | .btn-icon | square icon-only control | | .btn-elevated | faint rest shadow (not a 3D bevel) | | .btn-spin | loading spinner (::after) | | .btn-close | icon-only close control | | .fab | circular action; pin with .fixed | | .btn-group | on the parent wrapping sibling buttons |

<button>Default</button>
<button class="btn-primary">Primary</button>
<button class="btn-soft">Soft</button>
<a class="btn btn-outline" href="#">Link as button</a>

<button class="btn-sm btn-spin">Loading</button>
<button class="btn-elevated btn-primary">Elevated</button>
<button class="btn-close" aria-label="Close"></button>
<button class="btn-icon" aria-label="Add">+</button>

<div class="btn-group">
  <button class="btn-primary">Left</button>
  <button class="btn-primary">Middle</button>
  <button class="btn-primary">Right</button>
</div>

Tags

Required on the tag element: .tag

Optional: .tag-primary .tag-success .tag-warning .tag-danger .tag-soft .tag-soft-success .tag-soft-danger .tag-rounded

Removable: put .tag-x on a child <span> inside the tag.

<span class="tag">Default</span>
<span class="tag tag-primary">Primary</span>
<span class="tag tag-soft-success">active</span>
<span class="tag tag-primary">Removable <span class="tag-x"></span></span>

Forms

Labels & hints

Bare <label> is the field caption. Use .label on a <span> if you need the same look without a <label>. .hint is the sibling under the field.

Text fields

Bare <input>, <select>, <textarea> are styled. Use .input to apply the same styles to custom elements, or to pair with width utilities.

| Class | Where | |---|---| | .input | on the control (optional for native tags) | | .input-success / .input-error | on the control | | .m-error / .m-valid | on a wrapper around label + field + hint | | .m-w-auto .m-w-xs .m-w-sm .m-w-md .m-w-lg .m-w-full | on the control (width) |

Inputs default to width: var(--m-input-w) (100%). Override globally (:root { --m-input-w: 24rem }) or per field (style="--m-input-w: 10rem").

<label for="email">Email</label>
<input id="email" type="email" placeholder="[email protected]">
<span class="hint">We'll never share it.</span>

<div class="m-error">
  <label for="user">Username</label>
  <input id="user" type="text">
  <span class="hint">Only letters and numbers.</span>
</div>

<input class="input-success m-w-md" type="text" value="Looks good">

Checkbox / radio

Bare input[type=checkbox|radio] are styled. Wrap the control + caption in <label class="choice"> so they sit in a row. (Newer browsers can infer the row with :has(); .choice is the compatible API.)

<label class="choice"><input type="checkbox" checked> Checked</label>
<label class="choice"><input type="radio" name="r" checked> Radio A</label>

Switch

Required: .switch on a <label> wrapping a real checkbox + empty <i>.

<label class="switch"><input type="checkbox" checked><i></i></label>

Range

Bare input[type=range] is styled. The filled track reads --v (0–100). Default is 50. Sync from JS on input if you want a live fill:

<input type="range" min="0" max="100" value="50"
 oninput="this.style.setProperty('--v', this.value)">

Form group (prefix / suffix)

Required: .fgroup on the flex row. Put .fgroup-label on the prefix/suffix span; nest the input and button as siblings.

<div class="fgroup">
  <span class="fgroup-label">https://</span>
  <input type="text" value="example.com">
  <button class="btn-primary">Go</button>
</div>

Files, number, OTP, choice cards

<div class="files">
  <div class="file">
    <span class="file-name">brief.pdf</span>
    <span class="file-meta">128 KB</span>
    <button class="btn-close" aria-label="Remove"></button>
  </div>
</div>

<div class="num">
  <button type="button" class="num-btn" aria-label="Decrease">−</button>
  <input type="number" value="2">
  <button type="button" class="num-btn" aria-label="Increase">+</button>
</div>

<div class="input-wrap">
  <input type="password" value="secret">
  <button type="button" class="pass-toggle">Show</button>
</div>

<div class="otp">
  <input maxlength="1" inputmode="numeric" aria-label="Digit 1">
  <input maxlength="1" inputmode="numeric" aria-label="Digit 2">
</div>

<label class="pick selected">
  <input type="radio" name="plan" checked>
  <span>
    <span class="pick-title">Pro</span>
    <span class="pick-sub">$20 / month</span>
  </span>
</label>

Number +/− and password visibility are CSS slots — toggle the value / type from your app. Choice cards use .selected on older browsers.


Cards

| Class | Where | |---|---| | .card | outer container | | .card-body | padded content | | .card-title / .card-sub | heading / subtitle inside body | | .card-bar | footer action row | | .card-img | full-bleed image | | .card-hover | optional, on .card for lift on hover |

<div class="card card-hover">
  <div class="card-body">
    <p class="card-title">Card title</p>
    <p class="card-sub">With subtitle</p>
  </div>
  <div class="card-bar">
    <button class="btn-primary btn-sm">Action</button>
    <button class="btn-sm">Cancel</button>
  </div>
</div>

Tables

| Class | Where | |---|---| | .table-wrap | optional outer wrapper (horizontal scroll) | | (none) | bare <table> is styled | | .table | same look on a non-table wrapper | | .striped / .bordered / .sticky / .compact | on the <table> | | .selected | on a <tr> | | .sorted.asc / .sorted.desc | on a <th> |

<div class="table-wrap">
  <table class="striped">
    <thead>
      <tr><th>Plan</th><th>Price</th><th>Status</th></tr>
    </thead>
    <tbody>
      <tr><td>Free</td><td>$0</td><td><span class="tag tag-soft-success">active</span></td></tr>
      <tr class="selected"><td>Pro</td><td>$20</td><td><span class="tag tag-primary">selected</span></td></tr>
    </tbody>
  </table>
</div>

Tabs

| Class | Where | |---|---| | .tabs | parent | | .tabs-line | optional on parent for underline style | | .tabs-v | optional on parent for a vertical stack | | .tab | each tab control | | .selected | on the active .tab |

<div class="tabs">
  <button class="tab selected">Overview</button>
  <button class="tab">Analytics</button>
  <button class="tab">Settings</button>
</div>

<div class="tabs tabs-line">
  <button class="tab selected">Underline</button>
  <button class="tab">Variant</button>
</div>

<div class="tabs tabs-line tabs-v">…</div>

Alerts & toasts

Alert

Required: .alert on the box. Optional: .alert-primary .alert-success .alert-warning .alert-danger. Optional .alert-head / .alert-title / .alert-actions.

<div class="alert">Neutral alert.</div>
<div class="alert alert-success"><strong>Done.</strong> Changes saved.</div>
<div class="alert alert-danger"><strong>Error.</strong> Please retry.</div>

Toast

Required: .toast. Optional: .toast-primary .toast-success .toast-danger. Title via .toast-title.

<div class="toast">
  <p class="toast-title">Toast</p>
  <p>Neutral dark toast.</p>
</div>
<div class="toast toast-success">
  <p class="toast-title">Success</p>
  <p>Saved.</p>
</div>

Tooltips

Required: .tooltip on the trigger + data-tip="…".

Direction (optional, on the same element): .tooltip-b (bottom), .tooltip-r (right), .tooltip-l (left). Default is top.

<button class="tooltip" data-tip="Top tooltip">Top</button>
<button class="tooltip tooltip-b" data-tip="Bottom">Bottom</button>
<button class="tooltip tooltip-r" data-tip="Right">Right</button>

Modal

Add .open on .modal to show it. Remove .open to hide.

| Class | Where | |---|---| | .modal | overlay root | | .modal-overlay | full-screen dismiss layer | | .modal-box | dialog panel | | .modal-head / .modal-title / .modal-body / .modal-foot | structure inside the box |

<button type="button" class="btn-primary" onclick="document.getElementById('m1').classList.add('open')">Open modal</button>

<div id="m1" class="modal">
  <div class="modal-overlay" onclick="this.parentElement.classList.remove('open')"></div>
  <div class="modal-box">
    <div class="modal-head">
      <p class="modal-title">Modal title</p>
      <button type="button" class="btn-close" aria-label="Close" onclick="this.closest('.modal').classList.remove('open')"></button>
    </div>
    <div class="modal-body">
      <p>Add <code>.open</code> on <code>.modal</code> from your handler.</p>
    </div>
    <div class="modal-foot">
      <button type="button" onclick="this.closest('.modal').classList.remove('open')">Close</button>
    </div>
  </div>
</div>

Accordion

Bare <details> + <summary>. Use .acc only on a non-details host.

<details>
  <summary>Accordion item one</summary>
  <p>Expand/collapse with native details — no JS.</p>
</details>

Nav

| Class | Where | |---|---| | .nav | bar container | | .nav-brand | brand text/link | | .selected | on the active <a> | | .nav-space | spacer that pushes trailing items right |

<nav class="nav">
  <span class="nav-brand">Mates</span>
  <a href="#" class="selected">Home</a>
  <a href="#">Docs</a>
  <span class="nav-space"></span>
  <span class="avatar avatar-sm">AV</span>
</nav>

Avatar

.avatar — optional .avatar-sm / .avatar-lg. Stack with .avatars.

Status: .status + .status-dot with .online / .away / .busy / .offline on the status or the dot.

Badge: wrap a control in .badge. Child .badge-dot or .badge-n with data-n="8".

<span class="avatar">AL</span>
<span class="avatars">
  <span class="avatar avatar-sm">AL</span>
  <span class="avatar avatar-sm">SM</span>
</span>
<span class="status online"><i class="status-dot"></i> Online</span>
<span class="badge">
  <button class="btn-sm">Inbox</button>
  <i class="badge-n" data-n="8"></i>
</span>

Progress

Bare <progress>. Use .progress on a non-native host. Omit value for an indeterminate bar.

<progress value="65" max="100"></progress>
<progress aria-label="Loading"></progress>

Charts

Pure CSS — no JS, no SVG. Data lives in inline custom properties:

| Variable | Used by | Meaning | |---|---|---| | --v | bars, hbars, ring, spark | 0–100 | | --v1--v6 | donut / pie | segment percents | | --sv | stacked bar segments | share of the stack |

Series colors: .s1.s6 (set --m-c). Tokens --m-c1--m-c6 (c1–c4 alias primary/success/warning/danger).

Sizing: --m-chart-h (bars), --m-donut-size / --m-donut-w (donut/ring), --m-spark-h (sparkline).

Add role="img" + aria-label on chart containers for accessibility.

Vertical bars

Required: .chart-bars > .bar. Each bar: set --v on .bar; <i> is the fill; optional data-v on <i> shows a value bubble; <span> is the label.

<div class="chart-bars" role="img" aria-label="Sales by day">
  <div class="bar" style="--v: 65"><i data-v="65"></i><span>Mon</span></div>
  <div class="bar s2" style="--v: 90"><i data-v="90"></i><span>Tue</span></div>
</div>

Stacked bars

Required: .bar.stacked with --v for total height. Inside <i>, each <u style="--sv: …"> is a segment (color with .s2…).

<div class="chart-bars">
  <div class="bar stacked" style="--v: 100">
    <i>
      <u style="--sv: 45"></u>
      <u class="s2" style="--sv: 30"></u>
      <u class="s3" style="--sv: 25"></u>
    </i>
    <span>Q1</span>
  </div>
</div>

Horizontal bars

Required: .chart-hbars > .hbar. Structure: <span> label, <i> track, <b> value. Set --v on .hbar.

<div class="chart-hbars">
  <div class="hbar" style="--v: 80"><span>Design</span><i></i><b>80</b></div>
  <div class="hbar s2" style="--v: 65"><span>Frontend</span><i></i><b>65</b></div>
</div>

Donut / pie

Required: .donut with --v1--v6. Optional center label in <b>. Add .pie for a solid pie (no hole).

<div class="donut" style="--v1: 42; --v2: 28; --v3: 18; --v4: 12">
  <b>1,204</b>
</div>
<div class="donut pie" style="--v1: 60; --v2: 40"></div>

Progress ring

Required: .ring + --v (0–100). Optional center text in <b>. Color with .s1.s6.

<div class="ring" style="--v: 72"><b>72%</b></div>

Sparkline

Required: .spark with child <i style="--v:…"> per point.

<div class="spark">
  <i style="--v:30"></i><i style="--v:55"></i><i style="--v:42"></i><i style="--v:80"></i>
</div>

Legend

Required: .legend. Each item: <span><i class="s1"></i>Label</span>.

<div class="legend">
  <span><i class="s1"></i>Organic</span>
  <span><i class="s2"></i>Paid</span>
</div>
<div class="legend legend-v">…</div>

Form extras

Field stack

Required: .field wrapping <label> + control + .hint.

<div class="field">
  <label for="nm">Name</label>
  <input id="nm" type="text">
  <span class="hint">Public.</span>
</div>

Fieldset

Required: <fieldset> + native <legend>.

Input with icons

Required: .input-wrap. Put .input-ico on leading/trailing slots.

<div class="input-wrap">
  <span class="input-ico">@</span>
  <input type="text">
</div>

Search results

Required: .search wrapping the field. Add .open to show .search-panel (on focus, input, or your own trigger).

<div class="search" id="search">
  <input type="search" placeholder="Search…" onfocus="this.parentElement.classList.add('open')">
  <div class="search-panel">
    <a class="menu-item" href="#">Result</a>
  </div>
</div>

Dropzone

Required: .drop on a <label>. Add .active from JS on dragover.

<label class="drop">
  <input type="file" hidden>
  <span>Drop files</span>
  <span class="hint">or click to browse</span>
</label>

Chips

Required: .chip. Toggle .selected from JS (or start with it on the markup). Nested <input type="checkbox"> also lights the chip on browsers that support :has(). Sibling pattern input:checked + .chip needs no JS.

<label class="chip selected"><input type="checkbox" checked> Design</label>
<button class="chip selected">Filter</button>

Rating

Required: .rate with radios in reverse order (5 → 1) so CSS sibling selectors fill stars.

<span class="rate">
  <input type="radio" name="stars" value="5">
  <input type="radio" name="stars" value="4">
  <input type="radio" name="stars" value="3" checked>
  <input type="radio" name="stars" value="2">
  <input type="radio" name="stars" value="1">
</span>

Segmented control

Required: .seg parent, .seg-item children. Active via .selected. Newer browsers also match a checked radio with :has(:checked).

<div class="seg">
  <label class="seg-item selected"><input type="radio" name="p" checked> Day</label>
  <label class="seg-item"><input type="radio" name="p"> Week</label>
</div>

Skeleton, spinner, empty

<div class="skel skel-avatar"></div>
<div class="skel skel-text"></div>
<div class="skel skel-card"></div>

<i class="spin"></i>
<div class="spin-block"><i class="spin spin-lg"></i></div>

<div class="empty">
  <p class="empty-title">No projects</p>
  <p class="muted">Create one to get started.</p>
  <div class="empty-actions"><button class="btn-primary">New</button></div>
</div>

Result & count

<div class="result result-ok">
  <span class="result-icon" aria-hidden="true">✓</span>
  <p class="result-title">Paid</p>
  <div class="result-actions"><button>Receipt</button></div>
</div>

Inbox <span class="count">12</span>

Menu

Required: .menu wrapping a trigger (<button>) and .menu-panel. Items are .menu-item (use links or buttons). Optional .menu-r, .menu-sep, .menu-danger.

Add .open on .menu to show the panel. Toggle it from a click handler; close it on outside click if you want that.

<div class="menu" id="actions">
  <button type="button" aria-haspopup="menu" onclick="this.parentElement.classList.toggle('open')">Actions</button>
  <div class="menu-panel" role="menu">
    <a class="menu-item" href="#" role="menuitem">Edit</a>
    <hr class="menu-sep">
    <button type="button" class="menu-item menu-danger" role="menuitem">Delete</button>
  </div>
</div>

Drawer & sheet

Add .open on .drawer or .sheet. Optional .drawer-r docks the drawer to the right.

| Class | Where | |---|---| | .drawer / .sheet | overlay root | | .drawer-overlay / .sheet-overlay | dismiss layer | | .drawer-panel / .sheet-panel | sliding panel | | .drawer-r | on .drawer to dock right | | .sheet-handle | grabber bar inside the sheet |

<button type="button" onclick="document.getElementById('d1').classList.add('open')">Open drawer</button>

<div id="d1" class="drawer">
  <div class="drawer-overlay" onclick="this.parentElement.classList.remove('open')"></div>
  <aside class="drawer-panel">…</aside>
</div>

Popover

Required: .pop wrapping a trigger + .pop-panel. Add .open to show the panel. Direction: .pop-t .pop-l .pop-r.

<div class="pop" id="more">
  <button type="button" onclick="this.parentElement.classList.toggle('open')">More</button>
  <div class="pop-panel">Extra content</div>
</div>

Breadcrumbs, steps

<nav class="crumbs">
  <a href="#">Home</a>
  <a href="#">Docs</a>
  <span class="current">Here</span>
</nav>

<ol class="steps">
  <li class="step done"><span class="step-dot"></span><span class="step-label">Account</span></li>
  <li class="step current"><span class="step-dot"></span><span class="step-label">Plan</span></li>
  <li class="step"><span class="step-dot"></span><span class="step-label">Pay</span></li>
</ol>

Toggle .done / .current / .selected / .disabled from JS as the user moves. Add .steps-v for a vertical stepper.


Sidenav

Required: .sidenav + .sidenav-item. Active: .selected.

<nav class="sidenav">
  <a class="sidenav-item selected" href="#">Home</a>
  <a class="sidenav-item" href="#">Docs</a>
</nav>

Bottom nav

Required: .bottom-nav + .bnav-item. Active: .selected. Pin with .fixed on the bar.

<nav class="bottom-nav" aria-label="Primary">
  <a class="bnav-item selected" href="#">Home</a>
  <a class="bnav-item" href="#">Inbox</a>
</nav>

Subnav, TOC, footer, FAB

<nav class="subnav">
  <a class="selected" href="#">Overview</a>
  <a href="#">Members</a>
</nav>

<nav class="toc">
  <a class="selected" href="#intro">Intro</a>
  <a href="#install">Install</a>
</nav>

<footer class="footer">© 2026 Mates CSS</footer>
<button class="fab" aria-label="Create">+</button>

Pin the FAB with .fixed.


Lists & timeline

<div class="list">
  <a class="list-item selected" href="#">Inbox <span class="list-meta">12</span></a>
  <a class="list-item" href="#">Sent</a>
</div>

<div class="timeline">
  <div class="tl-item">
    <i class="tl-dot"></i>
    <div class="tl-content">Shipped v0.5</div>
  </div>
</div>

Stats & definition list

<div class="stat">
  <span class="stat-label">Revenue</span>
  <span class="stat-val">$24k</span>
  <span class="stat-delta up">+12%</span>
</div>

<dl>
  <dt>Plan</dt><dd>Pro</dd>
</dl>

<dl class="props">
  <dt>Plan</dt><dd>Pro</dd>
</dl>

Code & prose

Bare <code> and <pre>. Use .code / .code-block on a non-native host. .prose is an optional reading-width wrapper.

<code>import "mates-css"</code>
<pre>npm install mates-css</pre>
<article class="prose">…</article>
<span class="kbd-row"><kbd>⌘</kbd><kbd>K</kbd></span>

Messages, quote, figure

<div class="msgs">
  <div class="msg">Hi<span class="msg-meta">Ada</span></div>
  <div class="msg msg-out">Hello</div>
</div>

<figure class="quote">
  <blockquote>Paste it into any stack.</blockquote>
  <figcaption>Sean Freeman</figcaption>
</figure>

<figure class="fig">
  <img src="shot.jpg" alt="">
  <figcaption>Product shot</figcaption>
</figure>

<a class="link-card" href="#">
  <span class="link-card-img"></span>
  <span class="link-card-body"><b>Title</b><span>example.com</span></span>
</a>

<ul class="tree">
  <li>
    <details open>
      <summary>src</summary>
      <ul><li>index.css</li></ul>
    </details>
  </li>
</ul>

Banner: .banner (optional .banner-warn / .banner-danger). Toast stack: .toasts (corner .toasts-tl/tr/bl) wrapping .toast children.


Shell & layout blocks

| Class | Role | |---|---| | .shell | app grid: .shell-top + .shell-side + .shell-main | | .collapsed | on .shell to shrink the side (--m-side) | | .sticky-t / .sticky-b | sticky top/bottom | | .split + .split-a / .split-b | split panes; width via --split | | .aspect + .aspect-1x1/4x3/16x9 | aspect boxes |

<div class="shell">
  <header class="shell-top">…</header>
  <aside class="shell-side">…</aside>
  <main class="shell-main">…</main>
</div>

<div class="split" style="--split: 35%">
  <div class="split-a">A</div>
  <div class="split-b">B</div>
</div>

Layout (flex & grid)

Quick patterns

<div class="m-flex m-items-center m-justify-between m-gap-sm">…</div>

<div class="m-grid m-grid-cols-3 m-gap">…</div>
<div class="m-grid m-grid-cols-auto">…</div>

<div class="m-row m-g-sm">
  <div>equal</div>
  <div>equal</div>
  <div>equal</div>
</div>

<div class="m-row">
  <div class="c-4">⅓</div>
  <div class="c-8">⅔</div>
</div>

Directional stacks

| Class | Role | |---|---| | .m-row | horizontal flex; children share space | | .m-col | vertical flex stack | | .m-row-nowrap | no wrap | | .m-col-auto | on a child of .m-row — don't grow | | .m-g-0 .m-g-xs .m-g-sm .m-g-lg | set --m-gap for row/col |

Flex

.m-flex .m-inline-flex .m-flex-col .m-flex-row .m-flex-wrap .m-flex-nowrap .m-flex-1 .m-flex-auto .m-flex-none .m-grow .m-shrink-0 .m-order-first .m-order-last

Align: .m-items-* / short .m-i-start|center|end|stretch|baseline · .m-self-* · .m-justify-* / short .m-j-start|center|end|between|around|evenly

Grid

.m-grid + .m-grid-cols-2|3|4 · .m-grid-cols-auto / .m-grid-auto · .m-grid-cols-auto-sm|auto-lg · .m-grid-rows-2|3 · .m-grid-flow-dense · .m-col-span-2|3|full · .m-row-span-2

Multi-column grids collapse to one column under 640px.

Gaps

Named: .m-gap-0|xs|sm .m-gap .m-gap-lg|xl
Numeric px: .m-gap-5|10|15|20|30|40
Directional: .m-gap-x-* / .m-gap-y-* (same scales)

Width / container helpers

.container · .c-1.c-12 · .divider · .center · .muted · .subtle · .overline · .mono · .hidden

Surface: .surface .elev-1/2/3 .border .radius-sm .radius .radius-full
Text: .truncate .clamp-2/3 .text-sm/lg .fw-500/700 .tabular
Position: .rel .abs .fixed .inset-0 .z-10/20/50 .sticky-t/b
A11y: .sr-only .skip-link
State: .is-disabled .is-loading (also .open / .selected / .collapsed / .active on components)
Print: .print-only .no-print

Spacing (margin / padding)

m-* / p-* with 0|5|10|15|20|30|40 and sides t|b|l|r|x|y, e.g. .m-b-10, .p-x-5, .m-x-auto.

Full class → CSS tables: docs.html#utilities and demo.html#utilities.


Accessibility

  • Focus rings are keyboard-only (:focus-visible) on browsers that support it. Safari 15 falls back to a ring on :focus (including click).
  • Custom checkboxes / radios / switches are real inputs
  • Contrast-checked: white on #0172ad is 5.2:1; dark-mode fills 6–11:1

Browsers

Floor is about five years back: Safari 15, Chrome 94, Firefox 92 (late 2021).

| Feature | How we handle old engines | |---|---| | :has() | Optional enhancement. Use .choice on checkbox/radio labels, .selected on chips/seg items | | color-mix() | Replaced with tokens (--m-primary-soft-2, --m-alert-*-bg, …) | | translate / rotate properties | transform: … | | :focus-visible | @supports — Safari 15 uses :focus | | 100dvh | 100vh then 100dvh | | @property (chart easing) | Ignored if missing; --v still applies, values jump instead of easing |

When you rebrand --m-primary, also set --m-primary-soft, --m-primary-soft-2, and --m-primary-soft-3.

Size

| | | |---|---| | Raw | 75.0kb | | Minified | 61.5kb | | Min + gzip | 12.4kb |

Source

Edit files in src/. Entry is src/index.css. Do not hand-edit mates.css or mates.min.css.

npm run build   # mates.css + mates.min.css
npm run watch

Docs

Open docs.html — one-page catalog: install snippets, live component on the left, markup on the right. Playground: playground.html. Agents: llms.txt and AGENTS.md.

Demo

Open demo.html — every component, light and dark.

License

MIT © 2026 Sean Freeman