Display

Kbd

Shows a key or a key combination, for shortcut hints in menus, tooltips and help text.

Built on
<kbd>
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.3 kB
Dependencies
React
Command menuCommandK
Search filesCommandShiftF
Close panelEsc
Try its options

Loading its options.

Code
import { Kbd, KbdCombo } from "@/components/equip/kbd";

export default function Demo() {
  return (
    <div style={{ display: "grid", gap: 10, width: 200, fontSize: 13 }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
        <span>Command menu</span>
        <KbdCombo separator={null}>
          <Kbd label="Command">⌘</Kbd>
          <Kbd>K</Kbd>
        </KbdCombo>
      </div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
        <span>Search files</span>
        <KbdCombo separator={null}>
          <Kbd label="Command">⌘</Kbd>
          <Kbd label="Shift">⇧</Kbd>
          <Kbd>F</Kbd>
        </KbdCombo>
      </div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
        <span>Close panel</span>
        <Kbd>Esc</Kbd>
      </div>
    </div>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:10px;width:200px;font-size:13px">
  <div style="display:flex;align-items:center;justify-content:space-between">
    <span>Command menu</span>
    <kbd class="eq-kbd-combo" data-size="m">
      <kbd class="eq-kbd">
        <span class="eq-kbd-symbol" aria-hidden="true">⌘</span>
        <span class="eq-kbd-label">Command</span>
      </kbd>
      <kbd class="eq-kbd">K</kbd>
    </kbd>
  </div>
  <div style="display:flex;align-items:center;justify-content:space-between">
    <span>Search files</span>
    <kbd class="eq-kbd-combo" data-size="m">
      <kbd class="eq-kbd">
        <span class="eq-kbd-symbol" aria-hidden="true">⌘</span>
        <span class="eq-kbd-label">Command</span>
      </kbd>
      <kbd class="eq-kbd">
        <span class="eq-kbd-symbol" aria-hidden="true">⇧</span>
        <span class="eq-kbd-label">Shift</span>
      </kbd>
      <kbd class="eq-kbd">F</kbd>
    </kbd>
  </div>
  <div style="display:flex;align-items:center;justify-content:space-between">
    <span>Close panel</span>
    <kbd class="eq-kbd">Esc</kbd>
  </div>
</div>

Examples

Sizes

KTabEnter
KTabEnter
Code
import { Kbd } from "@/components/equip/kbd";

export default function Example() {
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: 16, alignItems: "center" }}>
      <div style={{ display: "flex", gap: 4, alignItems: "center" }}>
        <Kbd size="s">K</Kbd>
        <Kbd size="s">Tab</Kbd>
        <Kbd size="s">Enter</Kbd>
      </div>
      <div style={{ display: "flex", gap: 4, alignItems: "center" }}>
        <Kbd size="m">K</Kbd>
        <Kbd size="m">Tab</Kbd>
        <Kbd size="m">Enter</Kbd>
      </div>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;gap:16px;align-items:center">
  <div style="display:flex;gap:4px;align-items:center">
    <kbd class="eq-kbd" data-size="s">K</kbd>
    <kbd class="eq-kbd" data-size="s">Tab</kbd>
    <kbd class="eq-kbd" data-size="s">Enter</kbd>
  </div>
  <div style="display:flex;gap:4px;align-items:center">
    <kbd class="eq-kbd" data-size="m">K</kbd>
    <kbd class="eq-kbd" data-size="m">Tab</kbd>
    <kbd class="eq-kbd" data-size="m">Enter</kbd>
  </div>
</div>

Combinations

The keys sit inside an outer kbd, which is how HTML marks up a combination; the plus signs are hidden from screen readers.

CtrlShiftPOptionCommandIShiftTab
Code
import { Kbd, KbdCombo } from "@/components/equip/kbd";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 10, justifyItems: "start" }}>
      <KbdCombo>
        <Kbd>Ctrl</Kbd>
        <Kbd>Shift</Kbd>
        <Kbd>P</Kbd>
      </KbdCombo>
      <KbdCombo separator={null}>
        <Kbd label="Option">⌥</Kbd>
        <Kbd label="Command">⌘</Kbd>
        <Kbd>I</Kbd>
      </KbdCombo>
      <KbdCombo size="s">
        <Kbd>Shift</Kbd>
        <Kbd>Tab</Kbd>
      </KbdCombo>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:10px;justify-items:start">
  <kbd class="eq-kbd-combo" data-size="m">
    <kbd class="eq-kbd">Ctrl</kbd>
    <span class="eq-kbd-separator" aria-hidden="true">+</span>
    <kbd class="eq-kbd">Shift</kbd>
    <span class="eq-kbd-separator" aria-hidden="true">+</span>
    <kbd class="eq-kbd">P</kbd>
  </kbd>
  <kbd class="eq-kbd-combo" data-size="m">
    <kbd class="eq-kbd">
      <span class="eq-kbd-symbol" aria-hidden="true">⌥</span>
      <span class="eq-kbd-label">Option</span>
    </kbd>
    <kbd class="eq-kbd">
      <span class="eq-kbd-symbol" aria-hidden="true">⌘</span>
      <span class="eq-kbd-label">Command</span>
    </kbd>
    <kbd class="eq-kbd">I</kbd>
  </kbd>
  <kbd class="eq-kbd-combo" data-size="s">
    <kbd class="eq-kbd">Shift</kbd>
    <span class="eq-kbd-separator" aria-hidden="true">+</span>
    <kbd class="eq-kbd">Tab</kbd>
  </kbd>
</div>

In running text

Use size s inside a sentence: at 18px it stays close to the height of a 13px line of text.

Press CtrlEnter to send the review, or Esc to keep editing the draft.

Code
import { Kbd, KbdCombo } from "@/components/equip/kbd";

export default function Example() {
  return (
    <p style={{ margin: 0, maxWidth: 340, fontSize: 13, lineHeight: 1.6, color: "var(--eq-ink-2)" }}>
      Press{" "}
      <KbdCombo size="s">
        <Kbd>Ctrl</Kbd>
        <Kbd>Enter</Kbd>
      </KbdCombo>{" "}
      to send the review, or <Kbd size="s">Esc</Kbd> to keep editing the draft.
    </p>
  );
}
HTMLworks in any framework, with the part's CSS
<p style="margin:0;max-width:340px;font-size:13px;line-height:1.6;color:var(--eq-ink-2)">Press <kbd class="eq-kbd-combo" data-size="s"><kbd class="eq-kbd">Ctrl</kbd><span class="eq-kbd-separator" aria-hidden="true">+</span><kbd class="eq-kbd">Enter</kbd></kbd> to send the review, or <kbd class="eq-kbd" data-size="s">Esc</kbd> to keep editing the draft.</p>

Props

PropTypeDefaultWhat it does
size"s" | "m""m"18px for inside a line of text, 22px beside a label. Leave unset inside a combo to follow it.
labelstringnoneSpoken name for a symbol key, such as Command for ⌘. The symbol is then hidden from assistive tech.
...restComponentProps<"kbd">noneEvery native kbd attribute.
KbdCombo size"s" | "m""m"Size for every key inside it.
KbdCombo separatorReactNode"+"Drawn between keys and hidden from assistive tech, so use a symbol, not a word. Pass null for none, as macOS menus do.
KbdCombo ...restComponentProps<"kbd">noneEvery native kbd attribute.

Anatomy

key.eq-kbdA kbd element. Square for one character, wider for names.
symbol.eq-kbd-symbolThe visible glyph, hidden from assistive tech, only with label.
label.eq-kbd-labelVisually hidden spoken name, only with label.
combo.eq-kbd-comboAn outer kbd around the keys, the HTML way to mark up a combination.
separator.eq-kbd-separator

Notes

  • Kbd only shows a shortcut; it does not bind one. Wire the key handling where the action lives.
  • Screen readers may read ⌘, ⌥ and ⇧ as odd symbol names, so give symbol keys a label such as Command, Option or Shift.
  • Show the modifier for the reader's platform: ⌘ on macOS, Ctrl elsewhere.
  • If a control has a shortcut, also put it in aria-keyshortcuts on that control so assistive tech can announce it.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/kbd.tsx50 lines
import { Children, Fragment, type ComponentProps, type ReactNode } from "react";
import "./kbd.css";

export type KbdProps = ComponentProps<"kbd"> & {
  /** Leave unset inside KbdCombo to follow the combo. */
  size?: "s" | "m";
  /** Spoken name for a symbol key, e.g. "Command" for ⌘. The symbol is then hidden from assistive tech. */
  label?: string;
};

export function Kbd({ size, label, className, children, ...rest }: KbdProps) {
  return (
    <kbd {...rest} className={["eq-kbd", className].filter(Boolean).join(" ")} data-size={size}>
      {label ? (
        <>
          <span className="eq-kbd-symbol" aria-hidden="true">
            {children}
          </span>
          <span className="eq-kbd-label">{label}</span>
        </>
      ) : (
        children
      )}
    </kbd>
  );
}

export type KbdComboProps = ComponentProps<"kbd"> & {
  size?: "s" | "m";
  /** Drawn between keys and hidden from assistive tech. Pass null for none. */
  separator?: ReactNode;
};

export function KbdCombo({ size = "m", separator = "+", className, children, ...rest }: KbdComboProps) {
  const keys = Children.toArray(children);
  return (
    <kbd {...rest} className={["eq-kbd-combo", className].filter(Boolean).join(" ")} data-size={size}>
      {keys.map((key, i) => (
        <Fragment key={i}>
          {i > 0 && separator != null && (
            <span className="eq-kbd-separator" aria-hidden="true">
              {separator}
            </span>
          )}
          {key}
        </Fragment>
      ))}
    </kbd>
  );
}
components/equip/kbd.css62 lines
@layer equip {
  /* Size travels as inherited variables, so a KbdCombo can size the keys inside it.
     s sits inside a line of 13px text; m sits beside a menu item or button label. */
  :is(.eq-kbd, .eq-kbd-combo)[data-size="s"] {
    --_kbd-h: 18px;
    --_kbd-pad: calc(var(--eq-space-1) - 1px);
    --_kbd-text: var(--eq-text-xs);
  }

  :is(.eq-kbd, .eq-kbd-combo)[data-size="m"] {
    --_kbd-h: 22px;
    --_kbd-pad: var(--eq-space-1);
    --_kbd-text: var(--eq-text-s);
  }

  .eq-kbd {
    box-sizing: border-box;
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    /* A single key is square; longer names like Shift grow from there. */
    min-width: var(--_kbd-h, 22px);
    height: var(--_kbd-h, 22px);
    padding-inline: var(--_kbd-pad, var(--eq-space-1));
    border: 1px solid var(--eq-line-strong);
    /* The heavier lower edge is the side of the keycap, which lifts the face slightly. */
    border-bottom-width: 2px;
    border-radius: var(--eq-radius-s);
    background: var(--eq-raised);
    color: var(--eq-ink);
    font: var(--eq-weight) var(--_kbd-text, var(--eq-text-s)) / 1 var(--eq-font-mono);
    font-variation-settings: var(--eq-font-mono-settings);
    letter-spacing: 0;
    white-space: nowrap;
  }

  .eq-kbd-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .eq-kbd-combo {
    display: inline-flex;
    align-items: center;
    gap: var(--eq-space-1);
    color: var(--eq-ink-3);
    font: var(--eq-weight) var(--_kbd-text, var(--eq-text-s)) / 1 var(--eq-font);
    white-space: nowrap;
  }

  .eq-kbd-separator {
    flex: none;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index