# Keyboard key

> The <kbd> element drawn as a key.

Page: https://svelte-lean.vercel.app/primitives/kbd · Markdown: https://svelte-lean.vercel.app/primitives/kbd.md · Index: https://svelte-lean.vercel.app/llms.txt

The native kbd element drawn as a key, for shortcuts in running text, on buttons and in menus; combinations nest one key per kbd.

```svelte
<script lang="ts">
	import '@svelte-lean/styles/kbd.css';
</script>

<p>
	Press <kbd data-slean="kbd"><kbd>Ctrl</kbd>+<kbd>K</kbd></kbd> to search and
	<kbd data-slean="kbd">Esc</kbd> to close.
</p>
```

A single key is a kbd with text; a combination is a kbd holding one kbd per key.

## Examples

### Combinations

```svelte
<script lang="ts">
	import '@svelte-lean/styles/kbd.css';
</script>

<!-- One <kbd> per key inside the root; the plus signs stay text. -->
<p><kbd data-slean="kbd"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd></kbd> Command palette</p>
<p><kbd data-slean="kbd"><kbd>Cmd</kbd>+<kbd>Enter</kbd></kbd> Send</p>
<p><kbd data-slean="kbd">←</kbd> <kbd data-slean="kbd">→</kbd> Previous and next tab</p>
```

A root holding one kbd per key draws the keys, not itself.

### Search shortcut

```svelte
<script lang="ts">
	import '@svelte-lean/styles/input.css';
	import '@svelte-lean/styles/kbd.css';

	let input = $state<HTMLInputElement>();

	// The <kbd> only shows the shortcut; this handler makes it work.
	function onkeydown(event: KeyboardEvent) {
		if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
			event.preventDefault();
			input?.focus();
		}
	}
</script>

<svelte:window {onkeydown} />

<div class="search">
	<input
		bind:this={input}
		type="search"
		placeholder="Search the docs"
		aria-label="Search the docs"
		aria-keyshortcuts="Control+K Meta+K"
		data-slean="input"
	/>
	<kbd data-slean="kbd"><kbd>Ctrl</kbd>+<kbd>K</kbd></kbd>
</div>

<style>
	.search {
		position: relative;
		max-inline-size: 20rem;
	}

	.search > input {
		inline-size: 100%;
		padding-inline-end: 5rem;
	}

	.search > kbd {
		position: absolute;
		inset-block: 0;
		inset-inline-end: 0.5rem;
		margin-block: auto;
		pointer-events: none;
	}
</style>
```

Press Ctrl+K or Cmd+K anywhere on this page. aria-keyshortcuts announces the shortcut on the input.

### In a menu

```svelte
<script lang="ts">
	// role="menu" is Tier 1: @svelte-lean/vite registers the menu behavior.
	import '@svelte-lean/styles/button.css';
	import '@svelte-lean/styles/kbd.css';
	import '@svelte-lean/styles/menu.css';
</script>

<button
	type="button"
	id="kbd-edit-button"
	popovertarget="kbd-edit-menu"
	aria-haspopup="menu"
	data-slean="button"
	data-variant="outlined"
	data-color="neutral"
>
	Edit
</button>

<div id="kbd-edit-menu" popover role="menu" aria-labelledby="kbd-edit-button" data-slean="menu">
	<button type="button" role="menuitem" aria-keyshortcuts="Control+Z">
		Undo <kbd data-slean="kbd"><kbd>Ctrl</kbd>+<kbd>Z</kbd></kbd>
	</button>
	<button type="button" role="menuitem" aria-keyshortcuts="Control+Shift+Z">
		Redo
		<kbd data-slean="kbd"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></kbd>
	</button>
	<hr />
	<button type="button" role="menuitem" aria-keyshortcuts="Control+C">
		Copy <kbd data-slean="kbd"><kbd>Ctrl</kbd>+<kbd>C</kbd></kbd>
	</button>
	<button type="button" role="menuitem" aria-keyshortcuts="Control+V">
		Paste <kbd data-slean="kbd"><kbd>Ctrl</kbd>+<kbd>V</kbd></kbd>
	</button>
</div>
```

The menu's shortcut part aligns the keys to the end of each item.

### Shortcut list

```svelte
<script lang="ts">
	import '@svelte-lean/styles/kbd.css';

	const shortcuts = [
		{ keys: ['Ctrl', 'S'], action: 'Save' },
		{ keys: ['Ctrl', 'F'], action: 'Find in the page' },
		{ keys: ['Ctrl', 'Shift', 'N'], action: 'New window' },
		{ keys: ['Alt', 'Tab'], action: 'Switch application' }
	];
</script>

<dl class="shortcuts">
	{#each shortcuts as shortcut (shortcut.action)}
		<dt>{shortcut.action}</dt>
		<dd>
			<kbd data-slean="kbd">
				{#each shortcut.keys as key, i (key)}{#if i > 0}+{/if}<kbd>{key}</kbd>{/each}
			</kbd>
		</dd>
	{/each}
</dl>

<style>
	.shortcuts {
		display: grid;
		grid-template-columns: 1fr auto;
		gap: 0.75rem 2rem;
		max-inline-size: 24rem;
		margin: 0;
	}

	.shortcuts dd {
		margin: 0;
	}
</style>
```

## Usage

Plain markup with a stylesheet: install `@svelte-lean/styles`. No JavaScript runs.

```bash
npm install @svelte-lean/styles
```

`stylesheets`

```ts
import '@svelte-lean/styles/kbd.css';
```

Write key names as text: `Ctrl`, `Shift`, `Enter`. Symbols such as ⌘ and ⇧ are read differently, or not at all, by screen readers; when you show one, add the name as visually hidden text.

For a combination, put one `<kbd>` per key inside the root and keep the plus signs as text between them.

## Anatomy

| Part | Element | data-slean-part | Required | Notes |
| --- | --- | --- | --- | --- |
| root | `<kbd data-slean="kbd">` | – | yes | One key as text, or one \<kbd\> per key for a combination. |
| key | `<kbd> inside the root` | – | no | Drawn as keys; the root around them draws nothing. |

## Keyboard

| Key | When | Result |
| --- | --- | --- |
| `Tab` | anywhere | Passes the key by: it is text, not a control |

## Styling

`kbd.css` draws a key as a bordered box with a thicker bottom edge in the page font, and, through `:has()`, draws the inner keys of a combination instead of the root. The tokens it reads:

| Token | Default (light) | Applies to |
| --- | --- | --- |
| `--slean-border-strong` | `var(--slean-neutral-8)` | border |
| `--slean-radius-sm` | `calc(var(--slean-radius) * 0.6)` | border-radius |
| `--slean-surface` | `oklch(100% 0 0)` | background |
| `--slean-fg` | `var(--slean-neutral-12)` | color |
| `--slean-text-xs` | `0.75rem` | font-size |
| `--slean-font-weight-medium` | `500` | font-weight |
| `--slean-fg-muted` | `var(--slean-neutral-11)` | color |

## Under the hood

Runtime and size

- **Tier**: 0 · Native
- **Behavior JS**: 0 B brotli · 0 B gzip · `fixtures/results.json (native-only)` · [method](https://svelte-lean.vercel.app/performance/methodology.md)
- **Platform features**: `<kbd>`, `nested <kbd>`, `:has()`
- **Shared listeners**: none
- **Per-instance listeners**: none
- **Lazy state**: none
- **Native base**: `<kbd>`

Tier 0: the key has no behavior module, the Vite plugin maps `kbd` to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.

Why this implementation

`<kbd>` is HTML for user input, and nesting it is HTML for a combination: the outer element is the whole input, the inner ones are the keys. Browsers and screen readers read it as text. The primitive is the drawing of a key and the rule that a combination is drawn key by key.

Where a shortcut exists, it belongs to a control. `aria-keyshortcuts` on that control announces it; the `<kbd>` only shows it.

### The browser owns

- the \<kbd\> element as user input, and the nested form of a key combination
- the text screen readers read out

### Svelte Lean owns

- kbd.css: the key box with a thicker bottom edge, the combination drawn key by key
- the contract: key names as text, aria-keyshortcuts on the control
- documentation

Accessibility

- `<kbd>` is text; screen readers read the key names in order, the plus signs included.
- The shortcut itself is announced from `aria-keyshortcuts` on the control it activates, not from the `<kbd>`.
- Keys are not focusable and have no role.

Platform features

| Feature | Baseline | Outside the target |
| --- | --- | --- |
| `<kbd>` | Widely available | Not applicable within the support policy |
| `:has()` | Baseline 2023, widely available since June 2026 | A combination is drawn as one box around its keys |

Without JavaScript

Fully functional.

Server rendering and hydration

Static HTML.

Nothing is attached.

Compatibility

`<kbd>` is widely available. `:has()` is Baseline 2023 and widely available since June 2026; without it a combination is drawn as one box around its keys.

Tests and source

- `apps/playground/tests/primitives/display.spec.ts` Playwright: open state, exclusive groups, stretched links, roles and names, carousel scrolling, axe
- `packages/styles/tests` static checks of the stylesheet (layers, tokens, specificity, dark parity)

- `packages/primitives/src/kbd/contract.md` the contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behavior
- `packages/primitives/src/kbd/contract.ts` typed constants: name, tier, base, parts, options, events
- `packages/styles/css/kbd.css` the optional stylesheet
