Skip to content

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
  1. First step
  2. Second step
  3. 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 build
Markup
<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

Placeholder
Images get the base radius and never overflow their container. This one is an inline SVG data URI — no request leaves the page.
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

LayerJobSize
tokensEvery visual decision~30 properties
baseSemantic HTML, styled0 classes
layoutBands and structure~10 classes
componentsThin, 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

Options

60%

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.