CharacterCounter
MD · 1950B gzip budgetA character counter that counts graphemes, so one emoji costs one, not eleven.
- Category
- Forms & input
- Budget
- md, several states or a live subscription
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixture7 of 20
Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Why this exists
CharacterCounter
EnglishString#length counts code units. A user types one emoji and watches the counter jump by eleven, with no explanation.
Formatting shortcut
31 of 40
value.lengthInstall
npx shadcn@latest add https://gear5-ui.vercel.app/r/character-counter.jsonCopies the source into your project. Pulls in 4 shared registry items: cn, format, use-locale, locale.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
"use client";
import { useMemo } from "react";
import { cn } from "../lib/cn";
import { numberFormat } from "../lib/format";
import { useLocale } from "../lib/use-locale";
export interface CharacterCounterProps {
value: string;
limit: number;
/** `{count}` and `{limit}` are replaced with localised numbers. */
template?: string;
/** Announced when the limit is exceeded. `{over}` is the overage. */
overTemplate?: string;
className?: string;
}
/** Count user-perceived characters, not UTF-16 code units. */
function countGraphemes(text: string, locale: string): number {
if (typeof Intl.Segmenter !== "function") return [...text].length;
const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" });
return [...segmenter.segment(text)].length;
}
/**
* A character counter that counts the way the person typing counts.
*
* `value.length` counts UTF-16 code units, so a single emoji costs 2 and a
* family emoji costs 11 — a user types one character and watches the counter
* jump by eleven, with no explanation. Devanagari and Thai clusters have the
* same problem.
*
* The count is a live region so it is available to screen readers, but polite
* and only announced meaningfully near the limit; a region that speaks on
* every keystroke makes a field unusable.
*/
export function CharacterCounter({
value,
limit,
template = "{count} of {limit}",
overTemplate = "{over} over the limit",
className,
}: CharacterCounterProps) {
const { locale, direction } = useLocale();
const count = useMemo(() => countGraphemes(value, locale), [value, locale]);
const over = count - limit;
const format = (n: number) => numberFormat(locale).format(n);
const text =
over > 0
? overTemplate.replace("{over}", format(over))
: template.replace("{count}", format(count)).replace("{limit}", format(limit));
return (
<p
dir={direction}
// Only assertive once the limit is actually breached: before that this
// is reference information, not something worth interrupting typing for.
role={over > 0 ? "alert" : "status"}
aria-live={over > 0 ? "assertive" : "off"}
className={cn(
"text-start text-sm tabular-nums",
over > 0 ? "text-red-700 dark:text-red-400" : "text-neutral-600 dark:text-neutral-400",
className,
)}
>
{text}
</p>
);
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation