@orchestr-sh/motif
v0.5.0
Published
Modular CSS utility library by Orchestr. One file per component — import only what you need.
Maintainers
Readme
@orchestr-sh/motif
A thoughtfully designed CSS utility library built for clarity and memorability. Fewer classes, better naming, semantic tokens—import only what you need.
Why Motif?
- Smaller than Tailwind (~15KB vs 50KB+)
- Better naming — semantic scales (xs, sm, md, lg, xl) instead of arbitrary numbers
- Fewer to memorize — rational variants only, no class explosion
- Token-driven — change one variable, theme everything
- Dark mode built-in — one attribute toggle, OS preference fallback
- Responsive variants (optional) — sm:/md:/lg:/xl: prefixes, CDN-safe, no build step
Installation
Copy the @orchestr-sh/ folder into your project, or reference it via a CDN/package.
Usage
Install the package
npm install @orchestr-sh/motifImport everything (quick start)
@import '@orchestr-sh/motif';Or with full path:
@import '@orchestr-sh/motif/index.css';Import selectively (recommended)
Always start with tokens.css. Everything else depends on it.
/* Required */
@import '@orchestr-sh/motif/core/tokens.css';
@import '@orchestr-sh/motif/core/reset.css'; /* optional but recommended */
/* Pick what you need */
@import '@orchestr-sh/motif/components/button.css';
@import '@orchestr-sh/motif/components/card.css';
@import '@orchestr-sh/motif/utilities/layout.css';
@import '@orchestr-sh/motif/utilities/spacing.css';For unprocessed CSS (Vite, Webpack, PostCSS)
If using a bundler with CSS import support, use the full package path:
@import '@orchestr-sh/motif/core/tokens.css';
@import '@orchestr-sh/motif/components/button.css';The bundler resolves @orchestr-sh/motif to node_modules/@orchestr-sh/motif/ automatically.
File structure
@orchestr-sh/motif/
├── index.css ← barrel (imports everything)
│
├── bin/
│ └── motif.js ← CLI: npx motif build --content ... --output ...
│
├── core/
│ ├── tokens.css ← CSS custom properties (⚠ load first) + dark mode
│ ├── reset.css ← normalize & base defaults
│ └── typography.css ← headings, body text, prose
│
├── components/
│ ├── button.css ← .btn, .btn-primary, .btn-sm, etc.
│ ├── card.css ← .card, .card-header, .card-body …
│ ├── badge.css ← .badge and variants
│ ├── input.css ← .input, .select, .checkbox, .toggle …
│ ├── alert.css ← .alert, .alert-info, .alert-success, …
│ ├── modal.css ← dialog modal with .modal-header, .modal-body
│ ├── dropdown.css ← .dropdown with CSS-only :focus-within
│ └── tabs.css ← .tabs with aria-selected toggle
│
├── utilities/
│ ├── layout.css ← flex, grid, container, position (+ semantic gap)
│ ├── spacing.css ← margin, padding (semantic scale)
│ ├── color.css ← bg, border, shadow, opacity
│ └── responsive.css ← optional: sm:/md:/lg:/xl: responsive variants
│
├── extensions/
│ └── EXTENSIONS.md ← how to create custom components & themes
│
├── README.md ← you are here
├── NAMING.md ← naming philosophy & quick lookup
└── package.json ← with bin entry for CLITheming
Override any token in your own :root block after importing tokens.css:
@import '@orchestr-sh/core/tokens.css';
:root {
/* Change the accent color throughout the entire library */
--color-accent: #7c3aed;
--color-accent-hover: #6d28d9;
/* Rounder corners */
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
/* Custom font */
--font-sans: 'Geist', system-ui, sans-serif;
}Dark mode
Dark mode works out of the box — it's built into tokens.css and requires zero changes to your HTML or components. Everything uses semantic tokens, so they automatically invert in dark mode.
Automatic dark mode (OS preference)
The library respects the system preference by default. Users in dark mode OS automatically see the dark palette.
Manual dark mode toggle (JS)
Toggle dark mode explicitly with one line of JavaScript:
// Turn on dark mode
document.documentElement.dataset.theme = 'dark';
// Turn off (use light mode always)
document.documentElement.dataset.theme = 'light';
// Auto (follow OS preference)
delete document.documentElement.dataset.theme;No CSS changes needed — all components respond to the data-theme attribute automatically.
How it works
- All semantic tokens (
--color-bg,--color-text,--color-border, etc.) are redefined in dark mode - All raw palette tokens (
--color-primary-600,--color-neutral-900, etc.) stay the same — dark mode only overrides the semantic aliases - Result: changing the theme at
:rootlevel automatically themes every component, because nothing has hardcoded color values
Design philosophy
TL;DR: Learn the semantic scale (xs/sm/md/lg/xl) once and apply it everywhere. Everything else follows consistent patterns.
Naming conventions that stick
Everything in Motif uses semantic, memorable naming so you spend less time in the docs:
- Spacing —
p-xs,p-sm,p-md,p-lg,p-xl(notp-1,p-2,p-3…)- Map to real design sizes: xs = 0.25rem, sm = 0.5rem, md = 1rem, lg = 1.5rem, xl = 2rem
- Same scale for all: padding, margin, gap — consistency = memorability
- You only learn it once
- Components —
btn-*,card-*,badge-*— what it is- Variants grouped logically: color, size, shape, state
- All variants are optional — use just
btnfor defaults
- Colors — semantic names, not hex
text-default,text-muted,text-subtle(vstext-gray-600)bg-base,bg-subtle,bg-muted(vsbg-gray-50)- Status colors:
success,warning,danger(no arbitrary numbers)
- Layout — utilities describe what they do
flex,grid,center(notd-flex,d-grid)items-center,justify-between(notai-c,jc-sb)gap-md,gap-lg(gap uses the same semantic scale as spacing)
- Interactive states — handled in CSS, not as separate classes
- Buttons, cards, and inputs all have
:hover,:focus,:activebuilt in - No need for
hover:bg-bluevariants
- Buttons, cards, and inputs all have
Philosophy: Fewer, better classes
Motif ships ~200 classes. Tailwind ships 10,000+. We win on:
- Memorability — You learn the semantic scale once (xs, sm, md, lg, xl) and it applies everywhere
- Clarity — No ambiguous names like
top-0(does it mean position or margin-top?) - Size — Minimal footprint, no bloat for unused variants
- Flexibility — Token system means you can theme/customize without touching CSS
Want deep details? See NAMING.md for the complete naming philosophy with lookup tables.
Responsive variants (optional)
Responsive variants are optional — import only if you need them:
/* Add this to your CSS (in addition to the base imports) */
@import '@orchestr-sh/motif/utilities/responsive.css';Then use the sm:, md:, lg:, xl: prefixes in your HTML:
<!-- Mobile: column. Tablet (768px+): row -->
<div class="flex-col md:flex-row gap-sm md:gap-md">
<div>Sidebar</div>
<div>Content</div>
</div>
<!-- Mobile: 1 column. Desktop (1024px+): 3 columns -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-md">
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card">Card 3</div>
</div>
<!-- Hidden on mobile, visible from tablet onward -->
<div class="hidden md:flex">Desktop navigation</div>Breakpoints
| Prefix | Min width | Use case |
|--------|-----------|----------|
| (none) | 0px | Mobile-first (default) |
| sm: | 640px | Small tablets |
| md: | 768px | Tablets and larger |
| lg: | 1024px | Desktops and larger |
| xl: | 1280px | Large screens |
Classes with responsive variants
- Display:
block,flex,grid,hidden,inline-block,inline-flex,inline-grid - Flex:
flex-row,flex-col,flex-wrap,flex-nowrap,items-center,items-start,items-end,justify-center,justify-between,justify-start,justify-end - Grid:
grid-cols-1throughgrid-cols-4,grid-cols-6,grid-cols-12,col-span-full - Gap:
gap-xs,gap-sm,gap-md,gap-lg,gap-xl(and directional:gap-x-*,gap-y-*) - Padding:
p-xs,p-sm,p-md,p-lg,p-xl, and directional:px-*,py-* - Sizing:
w-full,w-auto,h-full,h-auto - Text:
text-sm,text-base,text-lg,text-xl,text-left,text-center,text-right
Note on syntax: The backslash in CSS (.md\:flex) is just CSS syntax. In your HTML, you write it without the backslash: class="md:flex". The backslash only appears in the CSS file, not in HTML.
Common patterns (copy-paste ready)
Here are the layouts and component combos you'll use 80% of the time:
Card grid with spacing
<div class="container container-lg">
<div class="grid grid-cols-3 gap-lg">
<div class="card card-elevated">
<img class="card-media" src="…" alt="…">
<div class="card-body">
<h3 class="card-title">Title</h3>
<p class="card-description">Description.</p>
</div>
</div>
<!-- repeat 2 more cards -->
</div>
</div>Why this works:
containercenters and constrains widthgrid grid-cols-3creates 3-column layoutgap-lgadds breathing room between cardscard-elevatedadds shadow for depth
Adapt it:
- 2 columns? Use
grid-cols-2 - Smaller spacing? Use
gap-smorgap-md - Responsive? Use
grid-auto-md(auto-fit with 280px min per item)
Header with title and action
<div class="flex items-center justify-between p-lg border-b">
<h1 class="text-2xl font-bold">Page title</h1>
<button class="btn btn-primary btn-sm">Add item</button>
</div>Why this works:
flex items-centeraligns title and button verticallyjustify-betweenpushes them to opposite endsp-lggives breathing roomborder-bseparates header from content
Variations:
- Left-aligned (no justify-between):
<div class="flex items-center gap-md p-lg"> - Stacked on mobile? Use
flex-coland adjust padding - Dark header? Add
bg-neutral-900 text-white
Form with labels and validation
<form class="stack gap-lg max-w-md">
<div class="field">
<label class="field-label field-label-required">Email</label>
<div class="input-group">
<span class="input-group-icon-left">📧</span>
<input class="input input-has-icon-left" type="email" placeholder="[email protected]">
</div>
</div>
<div class="field">
<label class="field-label">Message</label>
<textarea class="textarea" placeholder="Your message…"></textarea>
<span class="field-hint">Max 500 characters</span>
</div>
<div class="field">
<label class="field-label">Terms</label>
<div class="control">
<input type="checkbox" class="checkbox" id="agree">
<label class="control-label" for="agree">I agree to the terms</label>
</div>
</div>
<button class="btn btn-primary btn-block">Send</button>
</form>Why this works:
stack(flex column) keeps form verticalgap-lgspaces fields apartmax-w-mdprevents the form from getting too widefieldwrapper groups label, input, and hintinput-grouphandles icons cleanlycontrolkeeps checkbox and label aligned
Error state:
<input class="input input-error" value="…">
<span class="field-error-msg">Email is already registered</span>Sidebar layout (sticky nav, scrolling content)
<div class="flex h-screen">
<aside class="w-64 bg-neutral-50 border-r p-lg overflow-y-auto shrink-0">
<nav class="stack gap-md">
<a href="#" class="text-md font-semibold text-accent">Dashboard</a>
<a href="#" class="text-md">Users</a>
<a href="#" class="text-md">Settings</a>
</nav>
</aside>
<main class="flex-1 overflow-y-auto p-lg">
<!-- page content here -->
</main>
</div>Why this works:
flexmakes sidebar + content side-by-sideh-screenfills viewportw-64fixed width for sidebarshrink-0prevents sidebar from compressingoverflow-y-autolets each section scroll independentlyflex-1makes main expand to fill remaining space
Badge with status
<div class="flex items-center gap-sm">
<span class="badge badge-success badge-dot">Active</span>
<span>Jane Doe</span>
</div>Variations:
<!-- Pending -->
<span class="badge badge-warning">Pending</span>
<!-- Error -->
<span class="badge badge-danger badge-dot">Failed</span>
<!-- Counter (notifications) -->
<div class="badge-wrapper">
<button class="btn btn-ghost btn-icon">🔔</button>
<span class="badge badge-danger badge-counter badge-pos">5</span>
</div>Modal / Dialog overlay
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-modal">
<div class="card card-elevated w-96 p-lg">
<h2 class="card-title mb-md">Confirm action</h2>
<p class="card-description mb-lg">Are you sure?</p>
<div class="flex gap-md justify-end">
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
</div>Why this works:
fixed inset-0fills the viewportbg-black bg-opacity-50dims the backgroundflex items-center justify-centercenters the modalz-modalensures it's on top- Inner
cardis the modal itself
Quick reference
What you need to know on day one:
| What | How | Examples |
|------|-----|----------|
| Spacing | Use semantic scale: xs/sm/md/lg/xl | p-md, mb-lg, gap-sm |
| Colors | Semantic + palette number | text-muted, bg-danger, border-accent |
| Dark mode | Built-in: OS preference + JS toggle | document.documentElement.dataset.theme = 'dark' |
| Responsive | Optional breakpoint prefixes (import responsive.css) | md:flex-row, lg:grid-cols-3, sm:hidden |
| Size | Consistent naming pattern | btn-sm, card-lg, input-lg |
| Layout | Flex/grid utilities | flex, center, stack, grid grid-cols-3 |
| Forms | Field wrapper + input | <div class="field"> + <label> + <input class="input"> |
| Components | Combine: element + variant + size | <button class="btn btn-primary btn-lg"> |
The two most useful patterns:
<!-- Flex row: spread items apart -->
<div class="flex items-center justify-between gap-md">
<span>Title</span>
<button class="btn btn-sm">Action</button>
</div>
<!-- Flex column: stack vertically -->
<form class="stack gap-lg">
<input class="input">
<input class="input">
<button class="btn btn-primary btn-block">Submit</button>
</form>Component examples
Button
<!-- Color variants -->
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-danger">Delete</button>
<button class="btn btn-success">Confirm</button>
<button class="btn btn-ghost">Dismiss</button>
<button class="btn btn-subtle">Info</button>
<button class="btn btn-danger-outline">Remove</button>
<button class="btn btn-link">Learn more</button>
<!-- Sizes (semantic scale: xs/sm/md/lg/xl) -->
<button class="btn btn-primary btn-xs">Extra small</button>
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary btn-lg">Large</button>
<button class="btn btn-primary btn-xl">Extra large</button>
<!-- Shapes -->
<button class="btn btn-primary btn-pill">Rounded pill</button>
<button class="btn btn-primary btn-square">Sharp corners</button>
<!-- Icon buttons with sizes -->
<button class="btn btn-primary btn-icon">→</button>
<button class="btn btn-primary btn-icon btn-sm">→</button>
<button class="btn btn-primary btn-icon btn-lg">→</button>
<!-- Full width -->
<button class="btn btn-primary btn-block">Sign in</button>
<!-- Loading state -->
<button class="btn btn-primary btn-loading">Saving…</button>
<button class="btn btn-secondary btn-loading">Processing…</button>
<!-- Button group -->
<div class="btn-group">
<button class="btn btn-secondary">Left</button>
<button class="btn btn-secondary">Center</button>
<button class="btn btn-secondary">Right</button>
</div>Button variants:
- Color:
btn-primary,btn-secondary,btn-danger,btn-success,btn-ghost,btn-subtle,btn-danger-outline,btn-link - Size:
btn-xs,btn-sm,btn-lg,btn-xl(default: md) - Shape:
btn-pill,btn-square - Icon:
btn-icon(use with size modifiers) - Layout:
btn-block(full width) - State:
btn-loading(spinner animation)
Card
<!-- Basic elevated card -->
<div class="card card-elevated">
<div class="card-body">
<h3 class="card-title">Card title</h3>
<p class="card-description">Description text goes here.</p>
</div>
</div>
<!-- Card with header, media, and footer -->
<div class="card card-elevated card-interactive">
<div class="card-header">
<h3 class="card-title">Header section</h3>
</div>
<img class="card-media card-media-square" src="…" alt="…">
<div class="card-body">
<p class="card-description">Supporting text.</p>
</div>
<div class="card-footer">
<button class="btn btn-primary btn-sm">Action</button>
</div>
</div>
<!-- Flat and outlined variants -->
<div class="card card-flat">…</div>
<div class="card card-outlined">…</div>
<div class="card card-ghost">…</div>
<!-- Card sizes -->
<div class="card card-sm">…</div> <!-- default: md -->
<div class="card card-lg">…</div>
<!-- Horizontal layout (image left, content right) -->
<div class="card card-horizontal">
<img class="card-media" src="…" alt="…">
<div class="card-body">…</div>
</div>
<!-- Status accent borders -->
<div class="card card-accent-top">…</div> <!-- top accent -->
<div class="card card-accent-left">…</div> <!-- left accent (default color) -->
<div class="card card-accent-success">…</div> <!-- green -->
<div class="card card-accent-warning">…</div> <!-- yellow -->
<div class="card card-accent-danger">…</div> <!-- red -->
<!-- Media aspect ratios -->
<img class="card-media"> <!-- 16:9 default -->
<img class="card-media card-media-square"> <!-- 1:1 -->
<img class="card-media card-media-portrait"> <!-- 3:4 -->Card variants:
- Style:
card-elevated(shadow),card-flat(muted bg),card-outlined(border),card-ghost(transparent) - Sections:
card-header,card-body,card-footer,card-media - Layout:
card-horizontal(image + content side by side) - Size:
card-sm,card-lg(default: md) - Status:
card-accent-top,card-accent-left,card-accent-success,card-accent-warning,card-accent-danger - Media:
card-media-square,card-media-portrait(default: 16:9) - Interactive:
card-interactive(hover effects)
Badge
<!-- Color variants -->
<span class="badge badge-primary">Primary</span>
<span class="badge badge-success">Success</span>
<span class="badge badge-warning">Warning</span>
<span class="badge badge-danger">Danger</span>
<span class="badge badge-default">Default</span>
<span class="badge badge-dark">Dark</span>
<span class="badge badge-solid">Solid</span>
<span class="badge badge-outline">Outline</span>
<!-- Shapes and indicators -->
<span class="badge badge-success badge-dot">Active</span> <!-- dot indicator -->
<span class="badge badge-primary badge-square">v2.0</span> <!-- square corners -->
<!-- Sizes -->
<span class="badge badge-primary badge-sm">Small</span>
<span class="badge badge-primary">Medium (default)</span>
<span class="badge badge-primary badge-lg">Large</span>
<!-- Counter badge (for notifications) -->
<span class="badge badge-danger badge-counter">5</span>
<!-- Dismissible badge -->
<span class="badge badge-primary badge-dismiss">
Tag
<button class="badge-dismiss-btn" aria-label="Remove">×</button>
</span>
<!-- Positioned badge (notification dot) -->
<div class="badge-wrapper">
<img src="avatar.jpg" alt="User">
<span class="badge badge-danger badge-counter badge-pos">3</span>
</div>Alert
<!-- Basic alerts with variants -->
<div class="alert alert-info">
<span class="alert-icon">ℹ️</span>
<div class="alert-body">
<div class="alert-title">Note</div>
<p class="alert-description">This is an informational message.</p>
</div>
</div>
<div class="alert alert-success">
<span class="alert-icon">✓</span>
<div class="alert-body">
<div class="alert-title">Success!</div>
<p class="alert-description">Your changes have been saved.</p>
</div>
</div>
<div class="alert alert-warning">
<span class="alert-icon">⚠️</span>
<div class="alert-body">
<div class="alert-title">Warning</div>
<p class="alert-description">This action cannot be undone.</p>
</div>
</div>
<div class="alert alert-danger">
<span class="alert-icon">✕</span>
<div class="alert-body">
<div class="alert-title">Error</div>
<p class="alert-description">Something went wrong. Please try again.</p>
</div>
</div>
<!-- Dismissible alert -->
<div class="alert alert-info alert-dismissible">
<span class="alert-icon">ℹ️</span>
<div class="alert-body">
<div class="alert-title">Dismissible</div>
<p class="alert-description">You can close this alert.</p>
</div>
<button class="alert-dismiss-btn" aria-label="Close">×</button>
</div>Modal
<!-- Modal dialog -->
<dialog class="modal">
<div class="modal-header">
<h2 class="modal-title">Confirm action</h2>
<button class="modal-close" aria-label="Close">×</button>
</div>
<div class="modal-body">
<p>Are you sure you want to proceed? This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-danger">Delete</button>
</div>
</dialog>
<!-- JavaScript to open -->
<script>
document.querySelector('dialog').showModal();
</script>Sizes: modal-sm (20rem), modal-lg (48rem), modal-xl (64rem)
Dropdown
<!-- Dropdown menu -->
<div class="dropdown">
<button class="btn btn-secondary">Menu</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#edit">Edit</a>
<a class="dropdown-item" href="#share">Share</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#delete">Delete</a>
</div>
</div>
<!-- With labels and sections -->
<div class="dropdown">
<button class="btn btn-secondary">Actions</button>
<div class="dropdown-menu">
<div class="dropdown-label">Content</div>
<a class="dropdown-item" href="#edit">Edit</a>
<a class="dropdown-item" href="#duplicate">Duplicate</a>
<div class="dropdown-divider"></div>
<div class="dropdown-label">Danger zone</div>
<a class="dropdown-item" href="#delete">Delete</a>
</div>
</div>Tabs
<!-- Basic tabs -->
<div class="tabs">
<div class="tabs-list" role="tablist">
<button class="tab" role="tab" aria-selected="true">Home</button>
<button class="tab" role="tab" aria-selected="false">Settings</button>
<button class="tab" role="tab" aria-selected="false">Advanced</button>
</div>
<div class="tab-panels">
<div class="tab-panel" role="tabpanel">Home panel content</div>
<div class="tab-panel" role="tabpanel" hidden>Settings panel</div>
<div class="tab-panel" role="tabpanel" hidden>Advanced panel</div>
</div>
</div>
<!-- Pill-style tabs -->
<div class="tabs tabs-pill">
<div class="tabs-list">
<button class="tab" aria-selected="true">All</button>
<button class="tab" aria-selected="false">Active</button>
<button class="tab" aria-selected="false">Archived</button>
</div>
<div class="tab-panels">
<div class="tab-panel">All items...</div>
<div class="tab-panel" hidden>Active items...</div>
<div class="tab-panel" hidden>Archived items...</div>
</div>
</div>JavaScript snippet (basic tab toggle):
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
const index = Array.from(tab.parentElement.children).indexOf(tab);
document.querySelectorAll('.tab').forEach(t => t.setAttribute('aria-selected', 'false'));
document.querySelectorAll('.tab-panel').forEach((p, i) => p.hidden = i !== index);
tab.setAttribute('aria-selected', 'true');
});
});Badge variants:
- Color:
badge-primary,badge-success,badge-warning,badge-danger,badge-default,badge-dark,badge-solid,badge-outline - Shape:
badge-square(default: pill) - Indicator:
badge-dot(colored circle prefix) - Size:
badge-sm,badge-lg(default: md) - Special:
badge-counter(centered count),badge-dismiss(with close button),badge-pos(positioned notification)
Input / Form
<!-- Text input -->
<div class="field">
<label class="field-label field-label-required">Email</label>
<input class="input" type="email" placeholder="[email protected]">
<span class="field-hint">We'll never share your email.</span>
</div>
<!-- Input with icon -->
<div class="field">
<label class="field-label">Search</label>
<div class="input-group">
<span class="input-group-icon-left">🔍</span>
<input class="input input-has-icon-left" type="text" placeholder="Search…">
</div>
</div>
<!-- Input with prefix/suffix addon -->
<div class="field">
<label class="field-label">Price</label>
<div class="input-addon">
<span class="input-addon-prefix">$</span>
<input class="input" type="number" placeholder="0.00">
<span class="input-addon-suffix">.00</span>
</div>
</div>
<!-- Input sizes and states -->
<input class="input input-sm" type="text"> <!-- small -->
<input class="input" type="text"> <!-- default: md -->
<input class="input input-lg" type="text"> <!-- large -->
<input class="input input-error" value="Error"> <!-- error state -->
<input class="input input-success" value="Valid"> <!-- success state -->
<!-- Textarea -->
<div class="field">
<label class="field-label">Message</label>
<textarea class="textarea" placeholder="Your message…"></textarea>
</div>
<!-- Select dropdown -->
<div class="field">
<label class="field-label">Option</label>
<select class="select">
<option>Choose one…</option>
<option>Option A</option>
<option>Option B</option>
</select>
</div>
<!-- Checkbox -->
<div class="control">
<input type="checkbox" class="checkbox" id="agree">
<label class="control-label" for="agree">I agree to terms</label>
</div>
<!-- Radio -->
<div class="control">
<input type="radio" class="radio" name="choice" id="r1">
<label class="control-label" for="r1">Option 1</label>
</div>
<!-- Toggle switch -->
<label class="toggle">
<input type="checkbox">
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</label>Form field structure:
- Container:
field(flex column, gap between elements) - Label:
field-label,field-label-required(adds red asterisk) - Hint:
field-hint(small, muted text below input) - Error:
field-error-msg(small, red text)
Input variants:
- Size:
input-sm,input-lg(default: md) - State:
input-error,input-success - Types:
input(text),textarea,select
Input groups:
- Icon left/right:
input-group+input-group-icon-left/input-group-icon-right+input-has-icon-left/input-has-icon-right - Prefix/suffix:
input-addon+input-addon-prefix/input-addon-suffix
Checkboxes & Radio:
- Inline: Use
controlwrapper withcontrol-labelfor horizontal alignment - Classes:
checkbox,radio(styled with custom appearance)
Toggle switch:
- Structure:
<label class="toggle">wrapper with hidden<input>,toggle-track, andtoggle-thumbspans - Checked: Apply
:checkedon input to animate thumb and change track color
Layout & Typography
<!-- Container with max width -->
<div class="container container-lg">
<!-- content -->
</div>
<!-- Grid layout with semantic gap -->
<div class="grid grid-cols-3 gap-lg">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
<!-- Responsive auto-fit grid -->
<div class="grid grid-auto-md gap-md">
<div class="card">…</div>
<div class="card">…</div>
<div class="card">…</div>
</div>
<!-- Flex utilities with semantic gap -->
<div class="flex items-center justify-between gap-md">
<span class="text-lg font-semibold">Title</span>
<button class="btn btn-primary btn-sm">Add new</button>
</div>
<!-- Typography -->
<h1 class="h1">Heading 1 (text-4xl)</h1>
<h2 class="h2">Heading 2 (text-3xl)</h2>
<h3 class="h3">Heading 3 (text-2xl)</h3>
<p class="text-body">Body text at base size</p>
<p class="text-lead">Lead text slightly larger</p>
<p class="text-small">Small text</p>
<p class="text-caption">Caption text (xs)</p>
<!-- Text sizes (standalone) -->
<span class="text-xs">Extra small</span>
<span class="text-sm">Small</span>
<span class="text-base">Base</span>
<span class="text-md">Medium</span>
<span class="text-lg">Large</span>
<span class="text-xl">X-Large</span>
<span class="text-2xl">2X-Large</span>
<!-- Font weight -->
<span class="font-light">Light</span>
<span class="font-normal">Normal</span>
<span class="font-medium">Medium</span>
<span class="font-semibold">Semibold</span>
<span class="font-bold">Bold</span>
<span class="font-black">Black</span>
<!-- Text color -->
<span class="text-default">Default</span>
<span class="text-muted">Muted</span>
<span class="text-subtle">Subtle</span>
<span class="text-accent">Accent</span>
<span class="text-success">Success</span>
<span class="text-warning">Warning</span>
<span class="text-danger">Danger</span>
<!-- Text alignment & decoration -->
<p class="text-left">Left aligned</p>
<p class="text-center">Centered</p>
<p class="text-right">Right aligned</p>
<span class="underline">Underlined</span>
<span class="line-through">Strikethrough</span>
<!-- Line height / tracking -->
<p class="leading-tight">Tight line height</p>
<p class="leading-normal">Normal line height</p>
<p class="leading-relaxed">Relaxed line height</p>
<span class="tracking-tight">Tight letter spacing</span>
<span class="tracking-normal">Normal spacing</span>
<span class="tracking-wide">Wide spacing</span>
<!-- Text truncation -->
<p class="truncate">Very long text that will be cut off with ellipsis…</p>
<p class="line-clamp-2">Text limited to 2 lines with ellipsis…</p>
<p class="line-clamp-3">Text limited to 3 lines with ellipsis…</p>
<!-- Prose (styled markdown-like content) -->
<div class="prose">
<h1>Heading in prose</h1>
<p>Paragraphs are auto-spaced.</p>
<a href="#">Links are styled</a>
<code>Inline code has bg</code>
<ul>
<li>List items</li>
<li>are indented</li>
</ul>
</div>Spacing utilities:
- Padding:
p-xs,p-sm,p-md,p-lg,p-xl,p-2xl(or per-side:px-*,py-*,pt-*,pb-*,pl-*,pr-*) - Margin:
m-xs,m-sm,m-md,m-lg,m-xl(or per-side:mx-*,my-*,mt-*,mb-*) - Gap (flex/grid):
gap-xs,gap-sm,gap-md,gap-lg,gap-xl(or directional:gap-x-*,gap-y-*) - Space between (fallback):
space-x-*,space-y-*(adds margin to all siblings) - Negative margin:
-mt-xs,-mt-sm,-mt-md,-mx-md(useful for overlapping elements)
Semantic scale mapping:
xs = 0.25rem (4px)
sm = 0.5rem (8px)
md = 1rem (16px)
lg = 1.5rem (24px)
xl = 2rem (32px)Container sizes:
container-sm,container-md,container-lg,container-xl,container-2xl
Grid utilities:
- Columns:
grid-cols-1throughgrid-cols-6,grid-cols-12 - Auto-fit:
grid-auto-sm(200px min),grid-auto-md(280px min),grid-auto-lg(360px min) - Span:
col-span-1throughcol-span-6,col-span-12,col-span-full
Flex utilities:
- Direction:
flex-row,flex-col,flex-row-rev,flex-col-rev - Wrap:
flex-wrap,flex-nowrap,flex-wrap-rev - Grow/shrink:
flex-1,flex-auto,flex-none,grow,grow-0,shrink,shrink-0 - Justify:
justify-start,justify-center,justify-end,justify-between,justify-around,justify-evenly - Align:
items-start,items-center,items-end,items-baseline,items-stretch
Shortcuts:
center— flex + items-center + justify-center (perfect for centering anything)stack— flex column (for vertical layouts, forms, lists)cluster— flex wrap + items-center (for tag-like layouts)
Arbitrary values (CLI)
For edge cases where Motif's semantic scale doesn't fit exactly, use arbitrary values with the Motif CLI:
npm install @orchestr-sh/motif
npx motif build --content "src/**/*.html" --output motif.built.cssThen use arbitrary classes in your HTML:
<!-- Custom padding: 47px -->
<div class="p-[47px]">Custom padding</div>
<!-- Responsive arbitrary: 320px width at md breakpoint -->
<div class="w-full md:w-[320px]">Responsive custom width</div>
<!-- Custom z-index -->
<div class="z-[9999]">Custom stacking</div>
<!-- Custom gap -->
<div class="grid gap-[13px]">Custom gap</div>The CLI:
- Scans your HTML files for
prefix-[value]patterns - Generates CSS rules with escaped selectors
- Outputs:
motif.built.css= base CSS + generated arbitrary rules
Property mapping:
p-[value]→paddingpx/py/pt/pb/pl/pr-[value]→padding-*m-[value]→marginmx/my/mt/mb/ml/mr-[value]→margin-*w-[value]→widthh-[value]→heightgap-[value],gap-x/y-[value]→gap,column-gap,row-gaptext-[value]→font-sizerounded-[value]→border-radiusz-[value]→z-indexopacity-[value]→opacitytop/right/bottom/left-[value]→ position propertiesmax-w/min-w/max-h/min-h-[value]→ sizing properties
Note: Import the generated file after your base Motif CSS:
@import '@orchestr-sh/motif/index.css';
@import './motif.built.css'; /* Generated by CLI */Extensions & theming
Create custom components and themes for Motif without forking. See extensions/EXTENSIONS.md for the token contract and best practices.
Example custom component:
/**
* my-accordion.css
* Depends on: tokens.css
*/
.accordion {
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
}
.accordion-trigger {
padding: var(--space-4);
background: none;
border: none;
cursor: pointer;
color: var(--color-text);
font-weight: var(--font-weight-medium);
}
.accordion-trigger:hover {
background-color: var(--color-bg-muted);
}Import it alongside Motif:
@import '@orchestr-sh/motif/index.css';
@import './my-accordion.css';Your custom component automatically inherits dark mode support and token-based theming because it references semantic tokens only.
Dependency rules
| File | Depends on |
|------|-----------|
| reset.css | tokens.css |
| typography.css | tokens.css |
| button.css | tokens.css |
| card.css | tokens.css |
| badge.css | tokens.css |
| input.css | tokens.css |
| layout.css | tokens.css |
| spacing.css | tokens.css |
| color.css | tokens.css |
All files depend only on tokens.css. There are no cross-component dependencies.
Adding a new component
- Create
@orchestr-sh/components/mycomponent.css - Add
@import './core/tokens.css';at the top (comment only — for docs; the:rootonly needs one declaration) - Use
var(--token-name)throughout — no hardcoded values - Add the import to
index.cssif you want it in the barrel
Build tool setup
Vite
Works out of the box with zero config:
@import '@orchestr-sh/motif/core/tokens.css';
@import '@orchestr-sh/motif/components/button.css';Webpack
Requires css-loader with import: true (default in most setups):
// webpack.config.js
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { import: true } }
]
}Then use imports normally:
@import '@orchestr-sh/motif/core/tokens.css';PostCSS (with postcss-import)
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
// ... other plugins
]
}Then use imports:
@import '@orchestr-sh/motif/core/tokens.css';Plain browser / CDN
Include via <link> tag (no build step required):
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@orchestr-sh/[email protected]/index.css">Or reference individual files:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@orchestr-sh/[email protected]/core/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@orchestr-sh/[email protected]/components/button.css">