Tax-Inclusive Input

Makes "includes / excludes GST" an explicit choice and reads back net + tax = gross, so tax isn't added twice.

StopsGST added twice to a price

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/tax-inclusive-input.json

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

Usage

tax-inclusive-input-usage.tsx
import { TaxInclusiveInput } from "@/components/ui/tax-inclusive-input"

<TaxInclusiveInput label="Plan price" defaultValue={{ amount: 1180, rate: 18, includesTax: false }} />

What it catches

  • An "excludes tax" price that looks already taxed (divides back to a round number)
  • Switching mode that changed the total, with keep-the-total fix
  • Rates typed as a fraction (0.18), multiplier (1.18) or slipped digit (180)
  • Rates above 50%

Props

TaxInclusiveInput props
PropTypeDefault
label

Visible label of the amount field.

string"Price"
value

Amount, rate and includes-tax flag (controlled).

TaxPrice—
defaultValue

Starting price when uncontrolled.

TaxPrice{ amount: null, rate: 18, includesTax: true }
onValueChange

Called with the price after every change.

(value: TaxPrice) => void—
currency

ISO currency code.

string"INR"
locale

Locale for money formatting.

string"en-IN"
taxName

"GST", "VAT", "sales tax".

string"GST"
rates

Rates that are normal where you sell; others get a gentle note. Pass [] to skip that check.

number[]GST_RATES
inclusiveReadback

Templates: {net}, {tax}, {gross}, {rate}, {taxName} and {amount} are filled in.

string"{gross} incl. {rate}% {taxName} = {net} + {tax}"
exclusiveReadback

Readback when the amount excludes tax.

string"{net} + {rate}% {taxName} ({tax}) = {gross}"
includesLabel

Label of the includes-tax option.

string"Includes {taxName}"
excludesLabel

Label of the excludes-tax option.

string"Excludes {taxName}"
alreadyTaxedText

Warning when an excludes-tax amount looks already taxed.

string"{amount} is exactly {net} + {rate}%. Is {taxName} alread…
switchedText

Warning when switching mode changed the total.

string"Switching changed the total from {before} to {gross}."
keepTotalLabel

Label of the keep-the-total fix.

string"Keep {before}"
useRateLabel

Label of the use-likely-rate fix.

string"Use {rate}%"
alreadyTaxedFixLabel

Label of the treat-as-inclusive fix.

string"Yes, it includes {taxName}"

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

Accessibility

  • Visible <label> on the amount; readback via aria-describedby
  • Rate input has an aria-label; includes/excludes is a radiogroup
  • Warnings and fixes in a polite live region
  • Total highlight respects reduced motion

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

Limitations

  • Default rate list is India's main GST slabs (0, 5, 18, 40); special rates (e.g. 3% on gold, 0.25%) and tobacco still on old rates are not included
  • Rounds to the currency's minor unit; invoice rounding rules may differ
  • Does not decide which rate applies to an item

Rules

Rules last verified:

Re-checked 2026-10-17 against PIB release 2164586 (8 Sep 2025): the 56th GST Council moved GST from four slabs (5/12/18/28%) to two main rates, 5% (merit) and 18% (standard), plus a 40% special rate for sin/luxury goods, effective 22 Sep 2025. The default list [0, 5, 18, 40] (0 = nil-rated) matches; special rates such as 3% on gold and 0.25% on rough diamonds, and tobacco items still on 28% plus cess, are not in it. Informational; check CBIC rate notifications for an item's rate. Copied components don't update themselves, so re-check after rule changes (see updating a component).

What it stops

GST added twice to a price

Also known as

gst · tax inclusive · tax exclusive · mrp · price · invoice · vat · net gross

Field type

Dependencies

Pairs well with