v0.5.0 · ~12.4kb gzipped · MIT

Mates-CSS

A pure CSS library with no JavaScript.

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