KQ-1777 · SWIPE/REPLY
Swipe Reply
A direct-message thread on a phone-width screen with day dividers, delivery ticks, reactions and a composer. Swiping a bubble toward the middle pulls it 1:1 for swipeDistance px and rubber-bands past it while a ring in the gap it uncovers fills with the pull and, full, arms with a springs.flick pop; letting go always snaps the bubble home on springs.snap with the release velocity, and an armed pull docks the quoted message above the composer on springs.glide with its accent edge drawing down. Keyboard users walk the messages with the arrow keys, press R to reply and the menu key (or Shift+F10) to open reactions, which land under the bubble on springs.recoil; older messages load at the top behind a spinner without moving what you were reading, and a failed message offers retry.
Noor Haddad
Active 5 min ago
Arrow keys move between messages. R replies to the focused message; the menu key opens reactions.
swipe a bubble · or focus one and press r
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/swipe-reply
Props
- swipeDistance
- number
- default 64
- How far a pull travels before the reply arms, in px; past it the bubble rubber-bands.
- bubbleStyle
- "tail" | "round" | "block"
- default "tail"
- Bubble shape: a small tail on the last of a run, full pills, or squarer blocks.
- accent
- string
- default "var(--accent-bright)"
- Any CSS colour: your bubbles, the reply ring, the dock's edge and focus edges.
- messages / defaultMessages
- SwipeMessage[]
- default — / defaultSwipeMessages
- The thread, oldest first, controlled or initial: { id, from, text, at, status?, replyTo?, reactions? }.
- onMessagesChange
- (messages: SwipeMessage[]) => void
- default —
- Fires from a send, a reaction, a retry or older messages arriving, with every message.
- participants
- SwipePerson[]
- default defaultSwipePeople
- Everyone in the thread: { id, name, hue?, presence? }.
- me
- string
- default "you"
- Which participant is you; your bubbles sit right and swipe left.
- onSend
- (message: SwipeMessage) => void | Promise<void>
- default —
- A message was sent. Return a promise: resolved marks it sent, rejected marks it failed with a retry.
- onReply
- (message: SwipeMessage) => void
- default —
- A message was picked to reply to, by swipe, R or the hover bar.
- onReact
- (message: SwipeMessage, reaction: SwipeReaction, on: boolean) => void
- default —
- You added or removed a reaction (like, love, laugh, wow, sad, done).
- onRetry
- (message: SwipeMessage) => void
- default —
- Retry was pressed on a failed message; it is sent again through onSend.
- onLoadEarlier
- () => Promise<SwipeMessage[] | void> | void
- default —
- The thread reached the top; return older messages (oldest first) or add them yourself.
- hasEarlier
- boolean
- default true
- Whether older messages exist; shows the loader row at the top.
- now
- number
- default defaultSwipeNow (17 Oct 2026 09:40 UTC)
- The current time in ms, for day dividers (read in UTC). Never read from the clock.
- height
- number
- default 560
- Height of the screen in px; the thread scrolls inside.
- disabled
- boolean
- default false
- Nothing can be sent, swiped or reacted to.
- className
- string
- default —
- Classes for the outer box.
| Prop | Type | Default | Description |
|---|---|---|---|
| swipeDistance | number | 64 | How far a pull travels before the reply arms, in px; past it the bubble rubber-bands. |
| bubbleStyle | "tail" | "round" | "block" | "tail" | Bubble shape: a small tail on the last of a run, full pills, or squarer blocks. |
| accent | string | "var(--accent-bright)" | Any CSS colour: your bubbles, the reply ring, the dock's edge and focus edges. |
| messages / defaultMessages | SwipeMessage[] | — / defaultSwipeMessages | The thread, oldest first, controlled or initial: { id, from, text, at, status?, replyTo?, reactions? }. |
| onMessagesChange | (messages: SwipeMessage[]) => void | — | Fires from a send, a reaction, a retry or older messages arriving, with every message. |
| participants | SwipePerson[] | defaultSwipePeople | Everyone in the thread: { id, name, hue?, presence? }. |
| me | string | "you" | Which participant is you; your bubbles sit right and swipe left. |
| onSend | (message: SwipeMessage) => void | Promise<void> | — | A message was sent. Return a promise: resolved marks it sent, rejected marks it failed with a retry. |
| onReply | (message: SwipeMessage) => void | — | A message was picked to reply to, by swipe, R or the hover bar. |
| onReact | (message: SwipeMessage, reaction: SwipeReaction, on: boolean) => void | — | You added or removed a reaction (like, love, laugh, wow, sad, done). |
| onRetry | (message: SwipeMessage) => void | — | Retry was pressed on a failed message; it is sent again through onSend. |
| onLoadEarlier | () => Promise<SwipeMessage[] | void> | void | — | The thread reached the top; return older messages (oldest first) or add them yourself. |
| hasEarlier | boolean | true | Whether older messages exist; shows the loader row at the top. |
| now | number | defaultSwipeNow (17 Oct 2026 09:40 UTC) | The current time in ms, for day dividers (read in UTC). Never read from the clock. |
| height | number | 560 | Height of the screen in px; the thread scrolls inside. |
| disabled | boolean | false | Nothing can be sent, swiped or reacted to. |
| className | string | — | Classes for the outer box. |