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.
<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.
<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.
<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.
<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.
<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.
<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>
Search
Required: .search wrapping the field. Add .open on .search to show .search-panel. The library does not open it on focus — your handler should.
<div class="search" id="search">
<input type="search" placeholder="Search commands…">
<div class="search-panel">
<a class="menu-item" href="#">Open buttons</a>
<a class="menu-item" href="#">Open modal</a>
</div>
</div>
<!-- add .open on .search to show the panel -->
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.
<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.
<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.
| Plan | Price |
|---|---|
| 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.
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>
Banner
Required: .banner. Optional: .banner-warn .banner-danger.
<div class="banner">Scheduled maintenance tonight</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>
Modal
Structure: .modal > .modal-overlay + .modal-box with .modal-head .modal-title .modal-body .modal-foot. Open state: add .open on .modal. Remove it to hide. Mates CSS does not trap focus or lock scroll — do that in your app if you need it.
Modal title
Add .open on .modal.
<button onclick="document.getElementById('m1').classList.add('open')">Open</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 class="btn-close" aria-label="Close"
onclick="this.closest('.modal').classList.remove('open')"></button>
</div>
<div class="modal-body">…</div>
<div class="modal-foot">…</div>
</div>
</div>
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>
Breadcrumbs
.crumbs with links. Mark the last crumb .current.
<nav class="crumbs">
<a href="#">Home</a>
<a href="#">Docs</a>
<span class="current">Button</span>
</nav>
Steps
.steps list of .step. States: .done, .current. Each step: .step-dot + .step-label. Vertical: add .steps-v.
- Account
- Plan
- Pay
- Account
- Plan
- 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>
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.
<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.
Timeline
.timeline of .tl-item with .tl-dot and .tl-content.
Overlays and shell.
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.
<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.
<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.
<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.
<figure class="fig">
<div class="aspect"></div>
<figcaption>16×9 product shot</figcaption>
</figure>
Link card
URL preview: a.link-card with .link-card-img and .link-card-body. Hover menus still use .pop.
<a class="link-card" href="#">
<span class="link-card-img"></span>
<span class="link-card-body"><b>Mates CSS</b><span>mates-js.dev</span></span>
</a>
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"
<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>.
<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).
<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.
<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.
<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
| Class | CSS |
|---|---|
.m-flex | display: flex |
.m-inline-flex | display: inline-flex |
.m-flex-row | flex-direction: row |
.m-flex-col | flex-direction: column |
.m-flex-wrap | flex-wrap: wrap |
.m-flex-nowrap | flex-wrap: nowrap |
.m-flex-1 | flex: 1 1 0%; min-width: 0 |
.m-flex-auto | flex: 1 1 auto |
.m-flex-none | flex: none |
.m-grow | flex-grow: 1 |
.m-shrink-0 | flex-shrink: 0 |
.m-basis-0 | flex-basis: 0 |
.m-order-first | order: -1 |
.m-order-last | order: 99 |
Align & justify
Short aliases: .m-i-* = .m-items-*, .m-j-* = .m-justify-*.
| Class | CSS |
|---|---|
.m-items-start / .m-i-start | align-items: flex-start |
.m-items-center / .m-i-center | align-items: center |
.m-items-end / .m-i-end | align-items: flex-end |
.m-items-stretch / .m-i-stretch | align-items: stretch |
.m-items-baseline / .m-i-baseline | align-items: baseline |
.m-self-start | align-self: flex-start |
.m-self-center | align-self: center |
.m-self-end | align-self: flex-end |
.m-justify-start / .m-j-start | justify-content: flex-start |
.m-justify-center / .m-j-center | justify-content: center |
.m-justify-end / .m-j-end | justify-content: flex-end |
.m-justify-between / .m-j-between | justify-content: space-between |
.m-justify-around / .m-j-around | justify-content: space-around |
.m-justify-evenly / .m-j-evenly | justify-content: space-evenly |
.m-j-center-v | justify-content: center |
.m-content-start | align-content: flex-start |
.m-content-center | align-content: center |
.m-content-between | align-content: space-between |
Grid
.m-grid-cols-2|3|4 collapse to one column under 640px.
| Class | CSS |
|---|---|
.m-grid | display: grid |
.m-inline-grid | display: inline-grid |
.m-grid-cols-2 | grid-template-columns: repeat(2, minmax(0, 1fr)) |
.m-grid-cols-3 | grid-template-columns: repeat(3, minmax(0, 1fr)) |
.m-grid-cols-4 | grid-template-columns: repeat(4, minmax(0, 1fr)) |
.m-grid-cols-auto | grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)) |
.m-grid-cols-auto-sm | repeat(auto-fit, minmax(11rem, 1fr)) |
.m-grid-cols-auto-lg | repeat(auto-fit, minmax(22rem, 1fr)) |
.m-grid-rows-2 | grid-template-rows: repeat(2, minmax(0, 1fr)) |
.m-grid-rows-3 | grid-template-rows: repeat(3, minmax(0, 1fr)) |
.m-grid-flow-dense | grid-auto-flow: row dense |
.m-col-span-2 | grid-column: span 2 |
.m-col-span-3 | grid-column: span 3 |
.m-col-span-full | grid-column: 1 / -1 |
.m-row-span-2 | grid-row: span 2 |
.m-grid-auto | display: grid; columns auto-fit 16rem; gap: var(--m-gap, 1rem) |
Row / column stacks
| Class | CSS |
|---|---|
.m-row | display: flex; flex-wrap: wrap; align-items: center; gap: var(--m-gap, 1rem) — children share space |
.m-col | display: flex; flex-direction: column; gap: var(--m-gap, 1rem) |
.m-row-nowrap | flex-wrap: nowrap |
.m-col-auto | on a child of .m-row: flex: 0 0 auto; width: auto |
.c-1 … .c-12 | flex-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 |
.container | max-width: 72rem; margin-inline: auto; padding-inline: 1.25rem |
Gap
| Class | CSS |
|---|---|
.m-gap-0 | gap: 0 |
.m-gap-xs | gap: .4rem |
.m-gap-sm | gap: .65rem |
.m-gap | gap: 1rem |
.m-gap-lg | gap: 1.75rem |
.m-gap-xl | gap: 2.5rem |
.m-gap-5 .m-gap-10 .m-gap-15 .m-gap-20 .m-gap-30 .m-gap-40 | gap: 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.
| Class | CSS |
|---|---|
.m-0 … .m-40 | margin: 0 … margin: 40px |
.m-t-0 … .m-t-40 | margin-top |
.m-b-0 … .m-b-40 | margin-bottom |
.m-l-0 … .m-l-40 | margin-left |
.m-r-0 … .m-r-40 | margin-right |
.m-x-0 … .m-x-20 | margin-left + margin-right |
.m-x-auto | margin-left: auto; margin-right: auto |
.m-y-0 … .m-y-20 | margin-top + margin-bottom |
Padding
Same numeric scale and side letters as margin.
| Class | CSS |
|---|---|
.p-0 … .p-40 | padding: 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
| Class | CSS |
|---|---|
.m-w-auto | width: auto (also sets --m-input-w) |
.m-w-xs | width: 8rem |
.m-w-sm | width: 12rem |
.m-w-md | width: 18rem |
.m-w-lg | width: 26rem |
.m-w-full | width: 100% |
Surface, type, position
| Class | CSS |
|---|---|
.surface | background: var(--m-surface) |
.elev-1 | box-shadow: var(--m-shadow) |
.elev-2 | box-shadow: var(--m-elev) |
.elev-3 | large elevation shadow |
.border | border: 1px solid var(--m-border) |
.radius-sm | border-radius: var(--m-r-sm) |
.radius | border-radius: var(--m-r) |
.radius-full | border-radius: var(--m-r-full) |
.center | text-align: center |
.muted | color: var(--m-fg-2) |
.subtle | color: var(--m-fg-3) |
.mono | font-family: var(--m-mono) |
.overline | uppercase, tracked, color: var(--m-fg-3) |
.text-sm | font-size: 1rem |
.text-lg | font-size: 1.25rem |
.fw-500 | font-weight: 500 |
.fw-700 | font-weight: 700 |
.tabular | font-variant-numeric: tabular-nums |
.truncate | overflow: hidden; text-overflow: ellipsis; white-space: nowrap |
.clamp-2 | 2-line clamp |
.clamp-3 | 3-line clamp |
.hidden | display: none !important |
.rel | position: relative |
.abs | position: absolute |
.fixed | position: fixed |
.inset-0 | inset: 0 |
.z-10 .z-20 .z-50 | z-index: 10 / 20 / 50 |
.sticky-t | position: sticky; top: 0 |
.sticky-b | position: sticky; bottom: 0 |
.aspect | aspect-ratio: 16 / 9 |
.aspect-1x1 | aspect-ratio: 1 |
.aspect-4x3 | aspect-ratio: 4 / 3 |
.aspect-16x9 | aspect-ratio: 16 / 9 |
.scroll-y | overflow-y: auto |
.no-scrollbar | hides scrollbars |
.overscroll-none | overscroll-behavior: none |
.divider | horizontal rule with optional label |
.sr-only | visually hidden, available to AT |
.skip-link | off-screen until focused |
.is-disabled | opacity: .5; pointer-events: none |
.is-loading | opacity: .7; pointer-events: none |
.print-only | hidden on screen, shown in print |
.no-print | hidden in print |