Shortcut
Draws a keyboard shortcut the way the reader's platform prints it, and binds it.
pnpm dlx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-shortcut.jsonnpx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-shortcut.jsonbunx --bun shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-shortcut.jsonyarn dlx shadcn-svelte@latest add https://coral.imlargo.dev/r/kit-shortcut.json<Kbd>⌘</Kbd><Kbd>K</Kbd> is right for half the readers, and the keydown listener next to it usually
checks metaKey, so on Windows the hint is wrong and the shortcut does not work. Pressing / to
search fires while someone is typing a slash into a field, and a combo checked with includes also
fires on every combo that contains it.
Press Alt N
Presses: 0
<script lang="ts">
import Shortcut from '#lib/components/coral/kit/shortcut/shortcut.svelte';
let presses = $state(0);
</script>
<div class="flex flex-col items-center gap-3">
<p class="flex items-center gap-2 text-sm">
Press <Shortcut keys="alt+n" onpress={() => presses++} />
</p>
<p class="text-sm text-muted-foreground">Presses: {presses}</p>
</div>What Coral adds
modmeans the right key. Command on a Mac, Control everywhere else; drawn and matched alike.- Drawn by platform convention.
⌃⌥⇧⌘in Apple's order on a Mac,Ctrl Alt Shiftelsewhere. - Read out by name. Symbols are hidden from screen readers and named instead:
⌘is otherwise read as "place of interest sign". - Exact matching:
mod+kdoes not fire onmod+shift+k. - Layout-aware. Keys are matched by the character typed, so
ctrl+zis the key labelled Z on an AZERTY keyboard; the physical key is only consulted when Option has turnedkinto˚. - Stays out of fields. A plain key such as
sor?does not fire while the reader is typing; combos with Control, Command or Alt do. Key repeat and IME composition are ignored.
Platforms
| Shortcut | macOS | Windows / Linux |
|---|---|---|
mod+shift+p | Shift Command P | Control Shift P |
alt+enter | Option Enter | Alt Enter |
ctrl+alt+delete | Control Option Delete | Control Alt Delete |
shift+? | Shift ? | Shift ? |
<script lang="ts">
import Shortcut from '#lib/components/coral/kit/shortcut/shortcut.svelte';
const combos = ['mod+shift+p', 'alt+enter', 'ctrl+alt+delete', 'shift+?'];
</script>
<table class="text-sm">
<thead>
<tr class="text-muted-foreground">
<th class="pe-6 text-start font-normal">Shortcut</th>
<th class="pe-6 text-start font-normal">macOS</th>
<th class="text-start font-normal">Windows / Linux</th>
</tr>
</thead>
<tbody>
{#each combos as keys (keys)}
<tr>
<td class="py-1 pe-6"><code>{keys}</code></td>
<td class="py-1 pe-6"><Shortcut {keys} platform="mac" /></td>
<td class="py-1"><Shortcut {keys} platform="other" /></td>
</tr>
{/each}
</tbody>
</table>The platform is detected in the browser after mount. The server cannot know it, so it renders the
non-Mac version and a Mac corrects it on hydration: one repaint, and no hydration mismatch. When the
server does know, from the user-agent, pass platform and there is nothing to correct.
Plain keys
S focuses search from anywhere
<script lang="ts">
import SearchInput from '#lib/components/coral/kit/search-input/search-input.svelte';
import Shortcut from '#lib/components/coral/kit/shortcut/shortcut.svelte';
let field = $state<HTMLInputElement | null>(null);
</script>
<!-- A plain key: it does not fire while typing in a field, so `s` still types an s there. -->
<div class="flex w-80 flex-col gap-2">
<SearchInput bind:ref={field} placeholder="Search…" aria-label="Search" aria-keyshortcuts="S" />
<p class="flex items-center gap-2 text-sm text-muted-foreground">
<Shortcut keys="s" onpress={() => field?.focus()} /> focuses search from anywhere
</p>
</div>A shortcut something closer to the focus already handled (its keydown called preventDefault) is
left alone. This site's own search takes mod+k and /, which is why these demos use other keys.
Put aria-keyshortcuts on the control the shortcut activates, so assistive tech announces it there. ariaKeyshortcuts() in keys.ts produces the attribute's syntax from a combo.
Without drawing anything
listen is exported on its own and returns its cleanup, which is the shape an $effect wants:
<script lang="ts">
import { listen } from '#lib/components/coral/kit/shortcut/listen.js';
let open = $state(false);
$effect(() => listen('mod+k', () => (open = true)));
</script>Import
<script lang="ts">
import Shortcut from '#lib/components/coral/kit/shortcut/shortcut.svelte';
</script>Props
Everything the shadcn kbd group accepts stays available: class, aria-*, ref. On top of that:
| Prop | Type | Default | Description |
|---|---|---|---|
keys | string | - | The combo, as mod+shift+k. Required. |
onpress | (event: KeyboardEvent) => void | - | Binds the combo. Omit to only draw it. |
enabled | boolean | true | Stops listening without unmounting. |
allowInFields | boolean | with a modifier | Whether it fires while typing in a field. |
preventDefault | boolean | true | Stops the browser's own action for the combo. |
platform | 'mac' | 'other' | detected | Forces a platform's conventions. |
key | Snippet<[{ symbol, name }]> | - | Replaces how each key is drawn. |
Understood aliases: cmd, command, ctrl, control, opt, option, alt, shift, esc, return, del, up, down, left, right, space, plus.