Base
The second cascade layer styles semantic HTML directly — content with zero classes already looks designed. Each group shows the elements rendered, then the exact markup under a "Markup" disclosure.
Headings & prose
The whole type scale is fluid — every step is a clamp() — so text breathes with the viewport with no breakpoint styles.
Heading one
Heading two
Heading three
Heading four
A paragraph with a link, strong
text, emphasis, inline code,
marked text, and small print.
- An unordered list item
- Another item
- A third
- First step
- Second step
- Third step
- Definition term
- The description that goes with it.
- Another term
- Another description.
A blockquote. It reads slightly larger and softer, with an accent rule on the start edge.
A horizontal rule sits above this paragraph.
Markup
<h1>Heading one</h1>
<h2>Heading two</h2>
<h3>Heading three</h3>
<h4>Heading four</h4>
<p>A paragraph with <a href="#">a link</a>, <strong>strong
text</strong>, <em>emphasis</em>, inline <code>code</code>,
<mark>marked text</mark>, and <small>small print</small>.</p>
<ul>
<li>An unordered list item</li>
<li>Another item</li>
<li>A third</li>
</ul>
<ol>
<li>First step</li>
<li>Second step</li>
<li>Third step</li>
</ol>
<dl>
<dt>Definition term</dt>
<dd>The description that goes with it.</dd>
<dt>Another term</dt>
<dd>Another description.</dd>
</dl>
<blockquote>
<p>A blockquote. It reads slightly larger and softer, with an
accent rule on the start edge.</p>
</blockquote>
<hr>
<p>A horizontal rule sits above this paragraph.</p>
Rhythm
A heading that follows content — a paragraph, list, table, code
block, figure, blockquote, or details — automatically gets
--k-space-6 of breathing room above it. No classes.
The paragraph below ends a thought, and the heading after it makes
its own space:
Some content ends here.
A heading that follows it
The gap above that heading came from the base layer, not from a margin utility. This is why keel pages have rhythm without spacing utilities.
Code
Inline code gets a quiet chip. Keyboard input like
Ctrl + S gets the same chip with a heavier
bottom edge. Blocks scroll sideways instead of wrapping.
@layer tokens {
:root {
--k-accent: hsl(231 84% 56%);
--k-radius: 8px;
}
}
Markup
<p>Inline <code>code</code> gets a quiet chip. Keyboard input like
<kbd>Ctrl</kbd> + <kbd>S</kbd> gets the same chip with a heavier
bottom edge. Blocks scroll sideways instead of wrapping.</p>
<pre><code>@layer tokens {
:root {
--k-accent: hsl(231 84% 56%);
--k-radius: 8px;
}
}</code></pre>
Keyboard keys
The bare kbd element renders as a raised key cap — a
mono face, a strong border, and a heavier bottom edge that reads as
depth. No class is needed; .k-kbd is available if you
want the look on another element.
Press ⌘ K to search, Esc to close.
Markup
<p>Press <kbd>⌘</kbd> <kbd>K</kbd> to search,
<kbd>Esc</kbd> to close.</p>
Code block with copy
Wrap a pre in .k-code to place a
.k-code__copy button in the top-right corner. The button
stays hidden until you hover the block or focus something inside it.
keel styles the chrome only — wiring the copy is one line of your own
JavaScript that calls navigator.clipboard.writeText.
npm run buildMarkup
<div class="k-code">
<button type="button" class="k-code__copy">
<span class="k-icon k-icon--copy" aria-hidden="true"></span> Copy
</button>
<pre><code>npm run build</code></pre>
</div>
<script>
document.querySelector('.k-code__copy').addEventListener('click', () =>
navigator.clipboard.writeText('npm run build'));
</script>
Media
Markup
<figure>
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='640' height='320'%3E%3Crect width='640' height='320' fill='%23e4e7f0'/%3E%3Cline x1='0' y1='0' x2='640' y2='320' stroke='%23b8bfd4' stroke-width='2'/%3E%3Cline x1='640' y1='0' x2='0' y2='320' stroke='%23b8bfd4' stroke-width='2'/%3E%3Ctext x='320' y='166' text-anchor='middle' font-family='monospace' font-size='16' fill='%23555f7a'%3E640 x 320%3C/text%3E%3C/svg%3E" alt="Placeholder">
<figcaption>Images get the base radius and never overflow their
container. This one is an inline SVG data URI — no request
leaves the page.</figcaption>
</figure>
Tables
| Layer | Job | Size |
|---|---|---|
| tokens | Every visual decision | ~30 properties |
| base | Semantic HTML, styled | 0 classes |
| layout | Bands and structure | ~10 classes |
| components | Thin, token-fed pieces | ~15 classes |
Markup
<table>
<thead>
<tr><th>Layer</th><th>Job</th><th>Size</th></tr>
</thead>
<tbody>
<tr><td>tokens</td><td>Every visual decision</td><td>~30 properties</td></tr>
<tr><td>base</td><td>Semantic HTML, styled</td><td>0 classes</td></tr>
<tr><td>layout</td><td>Bands and structure</td><td>~10 classes</td></tr>
<tr><td>components</td><td>Thin, token-fed pieces</td><td>~15 classes</td></tr>
</tbody>
</table>
Forms
Markup
<form>
<p>
<label for="demo-name">Name</label>
<input type="text" id="demo-name" name="name" placeholder="Ana Reyes">
</p>
<p>
<label for="demo-zone">Delivery zone</label>
<select id="demo-zone" name="zone">
<option>Metro</option>
<option>Provincial</option>
<option>Pickup</option>
</select>
</p>
<p>
<label for="demo-notes">Notes</label>
<textarea id="demo-notes" name="notes" rows="3"
placeholder="Anything we should know?"></textarea>
</p>
<fieldset>
<legend>Options</legend>
<p>
<label style="display:inline; font-weight:400">
<input type="checkbox" name="gift"> Gift wrap
</label>
</p>
<p>
<label style="display:inline; font-weight:400">
<input type="radio" name="speed" checked> Standard
</label>
<label style="display:inline; font-weight:400">
<input type="radio" name="speed"> Express
</label>
</p>
<p>
<label for="demo-qty">Quantity</label>
<input type="range" id="demo-qty" name="qty" min="1" max="10" value="3">
</p>
<p>
<label for="demo-color">Accent color</label>
<input type="color" id="demo-color" name="color" value="#3d5af1">
</p>
</fieldset>
<p>
<label for="demo-progress">Order progress</label>
<progress id="demo-progress" max="100" value="60">60%</progress>
</p>
</form>
One honest wrinkle: base makes label a
block, which suits text fields. For checkbox and radio rows the
samples above set display:inline on the label —
that's the one inline style on this page.
Disclosure & dialog
A details element, closed by default
It opens and closes with no JavaScript at all — this behavior is built into HTML. keel only styles the surface.
Markup
<details>
<summary>A details element, closed by default</summary>
<p>It opens and closes with no JavaScript at all — this behavior
is built into HTML. keel only styles the surface.</p>
</details>
Dialog
Base styles dialog — surface, radius, shadow, and a
dimmed backdrop. But a dialog needs one line of JavaScript to open,
and keel ships none. That line is yours to write:
<dialog id="confirm">
<h3>Delete draft?</h3>
<p>This can't be undone.</p>
<form method="dialog">
<button>Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
<script>
document.querySelector('#confirm').showModal();
</script>
Once open, <form method="dialog"> closes it
without more script.
The contract: if an element looks wrong with zero classes, that's a keel bug — plain semantic HTML is the base layer's job, not something for you to patch with classes.