shadcn.io is not affiliated with official shadcn/ui
Credit Refund Request Billing Block for Svelte
A prepaid shipping-label service's Credit packs page with its refund dialog shown open: three packs with a checkbox each, packs that fail the rule disabled with the reason, a refundable total worked out in cents, a destination choice and an optional reason.
Installation
Overview
Let customers see what they can get back before they ask, with this refund request dialog for Svelte with Ark UI. It opens from the Credit packs page of Crateholm, a prepaid shipping-label service: the header holds an outline Request a refund Button, and the page itself is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The dialog states its rule in one line (30 days, at least half unused, minus 5% handling) and lists three packs with a checkbox each: the refundable ones show their amount, and a pack that fails the rule is disabled with the reason in words, such as "Bought 41 days ago". Ticking packs works out the total, the cents shared by largest remainder, and the Shine Button names it ("Request $40.85"). Below, two choices say where the money goes (back to Visa ending 4242 in 5 to 10 days, or as account credit now) and an optional Textarea takes a reason. For preview the dialog is drawn open in place when the page loads; the Request a refund button opens the real Dialog, with the same fields and the same state. For prepaid products, credits and any shop that refunds against a rule.
FAQ
Why is the dialog already open, and what do I keep?
So you can see it without a click. On load the dialog is a copy drawn in place over the page: a native dialog element that isn't modal, so nothing is trapped, hidden or focused, and the page around the preview keeps working. The Request a refund button opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same RefundFields component and share the block's state. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.
How is the refundable amount worked out?
For each pack: the price times the unused share times 95%. The total is the sum rounded to the cent, and the cents are shared out among the ticked packs by largest remainder, so the rows always add up to the total on the button. A pack is refusable for two reasons, both written on its row: more than 30 days since purchase (counted from a fixed date), or more than half used. Edit PACKS, WINDOW and HANDLING to change the rule.
What stays local, and how do I make it real?
Requesting marks the ticked packs as requested in this page's state, draws them with a dashed pill in the packs table and says "Nothing was refunded in this demo." The card is shown only as "Visa ending 4242". In your app send the pack ids and the destination to your server, recompute the amounts there, and let your payment provider move the money.
What if nothing can be ticked?
Request refund stays pressable but unavailable: pressing it, or Enter, shows "Choose at least one pack that can be refunded" under the list and moves focus to the first pack that can be, so a keyboard user always lands somewhere useful. Packs that were already requested become disabled with "Requested".
Is it accessible from the keyboard?
Yes. The real dialog is named by its title and described by its sentence, focus starts on the first refundable pack and stays inside until it closes, and Escape, Cancel and the close button all return focus to Request a refund. The packs are native checkboxes and the destination native radio buttons, so Space and the arrow keys work as the browser defines. The trigger is the button wrapped in DialogTrigger with asChild, and the dialog is controlled with open and onOpenChange.
How does it look on a phone?
The copy shown on load docks to the foot of the page as a bottom sheet, its actions pinned at the bottom with the request button on top, and the packs and fields scroll above them. The real dialog keeps the Dialog's own phone layout: a sheet stuck to the bottom of the screen.