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.

KQ-1777 · SWIPE/REPLY

Noor Haddad

Active 5 min ago

Arrow keys move between messages. R replies to the focused message; the menu key opens reactions.

  1. Noor Haddad, 18:40: Are we still on for the Fieldline site visit on Thursday?
  2. You, 18:42: Yes. I booked the 08:10 train, back by six. Read.
  3. Noor Haddad, 18:43: Perfect. Can you bring the survey tablets?
  4. You, 18:51, replying to Noor Haddad: Both charged and in my bag. Read. 1 reaction: thumbs up.
  5. Noor Haddad, 09:12: Morning. The client moved the walkthrough to 10:30.
  6. Noor Haddad, 09:13: Still fine for the train?
  7. You, 09:20: Fine. I'll grab coffee for both of us. Read.
  8. Noor Haddad, 09:31: You're the best. Gate 4, I'll be early. 1 reaction: heart.

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.