SMS Segments

Shows how many billed SMS parts a text becomes and names the character (curly quote, emoji) that forced Unicode and raised the count.

StopsOne emoji doubled the SMS bill

Installation

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

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

Usage

sms-segments-usage.tsx
import { SmsSegments } from "@/components/ui/sms-segments"

<SmsSegments rows={5} label="Order update SMS" defaultValue={draft} recipients={recipients} pricePerSegment={0.2} currency="INR" locale="en-IN" />

What it catches

  • Characters outside GSM-7 that force UCS-2 (curly quotes, emoji, look-alikes)
  • Extension characters (€, [, ~) that use two septets
  • Part count and cost per recipient count
  • Never splits a 2-septet character or surrogate pair across parts
  • One-tap replace or remove of each culprit

Props

SmsSegments props
PropTypeDefault
label

Visible label.

string"Message"
value

Controlled text.

string—
defaultValue

Initial text when uncontrolled.

string""
onValueChange

Called on every edit, including one-tap fixes.

(value: string) => void—
recipients

How many people receive it; every part is billed per recipient.

number1
pricePerSegment

Price of one part to one recipient. Omit to hide money and show message counts only.

number—
currency

ISO currency code for the price.

string"USD"
locale

Locale for numbers and money; defaults to the browser's.

string—
oneCulpritMessage

Shown when one character is the cause. `{name}` ("curly apostrophe"), `{char}`, `{count}`, `{plain}` are filled in.

string"The {name} ({char}) makes it {count} instead of {plain}."
manyCulpritsMessage

Shown when several characters are the cause.

string"These characters make it {count} instead of {plain}."
freeUnicodeMessage

Shown when Unicode costs nothing extra yet.

string"Sent as Unicode because of the {name} ({char}). Still {c…
fixAllLabel

Label of the button that fixes every culprit at once.

string"Fix all"
replaceLabel

Accessible names of the one-tap fixes; `{name}` and `{to}` are filled in.

string"Replace {name} with {to}"
removeLabel

Accessible label of a remove-character fix. `{name}` is filled in.

string"Remove {name}"
removeText

Shown in a fix chip when the character is simply dropped.

string"remove"
messageText

Message counts: `{n}` is filled in.

string"{n} message"
messagesText

Count label for several messages.

string"{n} messages"
partsLabel

Accessible name of the part meter, and its captions. `{capacity}` and `{left}` are filled in.

string"Message parts"
perPartText

Per-part capacity text.

string"{capacity} per part · tap a part to see it"
onePartText

Shown when the text fits one part.

string"{capacity} fit in one message"
leftText

Characters left in the current part.

string"{left} left"
costText

Cost readback: `{messages}`, `{recipients}`, `{sms}` and `{price}` are filled in.

string"{messages} × {recipients} = {sms} SMS"
pricedCostText

Total cost line when a price is set.

string"{messages} × {recipients} = {sms} SMS · {price}"
fixedText

Saving after the fix: `{sms}` and `{saving}` are filled in.

string"Fixed: {sms} SMS"
pricedFixedText

Cost after fixing, when a price is set.

string"Fixed: {sms} SMS, saves {saving}"
recipientText

Recipient counts: `{n}` is filled in.

string"{n} recipient"
recipientsText

Recipient count label for several recipients.

string"{n} recipients"

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

Accessibility

  • Visible <label>; meter and warning linked via aria-describedby
  • Parts are buttons in a labelled group, each with an aria-label to select it
  • Warning renders in a polite live region; each fix has an aria-label
  • Highlight animation skipped under reduced motion

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

Limitations

  • Assumes standard concatenation; some carriers or national shift tables differ
  • Cost uses the price you pass; carriers may bill differently
  • Replacing characters changes the text the recipient sees

Rules

Rules last verified:

Confirmed: 160 GSM 7-bit characters in 140 octets and the default alphabet extension table. The 153/67 per-part figures come from the concatenation header in 3GPP TS 23.040, not re-checked this round. Copied components don't update themselves, so re-check after rule changes (see updating a component).

What it stops

One emoji doubled the SMS bill

Also known as

sms · character count · gsm-7 · ucs-2 · segments · bulk sms · otp sms · twilio

Field type

Dependencies

Pairs well with