v0.5.0 · ~12.4kb gzipped · MIT

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 pure CSS library with no JavaScript. Style your HTML using the classes defined in this kit. Install mates-css and import it into your app component or root module in a frontend or fullstack application — or use the live CDN links.

Native tags (button, input, table…) are styled with no class. Classes are variants (.btn-primary) or the same look on a non-native host (<a class="btn">). Overlays stay closed until your app adds .open.

Install

Add the stylesheet once at the root of the app. After that, markup is enough — Mates CSS has no runtime.

npm install mates-css
// src/main.tsx — root module
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "mates-css";
import App from "./App";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>
);

CDN (jsDelivr): https://cdn.jsdelivr.net/npm/mates-css@0.5.0/mates.min.css. Unminified: import mates-css/css or mates.css.

Use with AI

Point an agent at the class contract, then let it build against mates-css. That name is the CSS library — not the Mates JS framework. If a widget is missing, the agent should compose native HTML and --m-* tokens — not invent class names or pull in another kit.

Contract: llms.txt · https://unpkg.com/mates-css/llms.txt. Project rules: AGENTS.md.

Build UI with mates-css. 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. use buit in classes whenever possible. Apply these classes to the native HTML elements using framework specific methods.

States

Mates CSS does not open overlays on focus or with a hidden checkbox. Your app toggles classes. This is the full state vocabulary.

Open overlays — add .open on the root: .modal, .menu, .pop, .drawer, .sheet, .search.

Selected — .selected on .tab, .sidenav-item, .list-item, .chip, .seg-item, .pick, .nav a, .subnav a, .toc a, .bnav-item, tr.

Collapsed — .collapsed on .shell shrinks the sidebar.

Active — .active on .drop while dragging files.

Done / current — .done and .current on .step; .current on a breadcrumb.

Disabled — native disabled on controls, or .disabled / .is-disabled on items.

el.classList.add("open");
el.classList.remove("open");
el.classList.toggle("open");
document.getElementById("m1").classList.add("open");
document.getElementById("m1").classList.remove("open");

// menu / popover / search — same .open class
menu.classList.toggle("open");

Tokens

Everything is a --m-* CSS variable on :root. Theme follows the OS (prefers-color-scheme) by default — do not set data-theme unless you need to override. Force a theme with data-theme="dark" or "light" on <html>.

Primary

Override tokens once; buttons, focus rings, and charts follow.

:root {
  --m-primary: #0172ad;
  --m-primary-h: #016091;
  --m-primary-a: #014e75;
  --m-on-primary: #ffffff;
  --m-r: 9px;
  --m-fs: 16px;
  --m-font: "Inter", sans-serif;
}

html[data-theme="dark"] { /* or prefers-color-scheme */ }

Button

Bare <button> and submit/reset inputs are styled with no class. Put .btn on a link. Variants stack on the same element: .btn-primary, .btn-dark, .btn-light, .btn-success, .btn-warning, .btn-danger, .btn-outline, .btn-soft, .btn-ghost, .btn-link. Sizes: .btn-sm .btn-lg .btn-block. Square icon control: .btn-icon. .btn-elevated adds a rest shadow. .btn-spin shows a spinner. .btn-close is the icon-only close control. Wrap siblings in .btn-group.

As a link
<button>Default</button>
<button class="btn-primary">Primary</button>
<button class="btn-soft">Soft</button>
<button class="btn-outline">Outline</button>
<button class="btn-ghost">Ghost</button>
<button class="btn-link">Link</button>
<button disabled>Disabled</button>
<a class="btn" href="#">As a link</a>
<button class="btn-sm btn-spin">Loading</button>
<button class="btn-sm">Small</button>
<button class="btn-lg">Large</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>
<button class="btn-icon btn-ghost" aria-label="More">⋯</button>
<div class="btn-group">
  <button class="btn-primary">Left</button>
  <button class="btn-primary">Middle</button>
  <button class="btn-primary">Right</button>
</div>

Tag

Required: .tag. Optional: .tag-primary .tag-success .tag-warning .tag-danger .tag-soft .tag-soft-success .tag-soft-danger .tag-rounded. Removable: child .tag-x.

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

Input

Bare <input> is styled. Use .input on a custom host. Width defaults to --m-input-w (100%). State classes on the control: .input-success .input-error. Wrap the field in .m-error or .m-valid to tint label, input, and hint together. Width helpers: .m-w-auto .m-w-xs .m-w-sm .m-w-md .m-w-lg .m-w-full.

We'll never share it.
Only letters and numbers.
<label for="email">Email</label>
<input id="email" type="email" placeholder="you@example.com">
<span class="hint">We'll never share it.</span>

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

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

Select

Bare <select> uses the same field chrome as text inputs.

<label for="plan">Plan</label>
<select id="plan">
  <option>Choose one…</option>
  <option>Free</option>
  <option>Pro</option>
</select>

Textarea

Bare <textarea> is styled. Pair with a <label> and optional .hint.

<label for="notes">Notes</label>
<textarea id="notes" placeholder="Write something…"></textarea>

Checkbox

Bare input[type=checkbox] is styled. Wrap with <label class="choice"> so the caption sits in a row (required on Safari 15 / Chrome 94; newer browsers also infer this with :has()). Use .check only on a non-native host.

<label class="choice"><input type="checkbox" checked> Checked</label>
<label class="choice"><input type="checkbox"> Unchecked</label>

Radio

Bare input[type=radio]. Group with the same name. Wrap each option in <label class="choice">.

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

Switch

Required: .switch on a <label> wrapping a real checkbox and an empty <i>. Checked state is the native input — no extra class.

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

Range

Bare input[type=range]. The filled track reads --v (0–100, default 50). Sync it from your input handler if you want a live fill. Track thickness: --m-range-h.

<label for="volume">Volume</label>
<input type="range" id="volume" min="0" max="100" value="60">

Field

Required: .field wrapping label + control + .hint as a vertical stack with even gaps.

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

Fieldset

Bare <fieldset> + native <legend>. Use .fieldset on a non-native host.

Profile
<fieldset>
  <legend>Profile</legend>
  <div class="field">
    <label for="fn">Name</label>
    <input id="fn" type="text" placeholder="Ada">
  </div>
</fieldset>

Form group

Required: .fgroup on the flex row. Prefix/suffix text uses .fgroup-label. Nest the input and button as siblings.

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

Input wrap

Required: .input-wrap. Put .input-ico on leading or trailing slots. Password show/hide: trailing .pass-toggle; your JS flips the input type.

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

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

Chip

Required: .chip. Selected via .selected (works everywhere). Nested checkboxes also light up with :has(:checked) on newer browsers. Sibling markup input:checked + .chip works without JS.

Always on
<label class="chip selected"><input type="checkbox" checked> Design</label>
<label class="chip"><input type="checkbox"> Backend</label>
<span class="chip selected">Always on</span>

Rating

Required: .rate with radios in reverse order (5 → 1) so sibling selectors fill the 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

Required: .seg parent, .seg-item children. Active via .selected (toggle it from your radio handler on older browsers). 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>
  <label class="seg-item"><input type="radio" name="p"> Month</label>
</div>

Dropzone

Required: .drop on a <label>. Add .active from JS on dragover; remove it on leave/drop.

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

Files

Attachment list: .files of .file rows. Name .file-name, size .file-meta. Remove with .btn-close — your app deletes the row.

brief.pdf128 KB
logo.svg4 KB
<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 class="file">
    <span class="file-name">logo.svg</span>
    <span class="file-meta">4 KB</span>
    <button class="btn-close" aria-label="Remove"></button>
  </div>
</div>

Number

.num wraps − / native number / +. The buttons do not change the value until your app calls stepDown / stepUp.

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

OTP

.otp of single-character inputs. Focus-advance is your JS (the demo script shows a pattern).

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

Choice cards

.picks of label.pick wrapping a radio. Title .pick-title, hint .pick-sub. Selected: .selected (required on older browsers). Newer engines also match :has(:checked).

<div class="picks">
  <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>
  <label class="pick">
    <input type="radio" name="plan">
    <span>
      <span class="pick-title">Team</span>
      <span class="pick-sub">$75 / month</span>
    </span>
  </label>
</div>

Card

.card is the shell. Inside: .card-body, .card-title, .card-sub, footer .card-bar, full-bleed .card-img. Optional .card-hover lifts the shadow.

Card title

With subtitle

<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>

Table

Bare <table> is styled. Wrap in .table-wrap for horizontal scroll. On the table: .striped .bordered .sticky .compact. Selected row: .selected on <tr>. Sort hint: .sorted.asc or .sorted.desc on <th>. Use .table on a non-table host.

PlanPrice
Free$0
Pro$20
<div class="table-wrap">
  <table class="striped compact">
    <thead><tr><th class="sorted asc">Plan</th><th>Price</th></tr></thead>
    <tbody>
      <tr><td>Free</td><td>$0</td></tr>
      <tr class="selected"><td>Pro</td><td>$20</td></tr>
    </tbody>
  </table>
</div>

Tabs

Parent .tabs, each control .tab. Active: .selected. Underline style: add .tabs-line on the parent. Vertical: add .tabs-v. Tabs are visual — switch panels in your app.

<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 tabs-v">
  <button class="tab selected">Account</button>
  <button class="tab">Billing</button>
  <button class="tab">Team</button>
</div>

Alert

Required: .alert. Optional: .alert-primary .alert-success .alert-warning .alert-danger. Optional head: .alert-head with .alert-title and .alert-actions.

Neutral alert.

New API

Callout with a title and action.

<div class="alert">Neutral alert.</div>
<div class="alert alert-primary">
  <div class="alert-head">
    <p class="alert-title">New API</p>
    <div class="alert-actions"><button class="btn-sm">Docs</button></div>
  </div>
  <p>Callout with a title and action.</p>
</div>

Toast

Required: .toast. Optional: .toast-primary .toast-success .toast-danger. Title: .toast-title. Stack in a corner with .toasts plus .toasts-tl .toasts-tr .toasts-bl.

Toast

Neutral dark toast.

Success

Saved.

<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>

Skeleton

.skel plus a shape: .skel-text .skel-avatar .skel-card.

<div class="skel skel-avatar"></div>
<div class="skel skel-text"></div>
<div class="skel skel-text" style="width:60%"></div>

Spinner

.spin on an <i>. Large: .spin-lg. Center a block with .spin-block.

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

Empty

.empty with .empty-title and optional .empty-actions.

No projects

Create one to get started.

<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 btn-sm">New</button></div>
</div>

Result

Full-page outcome: .result with optional .result-icon, .result-title, and .result-actions. Variants: .result-ok .result-err.

Paid

Invoice #1042

<div class="result result-ok">
  <span class="result-icon" aria-hidden="true">✓</span>
  <p class="result-title">Paid</p>
  <p class="muted">Invoice #1042</p>
  <div class="result-actions"><button class="btn-sm">Receipt</button></div>
</div>

Tooltip

Required: .tooltip on the trigger plus data-tip="…". Direction on the same element: default top, .tooltip-b bottom, .tooltip-r right, .tooltip-l left. Shown on hover and keyboard focus.

<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>

Popover

Required: .pop wrapping trigger + .pop-panel. Open state: .open on .pop. Direction: .pop-t .pop-l .pop-r.

Add .open on .pop.

<div class="pop" id="more">
  <button onclick="this.parentElement.classList.toggle('open')">Popover</button>
  <div class="pop-panel">Add .open on .pop.</div>
</div>

Drawer

.drawer > .drawer-overlay + .drawer-panel. Open state: .open on .drawer. Dock right with .drawer-r. Width token: --m-drawer-w.

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

Sheet

Bottom sheet: .sheet > .sheet-overlay + .sheet-panel. Optional .sheet-handle. Open state: .open on .sheet.

Bottom sheet

Toggle .open on .sheet.

<div id="s1" class="sheet">
  <div class="sheet-overlay" onclick="this.parentElement.classList.remove('open')"></div>
  <div class="sheet-panel">
    <div class="sheet-handle"></div>
    …
  </div>
</div>

Accordion

Bare <details> + <summary>. Open state is native — no .open class. Use .acc only on a non-details host.

Accordion item one

Expand with native details — no JS.

Accordion item two

Another panel.

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

Steps

.steps list of .step. States: .done, .current. Each step: .step-dot + .step-label. Vertical: add .steps-v.

  1. Account
  2. Plan
  3. Pay
  1. Account
  2. Plan
  3. Pay
<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>
<ol class="steps steps-v">…</ol>

Sidenav

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

<nav class="sidenav">
  <a class="sidenav-item selected" href="#">Home</a>
  <a class="sidenav-item" href="#">Buttons</a>
  <a class="sidenav-item disabled" href="#">Admin</a>
</nav>

Bottom nav

Mobile tab bar: .bottom-nav of .bnav-item. Active: .selected. Pin to the viewport 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="#">Search</a>
  <a class="bnav-item" href="#">Inbox</a>
  <a class="bnav-item" href="#">Me</a>
</nav>

TOC

In-page skip list: .toc of links. Current: .selected.

<nav class="toc">
  <a class="selected" href="#button">Button</a>
  <a href="#input">Input</a>
  <a href="#table">Table</a>
</nav>

FAB

Floating action: .fab. Pin with .fixed (bottom-right). Shown inline here so it does not cover the catalog.

<button class="fab" aria-label="Create">+</button>
<button class="fab fixed" aria-label="Create">+</button>

Avatar

.avatar. Sizes: .avatar-sm .avatar-lg. Stack with .avatars.

SM AL LG AL SM LG
<span class="avatar avatar-sm">SM</span>
<span class="avatar">AL</span>
<span class="avatar avatar-lg">LG</span>
<span class="avatars" aria-label="Members">
  <span class="avatar avatar-sm">AL</span>
  <span class="avatar avatar-sm">SM</span>
  <span class="avatar avatar-sm">LG</span>
</span>

Status

.status + .status-dot. Presence: .online .away .busy .offline on the status or the dot.

Online

Away

Busy

<p class="status online"><i class="status-dot"></i> Online</p>
<p class="status away"><i class="status-dot"></i> Away</p>
<p class="status"><i class="status-dot busy"></i> Busy</p>

Badge

Wrap a control in .badge. Child .badge-dot or .badge-n with data-n="8".

<span class="badge">
  <button class="btn-sm">Inbox</button>
  <i class="badge-n" data-n="8"></i>
</span>

Count

Inline numeric pill: .count. Optional .count-primary .count-danger. Use .badge when the number sits on a control.

Inbox 12 · Alerts 3

Inbox <span class="count">12</span> · Alerts <span class="count count-danger">3</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>

List

.list of .list-item. Meta: .list-meta. States: .selected .disabled.

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

Timeline

.timeline of .tl-item with .tl-dot and .tl-content.

Shipped 0.5

Overlays and shell.

Charts

Bars, donut, spark.

<div class="timeline">
  <div class="tl-item">
    <i class="tl-dot"></i>
    <div class="tl-content"><b>Shipped 0.5</b><p class="muted">Overlays and shell.</p></div>
  </div>
  <div class="tl-item">
    <i class="tl-dot"></i>
    <div class="tl-content"><b>Charts</b><p class="muted">Bars, donut, spark.</p></div>
  </div>
</div>

Stat

.stat with .stat-label, .stat-val, optional .stat-delta.up / .down.

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

Definition list

Bare <dl> / <dt> / <dd>. Use .dl on a custom host.

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

Property grid

Bordered key/value: dl.props with native <dt> / <dd>. Bare <dl> stays the quiet definition list.

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

Messages

Thread: .msgs of .msg. Outgoing: .msg-out. Meta: .msg-meta.

Can you ship today?Ada · 4:02
Yes.You · 4:03
<div class="msgs">
  <div class="msg">Can you ship today?<span class="msg-meta">Ada · 4:02</span></div>
  <div class="msg msg-out">Yes.<span class="msg-meta">You · 4:03</span></div>
</div>

Quote

Testimonial: figure.quote with a native <blockquote> and <figcaption>. Bare blockquote stays the pull-quote.

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

Figure

figure.fig plus <figcaption>. Pair with .aspect when you need a ratio box.

16×9 product shot
<figure class="fig">
  <div class="aspect"></div>
  <figcaption>16×9 product shot</figcaption>
</figure>

Tree

File tree: ul.tree. Folders are native <details> / <summary>. Nested lists indent; files are plain <li>.

  • src
    • patterns
      • tree.css
    • buttons.css
    • forms.css
  • README.md
  • package.json
<ul class="tree">
  <li>
    <details open>
      <summary>src</summary>
      <ul>
        <li>
          <details>
            <summary>patterns</summary>
            <ul><li>tree.css</li></ul>
          </details>
        </li>
        <li>buttons.css</li>
        <li>forms.css</li>
      </ul>
    </details>
  </li>
  <li>README.md</li>
  <li>package.json</li>
</ul>

Code

Bare <code> and <pre>. Keyboard: <kbd> and .kbd-row. Reading width: .prose.

import "mates-css"

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

Bar chart

Pure CSS — no SVG. Required: .chart-bars > .bar. Set --v (0–100) on each bar. Fill is <i>; optional data-v shows a value. Label is <span>. Series color: .s1–.s6. Height: --m-chart-h.

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

Stacked bars

.bar.stacked with --v for total height. Inside <i>, each <u style="--sv: …"> is a share of the stack.

<div class="chart-bars" role="img" aria-label="Stacked Q1">
  <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 class="bar stacked" style="--v: 80">
    <i>
      <u style="--sv: 40"></u>
      <u class="s2" style="--sv: 25"></u>
      <u class="s3" style="--sv: 15"></u>
    </i>
    <span>Q2</span>
  </div>
</div>

Horizontal bars

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

<div class="chart-hbars" role="img" aria-label="Skills">
  <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

.donut with segment percents --v1…--v6. Center label in <b>. Solid pie: add .pie. Legend: .legend with <span><i class="s1"></i>Label</span>.

Organic Paid Referral
<div class="donut" style="--v1: 42; --v2: 28; --v3: 18; --v4: 12">
  <b>1,204</b>
</div>
<div class="legend legend-v">
  <span><i class="s1"></i>Organic</span>
  <span><i class="s2"></i>Paid</span>
  <span><i class="s3"></i>Referral</span>
</div>

Ring

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

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

Sparkline

.spark with a child <i style="--v:…"> per point. Height: --m-spark-h.

<div class="spark" role="img" aria-label="Rising trend">
  <i style="--v:30"></i><i style="--v:55"></i><i style="--v:40"></i>
  <i style="--v:80"></i><i style="--v:62"></i><i class="s2" style="--v:90"></i>
</div>

Shell

App grid: .shell with .shell-top, .shell-side, .shell-main. Collapse the side with .collapsed on .shell (width token --m-side).

App shell

Add .collapsed on .shell.

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

Split

.split with panes .split-a and .split-b. Left width: --split.

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

Flex & grid

Utilities are prefixed m-. Flex: .m-flex .m-flex-col .m-flex-wrap .m-flex-1 plus .m-i-center / .m-j-between. Grid: .m-grid .m-grid-cols-2|3|4 (collapses to one column under 640px). Directional stacks: .m-row / .m-col with .c-1–.c-12. Spacing: .m-b-10, .p-x-5. Truncate: .truncate .clamp-2 .clamp-3. Aspect boxes: .aspect .aspect-1x1 .aspect-4x3 .aspect-16x9.

col
col
col
<div class="m-grid m-grid-cols-3 m-gap-sm">
  <div class="alert">col</div>
  <div class="alert alert-primary">col</div>
  <div class="alert">col</div>
</div>

Utility map

Reusable layout, spacing, and type classes — not components. Left is the class; right is the CSS it sets.

Display & flex

ClassCSS
.m-flexdisplay: flex
.m-inline-flexdisplay: inline-flex
.m-flex-rowflex-direction: row
.m-flex-colflex-direction: column
.m-flex-wrapflex-wrap: wrap
.m-flex-nowrapflex-wrap: nowrap
.m-flex-1flex: 1 1 0%; min-width: 0
.m-flex-autoflex: 1 1 auto
.m-flex-noneflex: none
.m-growflex-grow: 1
.m-shrink-0flex-shrink: 0
.m-basis-0flex-basis: 0
.m-order-firstorder: -1
.m-order-lastorder: 99

Align & justify

Short aliases: .m-i-* = .m-items-*, .m-j-* = .m-justify-*.

ClassCSS
.m-items-start / .m-i-startalign-items: flex-start
.m-items-center / .m-i-centeralign-items: center
.m-items-end / .m-i-endalign-items: flex-end
.m-items-stretch / .m-i-stretchalign-items: stretch
.m-items-baseline / .m-i-baselinealign-items: baseline
.m-self-startalign-self: flex-start
.m-self-centeralign-self: center
.m-self-endalign-self: flex-end
.m-justify-start / .m-j-startjustify-content: flex-start
.m-justify-center / .m-j-centerjustify-content: center
.m-justify-end / .m-j-endjustify-content: flex-end
.m-justify-between / .m-j-betweenjustify-content: space-between
.m-justify-around / .m-j-aroundjustify-content: space-around
.m-justify-evenly / .m-j-evenlyjustify-content: space-evenly
.m-j-center-vjustify-content: center
.m-content-startalign-content: flex-start
.m-content-centeralign-content: center
.m-content-betweenalign-content: space-between

Grid

.m-grid-cols-2|3|4 collapse to one column under 640px.

ClassCSS
.m-griddisplay: grid
.m-inline-griddisplay: inline-grid
.m-grid-cols-2grid-template-columns: repeat(2, minmax(0, 1fr))
.m-grid-cols-3grid-template-columns: repeat(3, minmax(0, 1fr))
.m-grid-cols-4grid-template-columns: repeat(4, minmax(0, 1fr))
.m-grid-cols-autogrid-template-columns: repeat(auto-fit, minmax(16rem, 1fr))
.m-grid-cols-auto-smrepeat(auto-fit, minmax(11rem, 1fr))
.m-grid-cols-auto-lgrepeat(auto-fit, minmax(22rem, 1fr))
.m-grid-rows-2grid-template-rows: repeat(2, minmax(0, 1fr))
.m-grid-rows-3grid-template-rows: repeat(3, minmax(0, 1fr))
.m-grid-flow-densegrid-auto-flow: row dense
.m-col-span-2grid-column: span 2
.m-col-span-3grid-column: span 3
.m-col-span-fullgrid-column: 1 / -1
.m-row-span-2grid-row: span 2
.m-grid-autodisplay: grid; columns auto-fit 16rem; gap: var(--m-gap, 1rem)

Row / column stacks

ClassCSS
.m-rowdisplay: flex; flex-wrap: wrap; align-items: center; gap: var(--m-gap, 1rem) — children share space
.m-coldisplay: flex; flex-direction: column; gap: var(--m-gap, 1rem)
.m-row-nowrapflex-wrap: nowrap
.m-col-autoon a child of .m-row: flex: 0 0 auto; width: auto
.c-1 … .c-12flex-basis / max-width 8.333% … 100% (12-col)
.m-g-0--m-gap: 0
.m-g-xs--m-gap: .4rem
.m-g-sm--m-gap: .65rem
.m-g-lg--m-gap: 1.75rem
.containermax-width: 72rem; margin-inline: auto; padding-inline: 1.25rem

Gap

ClassCSS
.m-gap-0gap: 0
.m-gap-xsgap: .4rem
.m-gap-smgap: .65rem
.m-gapgap: 1rem
.m-gap-lggap: 1.75rem
.m-gap-xlgap: 2.5rem
.m-gap-5 .m-gap-10 .m-gap-15 .m-gap-20 .m-gap-30 .m-gap-40gap: 5px … 40px
.m-gap-x-*column-gap (same named + px scale)
.m-gap-y-*row-gap (same named + px scale)

Margin

Scale: 0 = 0, 5 = 5px, 10 = 10px, 15 = 15px, 20 = 20px, 30 = 30px, 40 = 40px. Sides: t top, b bottom, l left, r right, x left+right, y top+bottom.

ClassCSS
.m-0 … .m-40margin: 0 … margin: 40px
.m-t-0 … .m-t-40margin-top
.m-b-0 … .m-b-40margin-bottom
.m-l-0 … .m-l-40margin-left
.m-r-0 … .m-r-40margin-right
.m-x-0 … .m-x-20margin-left + margin-right
.m-x-automargin-left: auto; margin-right: auto
.m-y-0 … .m-y-20margin-top + margin-bottom

Padding

Same numeric scale and side letters as margin.

ClassCSS
.p-0 … .p-40padding: 0 … padding: 40px
.p-t-*padding-top
.p-b-*padding-bottom
.p-l-*padding-left
.p-r-*padding-right
.p-x-*padding-left + padding-right
.p-y-*padding-top + padding-bottom

Width

ClassCSS
.m-w-autowidth: auto (also sets --m-input-w)
.m-w-xswidth: 8rem
.m-w-smwidth: 12rem
.m-w-mdwidth: 18rem
.m-w-lgwidth: 26rem
.m-w-fullwidth: 100%

Surface, type, position

ClassCSS
.surfacebackground: var(--m-surface)
.elev-1box-shadow: var(--m-shadow)
.elev-2box-shadow: var(--m-elev)
.elev-3large elevation shadow
.borderborder: 1px solid var(--m-border)
.radius-smborder-radius: var(--m-r-sm)
.radiusborder-radius: var(--m-r)
.radius-fullborder-radius: var(--m-r-full)
.centertext-align: center
.mutedcolor: var(--m-fg-2)
.subtlecolor: var(--m-fg-3)
.monofont-family: var(--m-mono)
.overlineuppercase, tracked, color: var(--m-fg-3)
.text-smfont-size: 1rem
.text-lgfont-size: 1.25rem
.fw-500font-weight: 500
.fw-700font-weight: 700
.tabularfont-variant-numeric: tabular-nums
.truncateoverflow: hidden; text-overflow: ellipsis; white-space: nowrap
.clamp-22-line clamp
.clamp-33-line clamp
.hiddendisplay: none !important
.relposition: relative
.absposition: absolute
.fixedposition: fixed
.inset-0inset: 0
.z-10 .z-20 .z-50z-index: 10 / 20 / 50
.sticky-tposition: sticky; top: 0
.sticky-bposition: sticky; bottom: 0
.aspectaspect-ratio: 16 / 9
.aspect-1x1aspect-ratio: 1
.aspect-4x3aspect-ratio: 4 / 3
.aspect-16x9aspect-ratio: 16 / 9
.scroll-yoverflow-y: auto
.no-scrollbarhides scrollbars
.overscroll-noneoverscroll-behavior: none
.dividerhorizontal rule with optional label
.sr-onlyvisually hidden, available to AT
.skip-linkoff-screen until focused
.is-disabledopacity: .5; pointer-events: none
.is-loadingopacity: .7; pointer-events: none
.print-onlyhidden on screen, shown in print
.no-printhidden in print