General
The paper model — a sheet, a grid of pieces, and zones that exist only when their element does.
dist/layouts/general.css→Blog
Centered, reading-optimized layout. Optional sticky table of contents and typography boost.
dist/layouts/general.css→Dashboard
The app-frame focus layer — link it after General: fixed rail + scrolling content pane via .layout__page--app.
general.css + dashboard.css→Store
A paper recipe, not a separate file: announce strip + sticky filter aside + .shop__grid product cards.
general.css + product-card.css→How Layouts Work
Every layout follows the same pattern:
1. Link the layout CSS after exsa.css (and a theme, if you use one — themes are optional).
2. Add the mode class to the sheet — .layout__page--app for the dashboard focus layer (link dashboard.css after general.css), .layout__grid--centered for blog.
3. Structure your HTML with the documented zone elements.
Layouts use the topbar component's --topbar-height variable, so body padding automatically matches your topbar size (topbar--sm, topbar--xl, etc.).
.layout__page) carries the page's look, a grid
(.layout__grid) cuts it into pieces, and components fill
the pieces. Stacking keys on the sheet's own width — correct even
when embedded.
Zones & Modifiers
| Class | Effect |
|---|---|
| .layout__page | The sheet — width, look, and responsive context (set --layout-page-w for e.g. 80%) |
| .layout__grid | The pieces container — all zones below are optional |
| .layout__header / .layout__footer | Full-width bands above / below the pieces |
| .layout__aside-start / --end | Side pieces (logical — RTL-safe) |
| .layout__content | The main piece — takes all leftover space |
| .layout__hero | Full-width hero band above the pieces (pair with hero.css) |
| .layout__topbar | In-page sticky topbar (pair with the .topbar component) |
| .layout__aside-start--sticky / --end--sticky | Side piece sticks while scrolling (auto-clears the topbar) |
| .layout__grid--centered | Content piece reads --layout-measure |
| .layout__page--auto | Sheet sizes to content (sticky footer off) |
| .layout__page--frame | Outside topbar + footer adopt the sheet's width |
| .sidebar--collapsed on a side piece | Icon-width rail — the track auto-matches its 56px width (--sidebar-collapsed-w); sub-menus open in a tray |
CSS Tokens
| Token | Default | Description |
|---|---|---|
| --layout-page-w | 100% | Sheet width (80% centers it on the table) |
| --layout-page-min-h | 100vh/dvh | Sheet height — footer pinned to the bottom |
| --layout-page-bg / --border / --radius / --shadow | surface tokens | The sheet's paper look |
| --layout-page-pad | 20px | Sheet inner padding |
| --layout-aside-start-w / --layout-aside-end-w | clamp(220px, 25%, 300px) | Side piece widths |
| --layout-gap | 16px | Spacing between pieces |
| --layout-hero-min-h | 55vh | Hero band height |
| --layout-measure | --width-content | Centered content width |
| --layout-sticky-top | auto | Sticky offset — syncs to the topbar height |
Zone Diagram
.layout__aside-start
(clamp width, sticky opt-in)
.layout__content
(1fr, takes the rest)
.layout__aside-end
(clamp width, sticky opt-in)
Starter HTML
Show starter template
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="exsa/dist/exsa.css">
<link rel="stylesheet" href="exsa/dist/themes/breeze.css">
<link rel="stylesheet" href="exsa/dist/components/topbar.css">
<link rel="stylesheet" href="exsa/dist/components/sidebar.css">
<link rel="stylesheet" href="exsa/dist/components/footer.css">
<link rel="stylesheet" href="exsa/dist/layouts/general.css">
</head>
<body class="exsa has-topbar">
<!-- Topbar — outside the sheet, full-bleed -->
<header class="topbar topbar--xl">
<div class="topbar__left">...</div>
<div class="topbar__right">...</div>
</header>
<!-- The sheet + its pieces -->
<div class="layout__page">
<div class="layout__grid">
<aside class="sidebar layout__aside-start layout__aside-start--sticky">
<ul class="sidebar__nav">...</ul>
</aside>
<main class="layout__content">
<!-- Your page content -->
</main>
</div>
<footer class="footer layout__footer">...</footer>
</div>
</body>
</html>
dist/layouts/general.css (+ dist/components/sidebar.css for a styled TOC).
Classes & Tokens
| Class | Effect |
|---|---|
| .layout__grid--centered | Single centered reading column (width = --layout-measure) |
| --layout-measure | Reading width override (default 1080px; 720px is the classic blog measure) |
| .layout__aside-end | End piece for the table of contents |
| .layout__aside-end--sticky | TOC sticks on scroll (stacks at ≤860px) |
Topbar/hero/footer: the shared .topbar + has-topbar, .layout__hero, and .layout__footer pieces. | |
CSS Tokens
| Token | Default | Description |
|---|---|---|
| --layout-measure | --width-content (1080px) | Reading column width for --grid--centered |
Starter HTML
Show starter template
<link rel="stylesheet" href="exsa/dist/layouts/general.css">
<link rel="stylesheet" href="exsa/dist/components/sidebar.css">
<body class="exsa has-topbar" style="--layout-measure:720px">
<header class="topbar">...</header>
<div class="layout__page">
<div class="layout__grid layout__grid--centered">
<main class="layout__content">
<article>
<h1>My Blog Post</h1>
<p>Article content...</p>
</article>
</main>
<aside class="layout__aside-end layout__aside-end--sticky">
<ul class="sidebar__nav">
<li><a class="sidebar__link" href="#">Introduction</a></li>
<li><a class="sidebar__link" href="#">Section 1</a></li>
</ul>
</aside>
</div>
</div>
<footer class="footer layout__footer">...</footer>
</body>
.layout__page--app (in dist/layouts/dashboard.css) turns the
sheet into a full-viewport app frame — no body scroll, a fixed full-height rail, and a scrolling
content pane. Link it after dist/layouts/general.css — same cascade
layer, so an app frame never stacks. No drawer, no auto-collapse, no panel: the layout is only
the frame; anything fancier is your application's concern.
Pieces
| Class | Effect |
|---|---|
| .layout__page--app | Sheet = the viewport (100dvh, no body scroll) |
| .layout__aside-start | Fixed full-height rail (pair with the .sidebar component for its chrome) |
| .layout__content | The scrolling pane (clears the rail) |
| .topbar + has-topbar | Optional outside topbar — rail and pane clear it automatically |
CSS Tokens
| Token | Default | Description |
|---|---|---|
| --layout-aside-start-w | clamp(220px, 25%, 300px) | Rail width |
| --topbar-height | 56px | Topbar clearance (synced automatically) |
Starter HTML
Show starter template
<link rel="stylesheet" href="exsa/dist/layouts/general.css">
<link rel="stylesheet" href="exsa/dist/layouts/dashboard.css">
<link rel="stylesheet" href="exsa/dist/components/sidebar.css">
<body class="exsa has-topbar">
<header class="topbar">
<!-- App topbar — rail and pane clear it automatically -->
</header>
<div class="layout__page layout__page--app">
<div class="layout__grid">
<aside class="sidebar layout__aside-start">
<ul class="sidebar__nav">
<li><a class="sidebar__link" href="#">Dashboard</a></li>
<li><a class="sidebar__link" href="#">Reports</a></li>
</ul>
</aside>
<main class="layout__content">
<!-- The scrolling pane — dash cards, grids, stats (components/dashboard.css) -->
</main>
</div>
</div>
</body>
.shop__grid product cards — all on the
paper model. Link dist/layouts/general.css +
dist/components/product-card.css.
Pieces
| Class | Effect |
|---|---|
| .layout__announce | Fixed full-bleed promo strip — body padding, topbar position, and sticky offsets follow it automatically |
| .topbar + has-topbar | Fixed cart/search topbar, below the announce strip |
| .layout__aside-start--sticky | Filter/category sidebar, sticking below announce + topbar |
| .layout__content | The main piece — hosts the product grid |
| .shop__grid / --compact | Responsive product grid (components/product-card.css) |
| .shop__card | Product tile (components/product-card.css) |
CSS Tokens
| Token | Default | Description |
|---|---|---|
| --layout-announce-h | 40px | Announce strip height |
| --layout-aside-start-w | clamp(220px, 25%, 300px) | Filter sidebar width |
| --layout-page-w | 100% | Sheet width (set on :root/body when the topbar should match) |
| --layout-hero-min-h | 55vh | Promo hero height (via .layout__hero) |
Starter HTML
Show starter template
<link rel="stylesheet" href="exsa/dist/layouts/general.css">
<link rel="stylesheet" href="exsa/dist/components/product-card.css">
<body class="exsa has-topbar">
<div class="layout__announce">🚀 Free shipping on orders over $50</div>
<header class="topbar">
<!-- Cart, search, account links -->
</header>
<div class="layout__page">
<div class="layout__grid">
<aside class="layout__aside-start layout__aside-start--sticky">
<div class="shop__aside-group">
<div class="shop__aside-title">Categories</div>
<a href="#">Clothing</a>
</div>
</aside>
<main class="layout__content">
<div class="shop__grid">
<div class="shop__card">
<img class="shop__card-image" src="product.jpg">
<div class="shop__card-body">
<div class="shop__card-title">Product</div>
<span class="shop__card-price">$24.99</span>
</div>
</div>
</div>
</main>
</div>
<footer class="footer layout__footer">...</footer>
</div>
</body>
bg-page and set
--bg-image; size, position, repeat, parallax, veils, blend modes, and filters are one class each.
Link dist/components/background.css after exsa.css + your theme.
Quick Setup
Copy-paste starter
<link rel="stylesheet" href="exsa/dist/components/background.css">
<!-- Page background: cover + fixed parallax + dark readability veil -->
<!-- Note: use root-relative paths — urls resolve against the CSS that consumes them -->
<body class="exsa bg-page bg-cover bg-fixed bg-veil--dark"
style="--bg-image:url('/img/bg.jpg')">
<!-- Section hero: centered, no repeat, custom overlay -->
<section class="bg-page bg-center bg-no-repeat"
style="--bg-image:url('/img/hero.jpg'); --bg-overlay:rgba(17,139,238,.25); min-height:60vh;">
...
</section>
<!-- Pattern tile: dot-grid, repeat -->
<div class="bg-page bg-pattern">...</div>
<!-- Multiple layers + blend -->
<body class="exsa bg-page bg-blend--multiply"
style="--bg-image:url('/img/photo.jpg'), linear-gradient(var(--color-secondary), var(--color-link))">
</body>
CSS Tokens
| Token | Default | Description |
|---|---|---|
| --bg-image | none | Background image — comma-separate for layers. Use root-relative paths (/img/bg.jpg): urls resolve against the consuming stylesheet |
| --bg-color | var(--color-bg) | Color behind the image (loading fallback) |
| --bg-size | cover | cover | contain | auto | custom |
| --bg-position | center | center | top | bottom | left | right |
| --bg-repeat | no-repeat | no-repeat | repeat | repeat-x | repeat-y |
| --bg-attachment | scroll | scroll | fixed (parallax) | local |
| --bg-overlay | transparent | Veil color/gradient above the image |
| --bg-blend | normal | normal | multiply | screen | overlay | soft-light |
| --bg-filter | none | Image filter — grayscale(1), sepia(.65), blur(4px)… |
Utility Classes
| Class | Effect |
|---|---|
| .bg-page | Activates the background (image + veil pseudo-layers) |
| .bg-cover / .bg-contain / .bg-auto | Image sizing |
| .bg-center / .bg-top / .bg-bottom / .bg-left / .bg-right | Image position |
| .bg-repeat / .bg-repeat-x / .bg-repeat-y / .bg-no-repeat | Image tiling |
| .bg-fixed / .bg-scroll / .bg-local | Parallax / attachment — fixed auto-falls-back under prefers-reduced-motion |
| .bg-veil--dark / .bg-veil--light / .bg-veil--gradient / .bg-veil--color | Readability overlays above the image |
| .bg-blend--multiply / .bg-blend--screen / .bg-blend--overlay / .bg-blend--soft | Blend the image with the layers beneath it |
| .bg-blur | Backdrop blur for your own panels — reads the active skin's --surface-backdrop, so it blurs only under a frosted skin (glossy/glow) |
| .glass / .glass--strong / .glass--dark | Complete glass panels — translucent bg + light border + shadow; the blur follows the active skin's frost tokens (--surface-backdrop / -strong) |
| .bg-grayscale / .bg-sepia | Filters the image only — content stays untouched |
| .bg-pattern | Tiled SVG dot-grid — override --bg-pattern-image / --bg-pattern-size |
| .bg-none | Remove the background |
general.css alone for sites, stores, and blogs; add dashboard.css after it for app frames.
| If you need... | Use |
|---|---|
| Documentation site with sidebar | general.css |
| Blog, articles, prose | general.css (blog mode) |
| Admin panel, dashboard | general.css + dashboard.css (.layout__page--app app frame) |
| Web store, product catalog | general.css + product-card.css (store recipe) |
| Single-viewport landing page | general.css (100dvh sheet + .layout__hero) |
| Multi-section marketing landing page | general.css + hero.css (.layout__header + .layout__hero + pieces) |
Quick Rules
- One base, one focus. Link
general.cssalways; adddashboard.cssafter it only for app frames. The recipes are classes on the same sheet — they don't fight. - Link order matters. Layout CSS goes after
exsa.css, theme, and component CSS — anddashboard.cssgoes aftergeneral.css. - Topbar height auto-syncs.
general.cssuses--topbar-heightwith:has()— swaptopbar--smfortopbar--xland body padding updates automatically. - Components work in any layout. Sidebar.css, footer.css, card.css — they're layout-agnostic. Use them everywhere.
- Tokens are your knobs. Override
--layout-page-w,--layout-aside-start-w,--layout-gap, etc. in your theme to customize the sheet and its pieces.