Keystroke
Physical keycaps that press, hold and spring back in real order, a shortcut overlay for keyboard-first product demos
$ pnpm dlx shadcn@latest add @remocn/keystrokeUsage
Keystroke is the keyboard counterpart of simulated-cursor. For each
shortcut, physical keycaps rise in near the bottom of the frame and press in
real order. The modifiers go down first and stay down while the letter taps,
then come up after it. Each keytop sinks into its base, so the side wall
shortens and the contact shadow tightens. On release it springs back with a small
overshoot, and the legend of a key that is down turns the accent color. The
caps leave together after an idle hold. When the next shortcut arrives, the
previous one slides back and dims before it leaves.
import { Backdrop } from "@/components/remocn/backdrop";
import { Keystroke } from "@/components/remocn/keystroke";
export const ShortcutOverlay = () => (
<Backdrop
fill={{ type: "color", value: "#e9e9e5" }}
padding={0}
radius={0}
shadow=""
>
<Keystroke />
</Backdrop>
);The component paints no background, so it composites over any scene.
theme="light" draws off-white caps on a light-gray wall, and theme="dark"
draws graphite caps. platform="mac" draws the glyphs ⌘ ⌥ ⇧ ⌃ ↩ ⌫ ⎋ ⇥, and
platform="windows" writes Ctrl, Alt, Shift and Enter on wider caps. x and
y place the center of the row as fractions of the frame. size is the
keycap height in reference pixels at 720p and scales with the composition
height, so the caps look the same at 1080p and 4K. In a vertical video, pass a
smaller size.
Your own shortcuts
Each step is { at, keys, hold }. at is the frame the shortcut fires: the
frame its last key bottoms out. It uses the same Sequence-local frames as the
at of a remocn-ui step, so a keystroke and the UI it opens can share one
number. Everything before the click is scheduled backward from at.
import { Backdrop } from "@/components/remocn/backdrop";
import { CommandMenu } from "@/components/remocn/command-menu";
import { Keystroke } from "@/components/remocn/keystroke";
import { useCurrentState } from "@/lib/remocn-ui";
const OPEN = 24;
const CLOSE = 96;
export const PaletteDemo = () => {
const state = useCurrentState(
[
{ at: OPEN, state: "opened" },
{ at: CLOSE, state: "closed" },
],
"closed",
);
return (
<Backdrop
fill={{ type: "color", value: "#f4f4f2" }}
padding={0}
radius={0}
shadow=""
>
<CommandMenu state={state} />
<Keystroke
steps={[
{ at: OPEN, keys: "mod+k" },
{ at: CLOSE, keys: "esc" },
]}
/>
</Backdrop>
);
};keys is a combo joined with + ("mod+k", "shift+mod+p", "enter") or a
sequence of combos separated by spaces ("g i" presses G, then I). A sequence
fires on its last key, and a subtle "then" joins its chords. mod is ⌘ on mac
and Ctrl on windows. The other modifiers are shift, alt/option,
ctrl/control, cmd/meta (Win on windows) and fn. They are always
drawn and pressed in the platform's standard order. Named keys include enter, esc, tab,
space, backspace, delete, the arrows (up, down, left, right),
f1 to f24, and plus for a plus key. Any other single character is drawn
as its uppercase letter.
Timing
At 30 fps with speed={1} and the default steps, mod+k on 20, g i on 78
and shift+mod+p on 114:
| Frames | Motion |
|---|---|
| 0–7 | Empty frame |
| 7–18 | ⌘ and K rise in and settle |
| 14–20 | ⌘ goes down, then K; ⌘K fires on 20 |
| 28–41 | K springs up on 28, ⌘ on 31 |
| 48–58 | G rises in while ⌘K slides back and dims; G fires on 58 |
| 61–69 | ⌘K leaves |
| 66–76 | G springs up |
| 68–78 | "then" and I rise in, the row re-centers and G dims; G then I fires on 78 |
| 86–96 | I springs up |
| 98–114 | ⇧, ⌘ and P rise in as G then I slides back, then go down 3 frames apart; ⇧⌘P fires on 114 |
| 116–124 | G then I leaves |
| 122–138 | P, ⌘ and ⇧ spring up in reverse order |
| 158–166 | ⇧⌘P leaves after its linger |
| 166–180 | Empty hold |
Each key takes 3 frames to go down. Keys in a combo start 3 frames apart and
come up 3 frames apart, last down first up. The chord stays down for its
hold (8 frames by default). The release lands exactly on rest 10 frames
later. The caps start rising before at: 10 frames for a single key, 13 for a
two-key combo, 16 for three, and 30 for g i. getKeystrokeLeadIn(keys, hold)
returns that lead-in, so leave at least that much before the first at in a
Sequence. After the last key comes up, a group waits linger frames (30 by
default), then leaves over 8 frames. A group pushed back by a newer one leaves
sooner, 18 frames after the push. If its keys are still down at the push, the
18 frames count from its last key-up instead, so its release still plays. At
most two groups hold the screen: a third arrival sends the oldest off at once.
keystrokeLength is 166, the frame the default content has left.
getKeystrokeDuration({ steps, linger, speed, fps }) returns the frames your
own steps need, plus a 14-frame tail. at, hold and linger are frames of
your composition, exactly like remocn-ui at. The press and release motion
keeps the same timing in seconds at any frame rate.
| Prop | Type | Default | Description |
|---|---|---|---|
steps | KeystrokeStep[] | three-step flow | Shortcuts in order, each { at, keys, hold? }; the default is ⌘K, G then I, ⇧⌘P |
platform | "mac" | "windows" | "mac" | Resolves mod to ⌘ or Ctrl and draws glyph or word legends |
theme | "light" | "dark" | "light" | Off-white caps on a light-gray wall, or graphite caps |
accent | string | "#0a84ff" | Legend color of a key while it is down |
x | number | 0.5 | Row center as a fraction of the frame width |
y | number | 0.86 | Row center as a fraction of the frame height |
size | number | 64 | Keycap height and 1u key width in reference px at 720p, clamped to 8–400 |
linger | number | 30 | Frames the caps stay after the last key comes up, before they leave |
speed | number | 1 | Playback multiplier applied to the frame, like remocn-ui; 0 never fires |
className | string | — | Optional class name on the full-frame root |
KeystrokeStep
| Prop | Type | Default | Description |
|---|---|---|---|
atrequired | number | — | Sequence-local frame the shortcut fires, when its last key bottoms out |
keysrequired | string | — | A combo such as "shift+mod+p", or chords separated by spaces such as "g i" |
hold | number | 8 | Frames the chord stays down after it fires |