Character count
StableA textarea that counts down to its limit, GOV.UK style: it never cuts text off, and speaks the count when typing pauses.
The limit is stated up front for screen readers (“You can enter up to 160 characters”) and the visible count changes as you type. With threshold, the count waits until that many characters are used (GOV.UK shows it from 75% of the limit). Going over is allowed: the count says how many characters are too many and the field turns invalid, so the form can report it on submit. Characters are counted the way people count them, so an accented letter is one however it was typed, and numbers read “2,000”.
Short bio
import { useState } from 'react'import { CharacterCount } from '@rchursin/product/forms'import { Field } from '@rchursin/product/ui'
/** Counts down as you type. Screen readers hear the limit up front, and the count once typing pauses. */export default function CharacterCountBio() { const [bio, setBio] = useState('Platform lead. I look after our coding agents and the skills they run.')
return ( <div className="w-full max-w-md"> <Field label="Short bio" hint="Shown next to your name in reports." optional> {(p) => <CharacterCount {...p} name="bio" value={bio} onChange={setBio} maxLength={160} />} </Field> </div> )}Usage
import { CharacterCount, type CharacterCountProps } from '@rchursin/product/forms'<Field label="Short bio" optional> {(p) => <CharacterCount {...p} value={bio} onChange={setBio} maxLength={160} />}</Field>Examples
Over the limit, with a threshold
Nothing is cut off: the count says how many characters to remove, with an icon and heavier weight. threshold hides the count again once the summary is under 38 characters, 75% of the limit.
import { useState } from 'react'import { CharacterCount } from '@rchursin/product/forms'import { Field } from '@rchursin/product/ui'
/** * Going over is allowed and nothing is cut off: the count says how many to remove, and the field * reads as invalid. threshold shows the count only from 75% of the limit, as GOV.UK does. */export default function CharacterCountOverLimit() { const [summary, setSummary] = useState('Narrow the test-fixer file glob and add a retry budget for slow suites')
return ( <div className="w-full max-w-md"> <Field label="Commit summary" hint="One line. Say what the change does."> {(p) => <CharacterCount {...p} value={summary} onChange={setSummary} maxLength={50} threshold={38} minRows={2} />} </Field> </div> )}API
Character count props
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | – | The text. |
onChangerequired | (value: string) => void | – | Called with the new text, even past the limit. |
maxLengthrequired | number | – | The limit. It is not set on the textarea, so typing past it still works. |
threshold | number | – | Show the count, and speak it, only once this many characters are used, e.g. Math.round(maxLength * 0.75). The limit sentence is always there. Default: from the first character. |
onBlur | (e: FocusEvent<HTMLTextAreaElement>) => void | – | Called when the textarea loses focus, e.g. useForm’s blur check. |
minRows | number | 3 | Starting height in lines; the field grows with its content. |
maxRows | number | 12 | The height stops growing here and the field scrolls (as Textarea). |
placeholder | string | – | Placeholder text. |
name | string | – | The form field name. |
disabled | boolean | – | Dims the field. |
readOnly | boolean | – | Focusable, readable, not editable; the field turns sunken. |
id | string | – | id of the textarea. Set by Field’s render prop ({...p}), along with aria-describedby and aria-invalid. |
aria-invalid | boolean | – | From Field when there is an error. The field also turns invalid on its own when over the limit. |
Accessibility
- A visually hidden sentence, “You can enter up to 160 characters”, is added to the textarea’s aria-describedby.
- The visible count is hidden from screen readers. A polite live region repeats it 0.9 seconds after typing stops, so it is not read out on every key, nor when the page loads. Below threshold it stays silent too.
- Over the limit the textarea gets aria-invalid, and the count gains an alert icon and heavier weight: shape and weight, not colour.
Guidelines
Do
- Use it only when there is a real limit, such as a database column or a notification.
- Report going over on submit, in the same words as the count.
Don’t
- Don’t cut text off at the limit; people paste and then edit down.
- Don’t use it to nudge people to write less when there is no limit.
In use
Related
- TextareaA multi-line field that grows with what is typed, up to a maximum, then scrolls.
- FieldLabel above, hint under it, error in words between the hint and the control, with the ids and aria wiring done for you.
- Form toolkituseForm for values, errors and validation timing, plus the checks and GOV.UK-style messages every form in the kit shares.
Source
The component’s own files, exactly as they run on this site.
/** * Textarea with a character count (GOV.UK character-count pattern). * * - The limit is stated up front for screen readers ("You can enter up to 2,000 characters"). * - The visible count ("42 characters remaining" / "3 characters too many") updates as you type; * screen readers get the same sentence once typing pauses, not on every keystroke. * - With `threshold`, the count only appears once that many characters are used (GOV.UK shows it * from 75% of the limit), so a long limit doesn't nag from the first letter. * - Going over is allowed (nothing is cut off); the form reports it on submit. */import { useEffect, useId, useRef, useState, type ChangeEvent, type FocusEvent } from "react"import { cx } from "../../components/cx"import { Icon } from "../icons/Icon"import { Textarea } from "../ui/fields"import { charLength } from "./validation"
export interface CharacterCountProps { id?: string value: string onChange: (value: string) => void