keycaps
Keycaps and keyboard shortcuts as Flutter widgets.
Write the shortcut once; the package prints it the way the platform does —
⌘⇧P on macOS, Ctrl+Shift+P on Windows, Super instead of Win on Linux.
import 'package:keycaps/keycaps.dart';
const Keycap('K'); // a single cap
Keycaps.parse('Cmd+Shift+P'); // a chord
KeycapSequence.parse(const <String>['Ctrl+K', 'Ctrl+S']); // chords in order
The same list, three platforms
Nothing below changed but the platform the caps resolve against — same strings,
same widgets. Apple sets modifiers solid as glyphs; Windows and Linux spell them
out, join them with +, and wear their own logo on the meta key.
| macOS | Windows | Linux |
|---|---|---|
![]() |
![]() |
![]() |
Both screenshots came out of the example app's platform switcher, on one machine.
Sources for a shortcut
| Source | Widget |
|---|---|
| Text | Keycaps.parse('Cmd+Shift+P') |
| A Flutter activator | Keycaps.activator(const SingleActivator(LogicalKeyboardKey.keyP, meta: true)) |
| Explicit tokens | Keycaps(const <KeyToken>[KeyToken.key(LogicalKeyboardKey.meta), KeyToken.text('Fn')]) |
| A single key | const Keycap.logical(LogicalKeyboardKey.escape) |
| Anything else | Keycap.child(child: Icon(Icons.keyboard_return)) |
Keycaps.activator also orders the modifiers conventionally: ⌃⌥⇧ then ⌘ on
Apple, meta first on Windows and Linux.
Parsing rules
Parts are separated by + and trimmed, so Ctrl + K and Ctrl+K are the same.
A + in key position is the plus key, so Cmd++ is two caps. Names are matched
case-insensitively against ShortcutParser.aliases (cmd, command, ⌘,
opt, esc, pgdn, f7, …); a single unknown character becomes that key, and
a longer unknown word is printed verbatim — handy for Fn.
Icons
Some keys say more as a picture: the meta key wears the OS logo, arrows point.
KeycapIcons decides how far that goes.
| Mode | Draws |
|---|---|
auto (default) |
Windows/Linux logo on meta, plus the four arrow keys. Apple keeps its native glyphs — ⌘ is the convention there. |
always |
Every key with an icon: ⌘ ⌥ ⌃ ⇧ ⇪ ⏎ ⌫ ⇥, Home/End, PgUp/PgDn |
never |
Text only |
Keycaps.parse('Cmd+Shift+P', style: const KeycapStyle(icons: KeycapIcons.always))
Icons come from phosphor_icons, referenced only through PhosphorIconsRegular
static const fields so --tree-shake-icons can subset the font. Release
builds should pass that flag.
The other five Phosphor weights are a subtler cost: the shaker only subsets a
font it found references for, so weights nothing draws are copied whole —
about 2 MB. Mount const KeycapIconFontGuard() anywhere in your tree to
reference one glyph from each and cut them to a few hundred bytes apiece. Skip
it if your app already draws from those weights.
Pressing and highlighting
Caps are not static pictures.
- Tap — a cap sinks under the finger and rises on release, whether or not
you pass a handler.
Keycap(onTap:)for one cap,Keycaps(onKeyTap:)for a row, which hands you theKeyTokenthat was hit. Turn the animation off withtapEffect: false. - Live highlight — a cap built from a
LogicalKeyboardKeylights up inhighlightColorwhile that key is actually held on the device, so a shortcut list doubles as a cheat sheet that answers as you type. Off withliveHighlight: false. - Forced —
Keycap(pressed: true)holds a cap down regardless of input.
Highlighting shares one app-wide HardwareKeyboard handler
(KeycapKeyboardWatcher), installed only while a highlighting cap is mounted
and never swallowing an event, so your own shortcuts still fire. Left/right
variants count: a cap for LogicalKeyboardKey.shift lights up for shiftLeft.
Styling
Appearance comes from the nearest KeycapTheme, falling back to one derived
from the ambient Material theme. Style a whole subtree:
KeycapTheme(
data: KeycapThemeData.fromTheme(Theme.of(context), size: KeycapSize.small)
.copyWith(variant: KeycapVariant.outlined),
child: child,
)
…or one widget, with a KeycapStyle (all fields optional, applied over the
inherited theme):
Keycaps.parse(
'Cmd+K',
style: const KeycapStyle(size: KeycapSize.large, variant: KeycapVariant.flat),
)
| Knob | Values |
|---|---|
variant |
raised (default, with a bottom edge), flat, outlined, bare |
size |
KeycapSize.small / medium / large, or set metrics individually |
join |
auto (+ off Apple, nothing on Apple), plus, none, tight |
platform |
Force KeycapPlatform.apple / windows / linux labels |
icons |
auto, always, never (see above), plus iconSize |
liveHighlight / tapEffect |
Turn the two reactions off |
KeycapThemeData also exposes faceColor, edgeColor, borderColor,
foregroundColor, highlightColor, highlightForegroundColor, height,
minWidth, padding, radius, depth, borderWidth, spacing,
pressDuration, textStyle and separatorStyle, plus lerp for animating
between themes.
Accessibility
Printed labels are glyphs (⌘, ⌫) or clipped words (PgDn), which read badly
aloud. Caps announce full words instead — a Keycaps.parse('Cmd+Shift+P') row
reads as “Command Shift P”. Override with Keycap(semanticLabel: …).
Example
cd example
flutter run
A gallery of every variant, size and icon mode, a platform switcher, tap feedback, and a live keyboard mirror.
Libraries
- keycaps
- Keycaps and keyboard shortcuts as Flutter widgets.


