KQ-1702 · ADD/ON/TRAY
Add-on Tray
An add-on picker: toggle cards above, beside or under a running total tray. Turning a card on drops its chip into the tray on a small arc — one progress value on springs.glide carries it from the card's price to its place, x straight and y lifted — and it lands with two small bounces on springs.recoil as the monthly total rolls to its new value on odometer wheels on springs.snap; turning it off lifts the chip back out toward its card while the tray closes up on glide. Cards are role="checkbox" buttons with two-dimensional arrow-key travel; incompatible add-ons stay focusable, say why in place of their description and bring the reason forward when pressed, prices shimmer while they load, and narrow containers dock the tray at the bottom.
Add-ons
Fieldline · Team plan, billed monthly
prices loading · fieldline team
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/add-on-tray
Props
- addons
- AddOn[]
- default defaultAddOns
- The add-ons offered: { id, name, description, price, icon?, conflicts?, requires?, note? }.
- value / defaultValue
- string[]
- default — / ["storage"]
- The ids turned on, controlled or initial.
- onValueChange
- (ids: string[]) => void
- default —
- Fires from the press or key that changed the selection, with every id on.
- onToggle
- (id: string, on: boolean) => void
- default —
- One add-on was turned on or off.
- onBlocked
- (id: string, reason: string) => void
- default —
- An add-on that cannot be added was pressed, with the reason it shows.
- onCheckout
- (order: { ids: string[]; total: number }) => void
- default —
- Checkout was pressed, with the ids and the monthly total.
- base
- { name: string; price: number }
- default defaultAddOnBase
- The plan the add-ons sit on; its price starts the total.
- pricesLoading
- boolean
- default false
- Prices are on their way: they shimmer (only while on screen) and checkout is held.
- cardStyle
- "outline" | "tile" | "row"
- default "outline"
- Bordered cards with a round check, filled tiles with a large icon and a price pill, or compact list rows with a square check.
- trayPosition
- "bottom" | "side" | "float"
- default "bottom"
- A flush panel under the cards, a receipt column beside them, or a floating bar; narrow containers dock every one at the bottom.
- accent
- "cobalt" | "signal" | "amber" | "graphite" | string
- default "cobalt"
- Checks, chips, the flight and the chosen cards' wash: a named accent or any CSS colour.
- formatPrice
- (amount: number) => string
- default US dollars
- Formats prices; the total's symbol is read from formatPrice(0), so other currencies roll correctly.
- period
- string
- default "mo"
- What the prices are per.
- title
- string
- default "Add-ons"
- The heading.
- checkoutLabel
- string
- default "Checkout"
- The checkout button's word.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the add-ons have arrived: placeholder cards while loading, a retry on error.
- onRetry
- () => void
- default —
- Try again was pressed after the add-ons failed to load.
- disabled
- boolean
- default false
- Holds every card and checkout.
- className
- string
- default —
- Classes for the surface; give it a height and the cards scroll above the tray.
| Prop | Type | Default | Description |
|---|---|---|---|
| addons | AddOn[] | defaultAddOns | The add-ons offered: { id, name, description, price, icon?, conflicts?, requires?, note? }. |
| value / defaultValue | string[] | — / ["storage"] | The ids turned on, controlled or initial. |
| onValueChange | (ids: string[]) => void | — | Fires from the press or key that changed the selection, with every id on. |
| onToggle | (id: string, on: boolean) => void | — | One add-on was turned on or off. |
| onBlocked | (id: string, reason: string) => void | — | An add-on that cannot be added was pressed, with the reason it shows. |
| onCheckout | (order: { ids: string[]; total: number }) => void | — | Checkout was pressed, with the ids and the monthly total. |
| base | { name: string; price: number } | defaultAddOnBase | The plan the add-ons sit on; its price starts the total. |
| pricesLoading | boolean | false | Prices are on their way: they shimmer (only while on screen) and checkout is held. |
| cardStyle | "outline" | "tile" | "row" | "outline" | Bordered cards with a round check, filled tiles with a large icon and a price pill, or compact list rows with a square check. |
| trayPosition | "bottom" | "side" | "float" | "bottom" | A flush panel under the cards, a receipt column beside them, or a floating bar; narrow containers dock every one at the bottom. |
| accent | "cobalt" | "signal" | "amber" | "graphite" | string | "cobalt" | Checks, chips, the flight and the chosen cards' wash: a named accent or any CSS colour. |
| formatPrice | (amount: number) => string | US dollars | Formats prices; the total's symbol is read from formatPrice(0), so other currencies roll correctly. |
| period | string | "mo" | What the prices are per. |
| title | string | "Add-ons" | The heading. |
| checkoutLabel | string | "Checkout" | The checkout button's word. |
| status | "ready" | "loading" | "error" | "ready" | Whether the add-ons have arrived: placeholder cards while loading, a retry on error. |
| onRetry | () => void | — | Try again was pressed after the add-ons failed to load. |
| disabled | boolean | false | Holds every card and checkout. |
| className | string | — | Classes for the surface; give it a height and the cards scroll above the tray. |