data-count
Live character or word count for textareas with configurable limits. Color shifts at warning and error thresholds for visual feedback.
Overview
The data-count attribute adds a live counter below a textarea that updates as the user types. It supports both character counting (with maxlength) and word counting (with data-maxwords), with automatic color shifts at warning and error thresholds.
<form-field>\n <label for="bio">Bio</label>\n <textarea id="bio" maxlength="200" data-count rows="3"></textarea>\n</form-field>
How It Works
Add data-count to any <textarea>. The init script:
- Creates an
<output>element withclass="textarea-counter" - Sets
aria-live="polite"andaria-atomic="true"on the counter for screen reader announcements - Places the counter after the textarea (inside
<form-field>if present, otherwise directly after) - Listens for
inputevents to update the count in real time - Calculates thresholds: warning at ≥80% of limit, error at ≥100%
- Sets
counter.dataset.stateto"","warning", or"error"for CSS styling - Sets
data-count-initto prevent double-binding
Attributes
| Attribute | Values | Description |
|---|---|---|
data-count |
"", "words" |
Enables the counter. Empty string (default) counts characters. "words" counts words. |
maxlength |
number | Standard HTML attribute. Used as the character limit. The counter displays current / max format. |
data-maxwords |
number | Word limit for word-count mode. The counter displays current / max format. |
data-count-init |
boolean | Set automatically to prevent double-binding. Do not set manually. |
Character Count
The default mode counts characters. Pair with maxlength to show a current / max display and enforce the limit.
<form-field>\n <label for="bio">Bio</label>\n <textarea id="bio" maxlength="200" data-count rows="3"></textarea>\n</form-field>
Word Count
Set data-count="words" for word counting. Use data-maxwords to set a word limit. Words are split on whitespace.
<form-field>\n <label for="essay">Essay</label>\n <textarea id="essay" data-count="words" data-maxwords="50" rows="4"></textarea>\n</form-field>
Count Without Limit
Omit maxlength or data-maxwords to show a running count without enforcing a limit. No threshold states are applied.
Characters without limit
<form-field>\n <label for="notes">Notes</label>\n <textarea id="notes" data-count rows="3"></textarea>\n</form-field>
Words without limit
<form-field>\n <label for="freeform">Freeform</label>\n <textarea id="freeform" data-count="words" rows="3"></textarea>\n</form-field>
Display Format
The counter text adapts based on the configuration:
| Configuration | Display | Example |
|---|---|---|
Characters with maxlength |
current / max |
42 / 200 |
| Characters without limit | count character(s) |
42 characters |
Words with data-maxwords |
current / max |
12 / 50 |
| Words without limit | count word(s) |
12 words |
Threshold States
When a limit is set, the counter shifts color at configurable thresholds via data-state:
- Normal (
data-state="") — below 80% of the limit, uses muted text color - Warning (
data-state="warning") — at or above 80% of the limit, shifts to warning color - Error (
data-state="error") — at or above 100% of the limit, shifts to danger color with bold weight
Without a limit, no threshold states are applied and the counter stays in its default style.
Combined with data-grow
Use data-count alongside data-grow for a textarea that expands as the user types while showing the count.
<form-field>\n <label for="msg">Message</label>\n <textarea id="msg" data-count data-grow maxlength="500" rows="2"></textarea>\n</form-field>
Programmatic Access
The counter element is a standard DOM node. Query it to read the current state programmatically.
const textarea = document.querySelector('[data-count]'); textarea.addEventListener('input', () => { const counter = textarea.parentElement.querySelector('.textarea-counter'); console.log('Count state:', counter.dataset.state); console.log('Counter text:', counter.textContent);});
Styling
The counter is an <output> element styled via the .textarea-counter class. Threshold states are driven by data-state attribute selectors.
/* Counter element */.textarea-counter { font-size: var(--text-sm); color: var(--color-text-muted); text-align: right; margin-block-start: var(--size-2xs);} /* Warning state (=80% of limit) */.textarea-counter[data-state="warning"] { color: var(--color-warning);} /* Error state (=100% of limit) */.textarea-counter[data-state="error"] { color: var(--color-error); font-weight: 600;}
The warning state uses --color-warning and the error state uses --color-error with increased font weight for emphasis.
Dynamic Elements
Textareas added to the DOM after page load are automatically enhanced via a MutationObserver. No manual initialization is needed.
<section> <h2>Accessibility</h2> <ul> <li>The counter uses <code>aria-live="polite"</code> so screen readers announce count changes without interrupting the user</li> <li><code>aria-atomic="true"</code> ensures the entire counter text is read, not just the changed portion</li> <li>The <code><output></code> element has implicit ARIA semantics for live regions</li> <li>Threshold state changes (warning, error) are conveyed through the updated counter text</li> <li>The counter is placed after the textarea in DOM order, matching visual reading order</li> <li>Without JavaScript, the textarea works normally with no counter (progressive enhancement)</li> </ul> </section>