KQ-1776 · VISITOR/CHAT
Visitor Chat
A website chat widget that lives in the corner of its own box, with the host page behind it. Pressing the launcher stretches it into the window on a goo layer used only while the shape moves: the window grows out of the launcher, rising on springs.snap and widening on springs.glide while its fill turns from the accent to the card and the neck between them thins until it lets go, then the crisp window takes over and its content arrives; closing runs back into the launcher on the exit ease. Inside, the header says who is on and how soon they reply, the messages are a log where an agent's reply resolves out of the typing bubble on springs.glide, the composer is a real textarea with attach and send, a failed send keeps a retry on its bubble, outside office hours the window asks for an email first, and Escape closes it and returns focus to the launcher.
Waylight help
Usually replies in 4 min · Ines and Tomás are online
waylight help · press the launcher in the corner
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/visitor-chat
Props
- launcherShape
- "circle" | "squircle" | "pill"
- default "circle"
- The launcher: a circle, a rounded square, or a pill with "Chat with us". The stretch starts from its shape.
- bubbleStyle
- "tinted" | "solid" | "minimal"
- default "tinted"
- Message bubbles: a soft tint, a solid accent for the visitor, or agents as plain text.
- accent
- string
- default "var(--accent-bright)"
- Any CSS colour: the launcher, the visitor's bubbles, read marks and focus edges.
- team
- ChatAgent[]
- default defaultChatTeam
- Who answers: { id, name, role?, online, hue? }.
- hours
- ChatHours
- default defaultChatHours
- When the team answers live: { days (0 = Sunday), open, close (minutes), utcOffset (minutes), zone }.
- messages / defaultMessages
- ChatMessage[]
- default — / []
- The thread, controlled or initial: { id, from ("visitor" or an agent id), text, at, status?, attachment? }.
- onMessagesChange
- (messages: ChatMessage[]) => void
- default —
- Fires from a send, a delivery change or a retry, with every message.
- typing
- string | null
- default null
- The agent typing now, by id; their next message resolves out of the indicator.
- onSend
- (message: ChatMessage, meta: { email: string | null }) => void | Promise<void>
- default —
- A message was sent or retried. Return a promise: resolved marks it sent, rejected keeps a retry on its bubble.
- onAttach
- (files: File[]) => void
- default —
- Files were picked with the attach button; the first rides on the next message.
- open / defaultOpen
- boolean
- default — / false
- Whether the window is open, controlled or initial. A host that opens it gets the same stretch, without moving focus.
- onOpenChange
- (open: boolean) => void
- default —
- The launcher, Escape or the minimise button changed the open state.
- onClose
- () => void
- default —
- The window was closed by the visitor.
- now
- number
- default defaultChatNow (Fri 16 Oct 2026 14:20 UTC)
- The current time in ms; decides office hours. Never read from the clock.
- replyMinutes
- number
- default 4
- How soon the team usually replies, in minutes, shown in the header.
- title
- string
- default "Waylight help"
- The window's title and the dialog's name.
- greeting
- string
- default "Hi, we're the Waylight team. Ask us anything about your workspace, billing or the API."
- The team's first message, always shown at the top.
- height
- number
- default 560
- Height of the box the widget lives in, in px.
- disabled
- boolean
- default false
- Nothing can be opened or sent.
- className
- string
- default —
- Classes for the outer box.
- children
- ReactNode
- default —
- The page behind the widget.
| Prop | Type | Default | Description |
|---|---|---|---|
| launcherShape | "circle" | "squircle" | "pill" | "circle" | The launcher: a circle, a rounded square, or a pill with "Chat with us". The stretch starts from its shape. |
| bubbleStyle | "tinted" | "solid" | "minimal" | "tinted" | Message bubbles: a soft tint, a solid accent for the visitor, or agents as plain text. |
| accent | string | "var(--accent-bright)" | Any CSS colour: the launcher, the visitor's bubbles, read marks and focus edges. |
| team | ChatAgent[] | defaultChatTeam | Who answers: { id, name, role?, online, hue? }. |
| hours | ChatHours | defaultChatHours | When the team answers live: { days (0 = Sunday), open, close (minutes), utcOffset (minutes), zone }. |
| messages / defaultMessages | ChatMessage[] | — / [] | The thread, controlled or initial: { id, from ("visitor" or an agent id), text, at, status?, attachment? }. |
| onMessagesChange | (messages: ChatMessage[]) => void | — | Fires from a send, a delivery change or a retry, with every message. |
| typing | string | null | null | The agent typing now, by id; their next message resolves out of the indicator. |
| onSend | (message: ChatMessage, meta: { email: string | null }) => void | Promise<void> | — | A message was sent or retried. Return a promise: resolved marks it sent, rejected keeps a retry on its bubble. |
| onAttach | (files: File[]) => void | — | Files were picked with the attach button; the first rides on the next message. |
| open / defaultOpen | boolean | — / false | Whether the window is open, controlled or initial. A host that opens it gets the same stretch, without moving focus. |
| onOpenChange | (open: boolean) => void | — | The launcher, Escape or the minimise button changed the open state. |
| onClose | () => void | — | The window was closed by the visitor. |
| now | number | defaultChatNow (Fri 16 Oct 2026 14:20 UTC) | The current time in ms; decides office hours. Never read from the clock. |
| replyMinutes | number | 4 | How soon the team usually replies, in minutes, shown in the header. |
| title | string | "Waylight help" | The window's title and the dialog's name. |
| greeting | string | "Hi, we're the Waylight team. Ask us anything about your workspace, billing or the API." | The team's first message, always shown at the top. |
| height | number | 560 | Height of the box the widget lives in, in px. |
| disabled | boolean | false | Nothing can be opened or sent. |
| className | string | — | Classes for the outer box. |
| children | ReactNode | — | The page behind the widget. |