KQ-2109 · LETTER/JUMP
Letter Jump
A keyboard-first sidebar that reads as a calm, ordinary list until you hold G: then every row raises a letter badge, each rising 4px on springs.snap in a ripple out from the current page, cascade milliseconds apart and inside the 600ms budget, and a letter jumps, its badge flashing as it fills with the accent and lands from 1.25x on springs.snap while the current pill glides to the row on springs.glide and focus follows. Letters several rows want become prefixes, so the first press dims every other row and lights the second letters, and the key assignment is a pure function the server and browser agree on. G can be held or tapped, the header's key button and a long press on the header show the badges for pointers and touch, ? opens a key sheet where any key can be rebound with collisions falling back to two letters, and the arrow keys still walk the list.
Inbox · 12 items · 4 two-letter keys
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/letter-jump
Props
- badgeStyle
- "keycap" | "pill" | "outline"
- default "keycap"
- How the letter badges are drawn: a raised key, a solid accent pill, or a hairline ring with an accent letter.
- cascade
- number
- default 20
- Milliseconds between one badge rising and the next, rippling out from the current row; the whole wave stays under 600ms.
- accent
- "cobalt" | "plum" | "mint" | "ink" | string
- default "cobalt"
- Accent for the current page, the flash and pill badges. A name or any CSS colour.
- sections
- LetterJumpSection[]
- default defaultLetterJumpSections
- The sections, in order: { id, label }.
- items
- LetterJumpItem[]
- default defaultLetterJumpItems
- Every row, in order within its section: { id, label, section, icon, count?, key? }. A key asked for in `key` is honoured unless it collides.
- current / defaultCurrent
- string
- default — / "inbox"
- The open page's id, controlled or initial.
- onNavigate
- (id: string) => void
- default —
- A row was opened: a press, Enter, or a letter jump.
- onRebindKey
- (id: string, key: string) => void
- default —
- A key was rebound in the key sheet; anything it collided with has fallen back.
- showKeys / defaultShowKeys
- boolean
- default — / false
- Whether the badges are up, controlled or initial.
- onShowKeysChange
- (shown: boolean) => void
- default —
- Fires when the badges rise or sink, from the key, button or press that did it.
- leader
- string
- default "g"
- The key held or tapped to raise the badges. It is never assigned to an item.
- timeout
- number
- default 1500
- How long the badges wait for a letter after a tapped leader, in ms.
- longPress
- number
- default 450
- How long a press on the header must last to raise the badges on touch, in ms.
- title
- string
- default "Gaugeworks"
- The product in the header.
- subtitle
- string
- default "Support desk"
- The line under the product.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the list's data has arrived: skeleton rows while loading, a message with Try again on error.
- onRetry
- () => void
- default —
- Try again after an error.
- label
- string
- default —
- The navigation's accessible name. Defaults to "<title> navigation".
- className
- string
- default —
- Classes for the outer container.
| Prop | Type | Default | Description |
|---|---|---|---|
| badgeStyle | "keycap" | "pill" | "outline" | "keycap" | How the letter badges are drawn: a raised key, a solid accent pill, or a hairline ring with an accent letter. |
| cascade | number | 20 | Milliseconds between one badge rising and the next, rippling out from the current row; the whole wave stays under 600ms. |
| accent | "cobalt" | "plum" | "mint" | "ink" | string | "cobalt" | Accent for the current page, the flash and pill badges. A name or any CSS colour. |
| sections | LetterJumpSection[] | defaultLetterJumpSections | The sections, in order: { id, label }. |
| items | LetterJumpItem[] | defaultLetterJumpItems | Every row, in order within its section: { id, label, section, icon, count?, key? }. A key asked for in `key` is honoured unless it collides. |
| current / defaultCurrent | string | — / "inbox" | The open page's id, controlled or initial. |
| onNavigate | (id: string) => void | — | A row was opened: a press, Enter, or a letter jump. |
| onRebindKey | (id: string, key: string) => void | — | A key was rebound in the key sheet; anything it collided with has fallen back. |
| showKeys / defaultShowKeys | boolean | — / false | Whether the badges are up, controlled or initial. |
| onShowKeysChange | (shown: boolean) => void | — | Fires when the badges rise or sink, from the key, button or press that did it. |
| leader | string | "g" | The key held or tapped to raise the badges. It is never assigned to an item. |
| timeout | number | 1500 | How long the badges wait for a letter after a tapped leader, in ms. |
| longPress | number | 450 | How long a press on the header must last to raise the badges on touch, in ms. |
| title | string | "Gaugeworks" | The product in the header. |
| subtitle | string | "Support desk" | The line under the product. |
| status | "ready" | "loading" | "error" | "ready" | Whether the list's data has arrived: skeleton rows while loading, a message with Try again on error. |
| onRetry | () => void | — | Try again after an error. |
| label | string | — | The navigation's accessible name. Defaults to "<title> navigation". |
| className | string | — | Classes for the outer container. |