Tip Picker

Shows tips in money on a stated base and holds back accidental huge tips, like 250 typed in the % field.

StopsTipped 250% by typo

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/tip-picker.json

The files land in your project. They're yours to edit.

Usage

tip-picker-usage.tsx
import { TipPicker } from "@/components/ui/tip-picker"

<TipPicker
  subtotal={2000}
  tax={100}
  serviceCharge={{ label: "A service charge", amount: 200 }}
  defaultCustomText="250"
  defaultCustomMode="percent"
/>

What it catches

  • An amount typed in the percent field (250%)
  • A percent typed in the money field (₹15 for 15%)
  • Tips at or above highPercent
  • A service charge already on the bill
  • Pre-tax vs post-tax base, stated and switchable

Props

TipPicker props
PropTypeDefault
subtotal* (required)

Items total before tax and service charge.

number—
tax

Tax on the bill.

number0
serviceCharge

A service charge already on the bill, e.g. `{ label: "Service charge (10%)", amount: 200 }`.

{ label: string; amount: number }—
presets

Preset percentages.

number[][10, 15, 18, 20]
base

Controlled tip base.

TipBase—
defaultBase

Initial tip base when uncontrolled. Etiquette and most guidance say pre-tax.

TipBase"pre-tax"
onBaseChange

Called when the tip base (pre- or post-tax) changes.

(base: TipBase) => void—
allowBaseSwitch

Hide the pre-tax / post-tax switch when the merchant fixes the base.

booleantrue
value

Controlled tip amount. A flagged custom tip is only reported once it's fixed or confirmed.

number—
defaultValue

Starting tip amount when uncontrolled.

number0
onValueChange

Called with the tip amount.

(amount: number) => void—
defaultCustomText

Custom text the picker opens with, e.g. a value restored from a draft.

string""
defaultCustomMode

Whether a custom tip starts in percent or money.

TipMode"percent"
highPercent

Tips at or above this percent of the base need a second tap.

number30
currency

ISO 4217 currency code.

string"INR"
locale

Locale for money formatting.

string"en-IN"
legend

Legend of the tip fieldset.

string"Add a tip"
noTipLabel

Label of the no-tip option.

string"No tip"
customLabel

Label of the custom-tip option.

string"Custom"
includedText

Notice when a service charge is already on the bill.

(charge: string, percent: string) => string(charge, percent) => `${charge} (${percent}) is already o…
amountInPercentText

Receives the typed percent, the tip it gives and the likely meant amount.

(typed: string, tip: string, meant: string) => string(typed, tip, meant) => `${typed} is a ${tip} tip. Did you…
percentInAmountText

Warning when a percent was typed as money.

(typed: string, percent: string) => string(typed, percent) => `${typed} is a very small tip. Did yo…
highText

Warning for an unusually large tip.

(amount: string, percent: string) => string(amount, percent) => `${amount} is ${percent} of the bill.`
keepLabel

Label of the keep-this-tip confirmation.

(amount: string) => string(amount) => `Yes, tip ${amount}`
baseGroupLabel

Accessible name of the pre-tax / post-tax choice.

string"Tip is calculated on"
customTipLabel

Accessible label of the custom tip field.

string"Custom tip"
customUnitLabel

Accessible name of the % / amount choice for a custom tip.

string"Custom tip unit"

* required. Also accepts the native props of <fieldset> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Fieldset with a legend; base switch is a radiogroup
  • Presets are aria-pressed buttons showing money amounts
  • Custom input has a <label> and a hint via aria-describedby
  • Rolling figures respect reduced motion

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Customs differ by country; presets and highPercent are yours to set
  • Rounds in the given currency; the payment system is the source of truth

What it stops

Tipped 250% by typo

Also known as

tip · gratuity · service charge · checkout · food delivery · restaurant bill · percentage · zomato

Field type

Dependencies

Pairs well with