layout
App shell
The frame of an app — its layout is one CSS variable, so any arrangement of navigation, header and content is data, not code.
UpdatedNative behaviorUses built-in HTML behavior, so you write no script.
Usage
The nav is a [popover] drawer below 48rem of the shell's own width and a docked column above it. Resize the window to see both.
Lessons
This week's schedule and bookings appear here.
<div class="ca-app-shell h-96 w-full rounded-lg border">
<nav id="app-nav" class="ca-app-shell__nav" aria-label="App" popover>
<div class="ca-sidebar">
<div class="ca-sidebar__group">
<p class="ca-sidebar__heading" id="app-school">School</p>
<ul class="ca-sidebar__list" aria-labelledby="app-school">
<li><a class="ca-sidebar__link" href="#lessons" aria-current="page">Lessons</a></li>
<li><a class="ca-sidebar__link" href="#students">Students</a></li>
<li><a class="ca-sidebar__link" href="#cars">Cars</a></li>
</ul>
</div>
</div>
</nav>
<header class="ca-app-shell__header">
<button class="ca-app-shell__toggle" type="button" popovertarget="app-nav" aria-label="Menu">
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16" /></svg>
</button>
<strong>Drive School</strong>
</header>
<main class="ca-app-shell__main p-4">
<h2 class="ca-heading ca-heading--sm">Lessons</h2>
<p class="ca-text">This week's schedule and bookings appear here.</p>
</main>
</div>Examples
Other layouts
--ca-shell-template holds the whole grid template — this mirrored shell is the same markup, one variable different.
Lessons
The nav lives on the end edge — one variable, same markup.
<div class="ca-app-shell h-96 w-full rounded-lg border" style="--ca-shell-template: 'header nav' auto 'main nav' 1fr / 1fr auto">
<nav id="app-nav-end" class="ca-app-shell__nav ms-auto" aria-label="App" popover>
<div class="ca-sidebar">
<div class="ca-sidebar__group">
<p class="ca-sidebar__heading" id="app-school-end">School</p>
<ul class="ca-sidebar__list" aria-labelledby="app-school-end">
<li><a class="ca-sidebar__link" href="#lessons" aria-current="page">Lessons</a></li>
<li><a class="ca-sidebar__link" href="#students">Students</a></li>
<li><a class="ca-sidebar__link" href="#cars">Cars</a></li>
</ul>
</div>
</div>
</nav>
<header class="ca-app-shell__header">
<strong>Drive School</strong>
<button class="ca-app-shell__toggle ms-auto" type="button" popovertarget="app-nav-end" aria-label="Menu">
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16" /></svg>
</button>
</header>
<main class="ca-app-shell__main p-4">
<h2 class="ca-heading ca-heading--sm">Lessons</h2>
<p class="ca-text">The nav lives on the end edge — one variable, same markup.</p>
</main>
</div>Guidance and accessibility
When to use
- Use one app shell as the frame of an application: navigation, a header, and scrollable content.
- Narrow, the nav is a native popover: the toggle opens it, Escape and a click outside close it and focus returns to the button. No script.
Layouts are data
Every part owns a named grid area (nav, header, main), and
--ca-shell-template on the root holds the whole grid-template
shorthand. The default is a full-height nav column at the inline start,
but the same markup rearranges into any layout by setting the variable —
grid areas follow the document direction, so start and end mirror
automatically in RTL:
/* Nav at the inline end — and dock the drawer there too with ms-auto */
.my-shell {
--ca-shell-template:
"header nav" auto
"main nav" 1fr /
1fr auto;
}Extra regions work the same way: name an area in your template and give
your element a plain [grid-area:*] utility — no contract class needed:
/* Three columns plus a status bar, for elements with
[grid-area:aside] and [grid-area:footer] */
.my-shell {
--ca-shell-template:
"nav header aside" auto
"nav main aside" 1fr
"nav footer footer" auto /
auto 1fr auto;
}Collapsing the nav
Collapse is host state — the contract styles it, your app owns it. Set
data-collapsed on the root (any button plus one line of script; persist
it however you like) and the docked nav shrinks to zero width. Add
ca-app-shell--rail to shrink to a 3.5rem icon rail instead; label text
clips away and icons stay. Widths come from --ca-shell-nav-w (16rem)
and --ca-shell-collapsed-w (0, or the rail width).
The toggle is for the drawer only — it hides when the nav is docked, so a collapse button on wide screens is your own element.
Accessibility
- Put the nav column in a
<nav>with anaria-label, such as “App”, so it is listed as a landmark. Keep it early in DOM order — grid areas move the box, not the reading order. - The drawer button needs a name, such as
aria-label="Menu". Browsers tell screen readers whether the drawer is open. - Mark the current page inside the nav with
aria-current="page".
Classes
Generated from the component contract. Parts use __, modifiers use --.
| Class | Kind | Use it for |
|---|---|---|
ca-app-shell | Component | The frame; a size container, so it adapts to its own width, not the window's. |
ca-app-shell__header | Part | The bar above the content. Holds the drawer button and the app-level actions. |
ca-app-shell__nav | Part | The navigation column — a [popover] drawer when narrow, docked in the grid when wide. Put a ca-sidebar inside. |
ca-app-shell__main | Part | The scrollable content region; the page itself never scrolls. Padding is yours — the examples add p-4. |
ca-app-shell__toggle | Part | The button that opens the nav drawer. Hidden when the nav is docked. |
ca-app-shell--rail | Layout | Collapse target: data-collapsed shrinks the nav to an icon-wide rail instead of hiding it. |
Skins
The same markup, dressed by each skin. Structure and behavior stay the same; only the look changes.
Base skin
Neutral grayscale starting point; product skins extend it and change what differs.
Lessons
This week's schedule and bookings appear here.
Calaca skin
The Calaca UI brand: hueso paper and tinta ink, cempasúchil marigold, rosa and jade.
Lessons
This week's schedule and bookings appear here.
Bloque skin
Example skin pushed far from base: square, heavy, offset shadows, larger targets.
Lessons
This week's schedule and bookings appear here.
Pliego skin
Example skin from the press sheet: cool ground, bond-paper sheets, rule-grey hairlines, one red mark.
Lessons
This week's schedule and bookings appear here.
Framework notes
Link the prebuilt stylesheet of your skin next to your brand stylesheet. No build step.
<link rel="stylesheet" href="brand.css" />
<link rel="stylesheet" href="ca-base.css" />In Astro, Vite and other Tailwind projects, import Calaca after Tailwind and your brand in your main stylesheet.
@import "tailwindcss";
@import "./brand.css";
@import "@usecalaca/ui/core.css";
@import "@usecalaca/ui/skins/base.css";Same stylesheet as above; write the classes in className.
<button className="ca-button ca-button--primary ca-button--md" type="button">Save changes</button>