Chinese Localization

CJK Word Count Textarea

A React textarea with CJK-aware counting: switch live between word, character and byte modes, a limit progress ring, near-limit and over-limit warnings, frozen counts while an IME is composing, and an optional hard limit.

A Chinese "word count" is not the same thing as an English one: every Han character counts as one, while an English run of letters counts as one word as a whole — and databases or legacy systems often count bytes instead. This textarea puts all three counting modes into a segmented switch at the bottom right and shows "current / limit" live next to an SVG progress ring that turns amber past the warnAt ratio and red, with a quick shake, once the limit is exceeded. While an IME (Zhuyin, kana and so on) is composing, the count is frozen so composition letters are never counted; with hardLimit on, input past the limit is blocked, but trimming waits until composition ends so a character being composed is never cut in half. Both the text value and the counting mode support controlled and uncontrolled modes.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add cjk-word-count-textarea.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

60
0.8
Active edited usageDefault props
<CjkWordCountTextarea/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/cjk-word-count-textarea.en.json

Or, once registries are configured in components.json, install it as @webberui/cjk-word-count-textarea.

Usage

import { CjkWordCountTextarea } from "@/components/ui/cjk-word-count-textarea";

<CjkWordCountTextarea
  label="Post content"
  maxCount={140}
  defaultMode="words"
  autoResize
  onChange={(text, counts) => {
    // counts carries the numbers for all three modes
    console.log(counts.words, counts.chars, counts.bytes);
  }}
/>

Props

PropTypeDefaultDescription
valuestring—Controlled text value; when omitted the component is uncontrolled
defaultValuestring""Initial text in uncontrolled mode
onChange(text: string, counts: CjkCounts) => void—Callback when the text changes; the second argument holds the live counts for all three modes
maxCountnumber—Count limit (in the current mode's units); when omitted only the count is shown and no ring is drawn
mode"words" | "chars" | "bytes"—Controlled counting mode, used together with onModeChange
defaultMode"words" | "chars" | "bytes""words"Initial counting mode in uncontrolled mode
onModeChange(mode: CjkCountMode) => void—Callback when the counting mode changes
warnAtnumber0.8Ratio (0–1) at which the amber warning state starts
rowsnumber4Number of visible textarea rows
autoResizebooleanfalseWhether the height grows with the content (based on scrollHeight)
labelstring—Field label; when omitted the placeholder is used as the accessible name
placeholderstring"輸入內容⋯"Placeholder text (default reads "type something…")
hardLimitbooleanfalseBlock input past the limit; nothing is trimmed while an IME is composing, only once composition ends
countOptionsCountCjkOptions—Counting options passed to countCjk (full-width punctuation, Big5 byte weighting)
labelsCjkWordCountLabels—UI text overrides (mode labels and warning announcements)
classNamestring—Forwarded to the outermost container and merged with cn()

CjkCounts

FieldTypeDefaultDescription
wordsnumber—CJK-style word count: each Han character counts as one, Latin runs are split on whitespace
charsnumber—Character count, by Unicode code point
bytesnumber—Byte count: UTF-8 length, or weighted by bytesPerCjk

CountCjkOptions

FieldTypeDefaultDescription
countPunctuationbooleanfalseIn words mode, also count each full-width punctuation mark (,。!?…) as one
bytesPerCjknumber—In bytes mode, a fixed byte weight for every CJK character (including full-width punctuation); when omitted the real UTF-8 length is used

CjkWordCountLabels

FieldTypeDefaultDescription
wordsstring"字數"Words mode label (default reads "words")
charsstring"字元"Chars mode label (default reads "characters")
bytesstring"位元組"Bytes mode label (default reads "bytes")
modeGroupstring"計數模式"Accessible name of the mode switch group (default reads "counting mode")
nearLimitstring"接近上限"Screen reader announcement prefix when crossing warnAt (default reads "near the limit")
overLimitstring"已超過上限"Screen reader announcement prefix when over the limit (default reads "over the limit")

countCjk(text, mode, opts) and clampCjk(text, max, mode, opts) are also named exports, so form validation or server code can count and trim with exactly the same rules; both are pure functions with no browser API dependency.

How it works

  • Words mode: each Han character (plus Japanese kana and Korean Hangul) counts as one; runs of Latin letters, digits and the like are split on whitespace or CJK characters and count as one word each; full-width punctuation is not counted by default, or counted one by one with countPunctuation: true. For example, "我買了 3 本 React 書" counts as 7.
  • Chars mode counts Unicode code points (Array.from), so surrogate pairs such as emoji are never split in two.
  • Bytes mode defaults to UTF-8 length (most Han characters are 3 bytes); many legacy systems in Taiwan count in Big5 (2 bytes per Han character), which you can match with countOptions={{ bytesPerCjk: 2 }}.
  • The hard limit uses binary search to find the longest valid prefix: the count only grows as the prefix grows, so there is no need to recount character by character.
  • autoResize resets the height before reading scrollHeight, so the field shrinks back when content is removed.

Accessibility

  • The label is associated with the textarea via htmlFor; without a label, the placeholder becomes the aria-label
  • The count text is linked to the textarea via aria-describedby; aria-invalid is set when over the limit
  • The mode switch is a role="radiogroup" whose tabs are role="radio" with aria-checked, using a roving tabindex; arrow keys cycle through modes, with a focus-visible ring
  • Warnings live in a role="status", aria-live="polite" region and are announced only once, at the moment warnAt or the limit is crossed, rather than on every keystroke; the progress ring and warning icon are hidden from assistive technology
  • When the user has reduce motion enabled at the system level, the over-limit shake and the sliding mode pill animation are disabled and the state switches directly