KQ-1337 · RETURN/FLOW
Return Flow
A returns flow built from data that ends as paper in your hand: pick the items (each tick draws on flick), give each a reason as chips, rows or tiles, choose where the refund goes while its digits roll on snap, and the label feeds out of a printer slot in 3px steps like a stepper motor. Pull it down and a rip runs along the perforation from the left while the label pivots about its unripped corner; past the line it comes free and lands on recoil, and released early it swings back on snap with the throw's velocity. Paged, the steps move by direction inside a box that glides to each panel's measured height under a rail whose pill slides on snap; every pick, reason and card is a real checkbox or radiogroup, and Tear off does by key what the pull does by hand.
step 1 of 4 · items
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/return-flow
Props
- "label" | "qr"
- default "label"
- What comes out of the slot: a shipping label with carrier band, addresses and a seeded barcode, or a printer-free slip with a large QR and a short counter code.
- steps
- "paged" | "inline"
- default "paged"
- Four pages (Items, Reason, Refund, Label) under a step rail with Back and Continue, or one page where reasons unfold under each picked item and the printer arrives at the end.
- reasons
- "chips" | "rows" | "tiles"
- default "chips"
- How a reason is picked: wrapping pills whose check slides in, rows with a detail line and a growing dot, or two- and three-up icon tiles. Each is a radiogroup.
- order
- ReturnOrder
- default defaultReturnOrder
- The order: id, merchant, delivered date (ms), return window in days, the payment a refund goes back to, both addresses and the items (name, variant, price, quantity, kind, tint, returnable).
- reasonOptions
- ReturnReason[]
- default defaultReturnReasons
- The reasons on offer. A reason with note opens a Tell us more field; one with waivesFee (a fault) drops the return fee.
- methods
- RefundMethod[]
- default defaultRefundMethods
- Where the money goes. bonus adds credit as a share of the items; fee: false waives the return fee.
- dropoffs
- ReturnDropoff[]
- default defaultReturnDropoffs
- How the parcel leaves; a fee is taken off the refund.
- value / defaultValue
- ReturnSelection
- default nothing picked, the first method and drop-off
- The selection: one line per picked item (quantity, reason, note) plus the refund method and drop-off. Controlled or uncontrolled.
- onValueChange
- (value: ReturnSelection) => void
- default —
- Fires from the pick, stepper, reason, note or card that changed the selection.
- step / defaultStep
- number
- default 0
- The page, 0 to 2 while editing; the label page (3) shows whenever the label state is ready.
- onStepChange
- (step: number) => void
- default —
- Fires from Continue, Back or a rail stop, and with 3 when the label is made.
- onCreateLabel
- (selection: ReturnSelection, refund: ReturnRefund) => void | Promise<ReturnShippingLabel | void>
- default —
- Make the label. A promise holds the button pending with its width kept; resolve with your label to print it, reject to show the error.
- state
- "idle" | "pending" | "ready" | "error"
- default —
- Controlled label state. Ready prints the label; only a print the visitor started makes sound.
- onStateChange
- (state: ReturnLabelState) => void
- default —
- Fires as the label is requested, made or refused.
- shippingLabel
- ReturnShippingLabel
- default one made from the order, seeded
- The label to print: carrier, tracking, RMA, counter code and drop-by date (ms).
- onTear
- () => void
- default —
- The label was torn off, by a pull or by Tear off.
- onDownload
- () => void
- default —
- Download on a torn label. Without it there is no Download button.
- onEmail
- () => void
- default —
- Email it on a torn label. Without it there is no Email button.
- onStartOver
- () => void
- default —
- Start over was pressed: the selection and the label are cleared and the flow returns to its first page.
- returnFee
- number
- default 4.95
- The return fee, taken off the refund unless the method or a fault reason waives it.
- now
- number | Date
- default defaultReturnNow
- The flow's moment: the days-left pill and the default drop-by date count from it, never the wall clock.
- formatPrice
- (amount: number) => string
- default US dollars
- Turns amounts into text everywhere they appear.
- title
- string
- default "Start a return"
- The heading, and the region's name unless label is given.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the order has arrived: loading shows skeleton rows, error a message with Try again.
- onRetry
- () => void
- default —
- Try again was pressed after the order failed to load.
- label
- string
- default —
- The region's accessible name, when the visible title is not enough.
- sound
- boolean
- default false
- Clicks for picks, reasons and steps, soft paper bursts as a visitor-started label feeds and a louder paper when it tears.
- disabled
- boolean
- default false
- Read only: everything shows, nothing can be changed.
- className
- string
- default —
- Classes for the root. It is at most 560px tall and scrolls inside itself.
| Prop | Type | Default | Description |
|---|---|---|---|
| "label" | "qr" | "label" | What comes out of the slot: a shipping label with carrier band, addresses and a seeded barcode, or a printer-free slip with a large QR and a short counter code. | |
| steps | "paged" | "inline" | "paged" | Four pages (Items, Reason, Refund, Label) under a step rail with Back and Continue, or one page where reasons unfold under each picked item and the printer arrives at the end. |
| reasons | "chips" | "rows" | "tiles" | "chips" | How a reason is picked: wrapping pills whose check slides in, rows with a detail line and a growing dot, or two- and three-up icon tiles. Each is a radiogroup. |
| order | ReturnOrder | defaultReturnOrder | The order: id, merchant, delivered date (ms), return window in days, the payment a refund goes back to, both addresses and the items (name, variant, price, quantity, kind, tint, returnable). |
| reasonOptions | ReturnReason[] | defaultReturnReasons | The reasons on offer. A reason with note opens a Tell us more field; one with waivesFee (a fault) drops the return fee. |
| methods | RefundMethod[] | defaultRefundMethods | Where the money goes. bonus adds credit as a share of the items; fee: false waives the return fee. |
| dropoffs | ReturnDropoff[] | defaultReturnDropoffs | How the parcel leaves; a fee is taken off the refund. |
| value / defaultValue | ReturnSelection | nothing picked, the first method and drop-off | The selection: one line per picked item (quantity, reason, note) plus the refund method and drop-off. Controlled or uncontrolled. |
| onValueChange | (value: ReturnSelection) => void | — | Fires from the pick, stepper, reason, note or card that changed the selection. |
| step / defaultStep | number | 0 | The page, 0 to 2 while editing; the label page (3) shows whenever the label state is ready. |
| onStepChange | (step: number) => void | — | Fires from Continue, Back or a rail stop, and with 3 when the label is made. |
| onCreateLabel | (selection: ReturnSelection, refund: ReturnRefund) => void | Promise<ReturnShippingLabel | void> | — | Make the label. A promise holds the button pending with its width kept; resolve with your label to print it, reject to show the error. |
| state | "idle" | "pending" | "ready" | "error" | — | Controlled label state. Ready prints the label; only a print the visitor started makes sound. |
| onStateChange | (state: ReturnLabelState) => void | — | Fires as the label is requested, made or refused. |
| shippingLabel | ReturnShippingLabel | one made from the order, seeded | The label to print: carrier, tracking, RMA, counter code and drop-by date (ms). |
| onTear | () => void | — | The label was torn off, by a pull or by Tear off. |
| onDownload | () => void | — | Download on a torn label. Without it there is no Download button. |
| onEmail | () => void | — | Email it on a torn label. Without it there is no Email button. |
| onStartOver | () => void | — | Start over was pressed: the selection and the label are cleared and the flow returns to its first page. |
| returnFee | number | 4.95 | The return fee, taken off the refund unless the method or a fault reason waives it. |
| now | number | Date | defaultReturnNow | The flow's moment: the days-left pill and the default drop-by date count from it, never the wall clock. |
| formatPrice | (amount: number) => string | US dollars | Turns amounts into text everywhere they appear. |
| title | string | "Start a return" | The heading, and the region's name unless label is given. |
| status | "ready" | "loading" | "error" | "ready" | Whether the order has arrived: loading shows skeleton rows, error a message with Try again. |
| onRetry | () => void | — | Try again was pressed after the order failed to load. |
| label | string | — | The region's accessible name, when the visible title is not enough. |
| sound | boolean | false | Clicks for picks, reasons and steps, soft paper bursts as a visitor-started label feeds and a louder paper when it tears. |
| disabled | boolean | false | Read only: everything shows, nothing can be changed. |
| className | string | — | Classes for the root. It is at most 560px tall and scrolls inside itself. |