20 CSS Kbd Keys01 / 20

Pure CSSMIT licensed

Inline HTML Kbd Tag Basic Styling

The reference treatment the rest of the collection builds on: the kbd element styled as a small cap that sits inside a real paragraph of documentation without disturbing the line it lives on. A hairline border plus a one-pixel bottom edge reads as key depth, and the label is set slightly smaller than the surrounding prose so the cap optically matches the x-height of the text around it.

Published

Live Demo
Try it

The code

<div class="kbd-01">
  <div class="kbd-01__stage">
    <article class="kbd-01__doc">
      <p class="kbd-01__eyebrow">Editor · Keyboard</p>
      <h2 class="kbd-01__h">Saving, undoing, escaping</h2>
      <p class="kbd-01__p">Your work is written to disk the moment you press <kbd class="kbd-01__key">Ctrl</kbd> <span class="kbd-01__plus">+</span> <kbd class="kbd-01__key">S</kbd>, and there is no separate confirm step. If an edit turns out wrong, <kbd class="kbd-01__key">Ctrl</kbd> <span class="kbd-01__plus">+</span> <kbd class="kbd-01__key">Z</kbd> walks back through the history one change at a time; <kbd class="kbd-01__key">Esc</kbd> closes whichever panel is in the way first.</p>
      <p class="kbd-01__p">Every key above is a real <code class="kbd-01__code">&lt;kbd&gt;</code> element sitting in ordinary prose. Notice that the lines stay evenly spaced: the cap has <code class="kbd-01__code">line-height: 1</code>, so it never inflates the line box it lives in.</p>
      <dl class="kbd-01__anatomy">
        <div class="kbd-01__row"><dt class="kbd-01__dt">Border</dt><dd class="kbd-01__dd">One hairline, one tone darker than the cap face</dd></div>
        <div class="kbd-01__row"><dt class="kbd-01__dt">Edge</dt><dd class="kbd-01__dd">A single 1px bottom shadow, no blur</dd></div>
        <div class="kbd-01__row"><dt class="kbd-01__dt">Label</dt><dd class="kbd-01__dd">0.82em of the surrounding text, never below 11px</dd></div>
      </dl>
    </article>
    <div class="kbd-01__theme">
      <input class="kbd-01__themebox" type="checkbox" id="kbd-01-theme">
      <label class="kbd-01__themelabel" for="kbd-01-theme">
        <span class="kbd-01__themedot" aria-hidden="true"></span>
        <span class="kbd-01__themebase">Light</span><span class="kbd-01__themeflip">Dark</span>
      </label>
    </div>
  </div>
</div>
.kbd-01 {
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  --paper: #faf7f1;
  --ink: #221e18;
  --muted: #7c7264;
  --rule: #e2dbcd;
  --cap: #ffffff;
  --cap-border: #cfc7b6;
  --cap-edge: #bdb3a0;
  --cap-ink: #332d24;
  --radius: 5px;
  --key-size: 0.82em;
  --font-key: ui-sans-serif, system-ui, "Helvetica Neue", sans-serif;
  --font-body: Georgia, "Iowan Old Style", "Times New Roman", serif;
  background: var(--paper);
  color: var(--ink);
  padding: 40px 20px;
}

.kbd-01__stage {
  display: grid;
  place-items: center;
  min-height: calc(100vh - 80px);
}

.kbd-01__doc {
  inline-size: 100%;
  max-inline-size: 33rem;
  min-inline-size: 0;
}

.kbd-01__eyebrow {
  margin: 0 0 10px;
  font: 600 10px/1 var(--font-key);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
}

.kbd-01__h {
  margin: 0 0 18px;
  font: 400 clamp(24px, 5vw, 33px)/1.15 var(--font-body);
  letter-spacing: -0.015em;
}

.kbd-01__p {
  margin: 0 0 16px;
  font: 400 clamp(15px, 3.4vw, 17px)/1.75 var(--font-body);
  text-wrap: pretty;
}

.kbd-01__key {
  display: inline-block;
  padding: 0.28em 0.55em;
  min-inline-size: 1.1em;
  font-family: var(--font-key);
  font-size: var(--key-size);
  font-weight: 600;
  line-height: 1;
  text-align: center;
  color: var(--cap-ink);
  background: var(--cap);
  border: 1px solid var(--cap-border);
  border-radius: var(--radius);
  box-shadow: 0 1px 0 var(--cap-edge);
  vertical-align: 0.06em;
  white-space: nowrap;
}

.kbd-01__plus {
  font-family: var(--font-key);
  font-size: 0.78em;
  color: var(--muted);
  padding: 0 0.05em;
}

.kbd-01__code {
  font: 0.86em/1 ui-monospace, "SFMono-Regular", Menlo, monospace;
  background: rgba(124, 114, 100, 0.12);
  padding: 0.2em 0.35em;
  border-radius: 3px;
}

.kbd-01__anatomy {
  margin: 26px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  display: grid;
  gap: 10px;
}

.kbd-01__row {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  gap: 12px;
  align-items: baseline;
}

.kbd-01__dt {
  margin: 0;
  font: 600 10px/1.4 var(--font-key);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  min-inline-size: 0;
}

.kbd-01__dd {
  margin: 0;
  font: 400 14px/1.5 var(--font-body);
  min-inline-size: 0;
}

@media (max-width: 380px) {
  .kbd-01__row {
    grid-template-columns: 1fr;
    gap: 2px;
  }
}

.kbd-01[data-theme="dark"],
[data-theme="dark"] .kbd-01 {
  --paper: #17150f;
  --ink: #f2ede1;
  --muted: #9a9080;
  --rule: #322d23;
  --cap: #2a251c;
  --cap-border: #4a4232;
  --cap-edge: #100e09;
  --cap-ink: #f6f1e5;
}
/* In-demo theme switch. State is one checkbox inside this demo, so the toggle
   themes THIS demo only — no root attribute, no script. */

.kbd-01__theme {
  position: absolute;
  inset-block-start: 14px;
  inset-inline-end: 16px;
  z-index: 5;
  display: flex;
}
/* the pill owns the top-right corner, so the stage reserves that band */

.kbd-01__stage {
  box-sizing: border-box;
  padding-block-start: 46px;
}

.kbd-01__themebox {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  opacity: 0;
  pointer-events: none;
}

.kbd-01__themelabel {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-block-size: 44px;
  padding: 0 16px;
  box-sizing: border-box;
  font-family: var(--font-key, ui-sans-serif, system-ui, sans-serif);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink, var(--cap-ink, currentColor));
  background: transparent;
  border: 1px solid currentColor;
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
}

.kbd-01__themedot {
  inline-size: 12px;
  block-size: 12px;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}

.kbd-01__themebox:focus-visible + .kbd-01__themelabel {
  outline: 3px solid var(--accent, var(--cap-ink, currentColor));
  outline-offset: 2px;
}

@supports (border-color: color-mix(in oklab, currentColor 50%, transparent)) {
  .kbd-01__themelabel {
    border-color: color-mix(in oklab, currentColor 65%, transparent);
  }
}

.kbd-01__themeflip {
  display: none;
}

.kbd-01:has(.kbd-01__themebox:checked) {
  --paper: #17150f;
  --ink: #f2ede1;
  --muted: #9a9080;
  --rule: #322d23;
  --cap: #2a251c;
  --cap-border: #4a4232;
  --cap-edge: #100e09;
  --cap-ink: #f6f1e5;
}

.kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themebase {
  display: none;
}

.kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themeflip {
  display: inline;
}
/* The flip-back rule: on the opposite OS preference the unchecked state is the
   flipped palette, and checking the box returns to the demo's own default. */

@media (prefers-color-scheme: dark) {
  .kbd-01:not(:has(.kbd-01__themebox:checked)) {
    --paper: #17150f;
    --ink: #f2ede1;
    --muted: #9a9080;
    --rule: #322d23;
    --cap: #2a251c;
    --cap-border: #4a4232;
    --cap-edge: #100e09;
    --cap-ink: #f6f1e5;
  }

  .kbd-01:has(.kbd-01__themebox:checked) {
    --paper: #faf7f1;
    --ink: #221e18;
    --muted: #7c7264;
    --rule: #e2dbcd;
    --cap: #ffffff;
    --cap-border: #cfc7b6;
    --cap-edge: #bdb3a0;
    --cap-ink: #332d24;
  }

  .kbd-01:not(:has(.kbd-01__themebox:checked)) .kbd-01__themebase {
    display: none;
  }

  .kbd-01:not(:has(.kbd-01__themebox:checked)) .kbd-01__themeflip {
    display: inline;
  }

  .kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themebase {
    display: inline;
  }

  .kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themeflip {
    display: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Kbd Key from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Inline HTML Kbd Tag Basic Styling
Source: https://codefronts.com/snippets/css-kbd-keys/inline-html-kbd-tag-basic-styling/

The reference treatment the rest of the collection builds on: the kbd element styled as a small cap that sits inside a real paragraph of documentation without disturbing the line it lives on. A hairline border plus a one-pixel bottom edge reads as key depth, and the label is set slightly smaller than the surrounding prose so the cap optically matches the x-height of the text around it.
## HTML
```html
<div class="kbd-01">
  <div class="kbd-01__stage">
    <article class="kbd-01__doc">
      <p class="kbd-01__eyebrow">Editor · Keyboard</p>
      <h2 class="kbd-01__h">Saving, undoing, escaping</h2>
      <p class="kbd-01__p">Your work is written to disk the moment you press <kbd class="kbd-01__key">Ctrl</kbd> <span class="kbd-01__plus">+</span> <kbd class="kbd-01__key">S</kbd>, and there is no separate confirm step. If an edit turns out wrong, <kbd class="kbd-01__key">Ctrl</kbd> <span class="kbd-01__plus">+</span> <kbd class="kbd-01__key">Z</kbd> walks back through the history one change at a time; <kbd class="kbd-01__key">Esc</kbd> closes whichever panel is in the way first.</p>
      <p class="kbd-01__p">Every key above is a real <code class="kbd-01__code">&lt;kbd&gt;</code> element sitting in ordinary prose. Notice that the lines stay evenly spaced: the cap has <code class="kbd-01__code">line-height: 1</code>, so it never inflates the line box it lives in.</p>
      <dl class="kbd-01__anatomy">
        <div class="kbd-01__row"><dt class="kbd-01__dt">Border</dt><dd class="kbd-01__dd">One hairline, one tone darker than the cap face</dd></div>
        <div class="kbd-01__row"><dt class="kbd-01__dt">Edge</dt><dd class="kbd-01__dd">A single 1px bottom shadow, no blur</dd></div>
        <div class="kbd-01__row"><dt class="kbd-01__dt">Label</dt><dd class="kbd-01__dd">0.82em of the surrounding text, never below 11px</dd></div>
      </dl>
    </article>
    <div class="kbd-01__theme">
      <input class="kbd-01__themebox" type="checkbox" id="kbd-01-theme">
      <label class="kbd-01__themelabel" for="kbd-01-theme">
        <span class="kbd-01__themedot" aria-hidden="true"></span>
        <span class="kbd-01__themebase">Light</span><span class="kbd-01__themeflip">Dark</span>
      </label>
    </div>
  </div>
</div>
```
## CSS
```css
.kbd-01 {
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  --paper: #faf7f1;
  --ink: #221e18;
  --muted: #7c7264;
  --rule: #e2dbcd;
  --cap: #ffffff;
  --cap-border: #cfc7b6;
  --cap-edge: #bdb3a0;
  --cap-ink: #332d24;
  --radius: 5px;
  --key-size: 0.82em;
  --font-key: ui-sans-serif, system-ui, "Helvetica Neue", sans-serif;
  --font-body: Georgia, "Iowan Old Style", "Times New Roman", serif;
  background: var(--paper);
  color: var(--ink);
  padding: 40px 20px;
}

.kbd-01__stage {
  display: grid;
  place-items: center;
  min-height: calc(100vh - 80px);
}

.kbd-01__doc {
  inline-size: 100%;
  max-inline-size: 33rem;
  min-inline-size: 0;
}

.kbd-01__eyebrow {
  margin: 0 0 10px;
  font: 600 10px/1 var(--font-key);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
}

.kbd-01__h {
  margin: 0 0 18px;
  font: 400 clamp(24px, 5vw, 33px)/1.15 var(--font-body);
  letter-spacing: -0.015em;
}

.kbd-01__p {
  margin: 0 0 16px;
  font: 400 clamp(15px, 3.4vw, 17px)/1.75 var(--font-body);
  text-wrap: pretty;
}

.kbd-01__key {
  display: inline-block;
  padding: 0.28em 0.55em;
  min-inline-size: 1.1em;
  font-family: var(--font-key);
  font-size: var(--key-size);
  font-weight: 600;
  line-height: 1;
  text-align: center;
  color: var(--cap-ink);
  background: var(--cap);
  border: 1px solid var(--cap-border);
  border-radius: var(--radius);
  box-shadow: 0 1px 0 var(--cap-edge);
  vertical-align: 0.06em;
  white-space: nowrap;
}

.kbd-01__plus {
  font-family: var(--font-key);
  font-size: 0.78em;
  color: var(--muted);
  padding: 0 0.05em;
}

.kbd-01__code {
  font: 0.86em/1 ui-monospace, "SFMono-Regular", Menlo, monospace;
  background: rgba(124, 114, 100, 0.12);
  padding: 0.2em 0.35em;
  border-radius: 3px;
}

.kbd-01__anatomy {
  margin: 26px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  display: grid;
  gap: 10px;
}

.kbd-01__row {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  gap: 12px;
  align-items: baseline;
}

.kbd-01__dt {
  margin: 0;
  font: 600 10px/1.4 var(--font-key);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  min-inline-size: 0;
}

.kbd-01__dd {
  margin: 0;
  font: 400 14px/1.5 var(--font-body);
  min-inline-size: 0;
}

@media (max-width: 380px) {
  .kbd-01__row {
    grid-template-columns: 1fr;
    gap: 2px;
  }
}

.kbd-01[data-theme="dark"],
[data-theme="dark"] .kbd-01 {
  --paper: #17150f;
  --ink: #f2ede1;
  --muted: #9a9080;
  --rule: #322d23;
  --cap: #2a251c;
  --cap-border: #4a4232;
  --cap-edge: #100e09;
  --cap-ink: #f6f1e5;
}
/* In-demo theme switch. State is one checkbox inside this demo, so the toggle
   themes THIS demo only — no root attribute, no script. */

.kbd-01__theme {
  position: absolute;
  inset-block-start: 14px;
  inset-inline-end: 16px;
  z-index: 5;
  display: flex;
}
/* the pill owns the top-right corner, so the stage reserves that band */

.kbd-01__stage {
  box-sizing: border-box;
  padding-block-start: 46px;
}

.kbd-01__themebox {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  opacity: 0;
  pointer-events: none;
}

.kbd-01__themelabel {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-block-size: 44px;
  padding: 0 16px;
  box-sizing: border-box;
  font-family: var(--font-key, ui-sans-serif, system-ui, sans-serif);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink, var(--cap-ink, currentColor));
  background: transparent;
  border: 1px solid currentColor;
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
}

.kbd-01__themedot {
  inline-size: 12px;
  block-size: 12px;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}

.kbd-01__themebox:focus-visible + .kbd-01__themelabel {
  outline: 3px solid var(--accent, var(--cap-ink, currentColor));
  outline-offset: 2px;
}

@supports (border-color: color-mix(in oklab, currentColor 50%, transparent)) {
  .kbd-01__themelabel {
    border-color: color-mix(in oklab, currentColor 65%, transparent);
  }
}

.kbd-01__themeflip {
  display: none;
}

.kbd-01:has(.kbd-01__themebox:checked) {
  --paper: #17150f;
  --ink: #f2ede1;
  --muted: #9a9080;
  --rule: #322d23;
  --cap: #2a251c;
  --cap-border: #4a4232;
  --cap-edge: #100e09;
  --cap-ink: #f6f1e5;
}

.kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themebase {
  display: none;
}

.kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themeflip {
  display: inline;
}
/* The flip-back rule: on the opposite OS preference the unchecked state is the
   flipped palette, and checking the box returns to the demo's own default. */

@media (prefers-color-scheme: dark) {
  .kbd-01:not(:has(.kbd-01__themebox:checked)) {
    --paper: #17150f;
    --ink: #f2ede1;
    --muted: #9a9080;
    --rule: #322d23;
    --cap: #2a251c;
    --cap-border: #4a4232;
    --cap-edge: #100e09;
    --cap-ink: #f6f1e5;
  }

  .kbd-01:has(.kbd-01__themebox:checked) {
    --paper: #faf7f1;
    --ink: #221e18;
    --muted: #7c7264;
    --rule: #e2dbcd;
    --cap: #ffffff;
    --cap-border: #cfc7b6;
    --cap-edge: #bdb3a0;
    --cap-ink: #332d24;
  }

  .kbd-01:not(:has(.kbd-01__themebox:checked)) .kbd-01__themebase {
    display: none;
  }

  .kbd-01:not(:has(.kbd-01__themebox:checked)) .kbd-01__themeflip {
    display: inline;
  }

  .kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themebase {
    display: inline;
  }

  .kbd-01:has(.kbd-01__themebox:checked) .kbd-01__themeflip {
    display: none;
  }
}
```

How this works

A keycap is a typographic problem before it is a visual one. The cap has to sit inside running prose without pushing the line box around it. Two declarations do the work: line-height: 1 on the key, so its own leading never inflates the paragraph's line box, and padding expressed in em, so the cap grows and shrinks with whatever text it is nested in.

Depth comes from one shadow, not three. A single box-shadow: 0 1px 0 in a slightly darker tone than the border reads as the front wall of a key. Anything more — blur, spread, a second layer — starts to look like a floating button rather than a cap embedded in text. The border stays hairline so the key does not out-shout the sentence.

The trap is vertical alignment. Browsers style <kbd> as monospace by default, and monospace faces run at a different x-height than the body serif, so a cap set at the same font-size as its paragraph looks a pixel or two too tall. Setting the label to about 0.82em and nudging the cap with a small negative vertical-align lands it on the baseline properly.

Theme through tokens, never through overrides. Every colour in the demo resolves from four custom properties on the root, so the dark scheme is a four-line block rather than a duplicate of the whole rule set — the pattern all twenty demos share.

Make it yours

  • Change --cap and --cap-edge on the root to retint every key at once.
  • Raise --key-size from 0.82em to 0.9em if your body face has a tall x-height.
  • Swap --font-key for a monospace stack for a more code-like read.
  • Increase the bottom edge from 1px to 2px for a chunkier cap.
  • Drop the edge shadow entirely for the flat treatment shown in demo 04.
  • Set --radius to 3px for a squarer, more technical key.

Gotchas — read before shipping

  • A fixed line-height greater than 1 on the key inflates the paragraph's line box and makes text lines visibly uneven around each cap.
  • Padding in px looks correct in body copy and broken inside a heading; use em so the cap scales with its context.
  • The default <kbd> monospace stack has a different x-height than most body faces, so a cap at 1em reads as oversized — set the label smaller than the prose.
  • Setting display:inline-block without vertical-align lets the cap drift above the baseline in Safari.
  • Hiding the switch's checkbox with display:none takes it out of the tab order, so the toggle stops working for keyboard users — clip it instead.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 105+.

Custom properties, box-shadow and em-based padding have been universally supported for years. The dark scheme needs prefers-color-scheme (Chrome 76+, Safari 12.1+, Firefox 67+) and falls back to the light palette everywhere else. The versions above are set by :has(), which drives the in-demo theme switch; the demo's own key technique works back to Chrome 49, Safari 9.1 and Firefox 31. Without :has() the demo still follows the OS preference and only the switch stops responding.

Techniques used in this demo

Search CodeFronts

Loading…