Character Counter
Fixed-height text area with an "n / 150" counter tucked into its bottom-right corner, amber at 80% of the limit, red once it is reached.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Forms has 20 effects, including 2 free. Explore the category →
3 usage examples



How it works
The counter is a <span> positioned absolutely in the block's bottom-right corner, over the area's bottom padding (30 px kept free). On every input event, the script writes "length / maximum", the maximum coming from the maxlength attribute (150 shipped); changing that attribute is enough for a 160-character text message or a 45-character question.
At 80% of the maximum (120 of 150) the counter gets the is-warning class, amber; at the limit, is-limit, red. The area keeps its height (100 px minimum): the user can enlarge it vertically with the handle (resize: vertical), otherwise the text scrolls.
The <textarea> tag is empty in the shipped HTML: the placeholder shows and the counter starts at 0 (keep both tags touching if you edit it). No fixed ids: two areas on the same page count separately, and the field has a name sent with the form.
Accessibility
- Label: "Message" in a visually hidden
<label>, to rename for your use. - Linked counter:
aria-describedbypoints to the counter, read when entering the area. - Measured announcements: an
aria-live="polite"region speaks when entering amber ("30 characters left") then at the limit, never on every key. - Motion: no animation apart from the border colour change on focus.
- Limit: if the text runs past the visible height, its last lines can slide under the counter. Pick a height (
--chct-min-height) that holds the whole limit.
Browser compatibility
1.8 KB of JavaScript, CSS with variables. Measured on 08/11/2026 in Chromium, Firefox and WebKit (Playwright): empty starting value, placeholder shown, "1 / 150" after one key, 150 / 150 in red after an over-long paste, two independent copies, touch at 390 px, no errors. Floor set by NodeList.forEach (Chrome 51, Firefox 50, Safari 10, according to @mdn/browser-compat-data).
Without JavaScript, the counter stays on "0 / 150" but the browser still enforces maxlength.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0299-root">
<div class="chct-group">
<label class="chct-field">
<span class="chct-sr">Message</span>
<textarea class="chct-input" name="message" placeholder="Type your message..." maxlength="150"></textarea>
</label>
<span class="chct-count">0 / 150</span>
<span class="chct-sr chct-status" aria-live="polite"></span>
<span class="chct-words" hidden>
<span data-msg="warning">{n} characters left</span>
<span data-msg="limit">{max}-character limit reached</span>
</span>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Limit (HTML — maxlength on the area) |
150 | The counter text and thresholds follow; also put the right value in the initial text of .chct-count for display without JavaScript. |
Amber threshold (JS — max * 0.8) |
80% | 0.9 to warn only in the last 10%. |
Counter colours (CSS — --chct-muted, --chct-warning, --chct-limit on .fx-0299-root) |
#a1a1aa · #f59e0b · #ef4444 | On a light background: #6b7280, #b45309, #dc2626. |
Height (CSS — --chct-min-height) |
100px | Pick a height that holds the whole limit so the text never slides under the counter. |
Field (CSS — --chct-field, --chct-border, --chct-focus, --chct-text) |
transparent · 30% indigo · #6366f1 · #fff | Background, border at rest and on focus, text colour. |
Width and frame (CSS — --chct-width, --chct-bg, --chct-root-min-height) |
280px · #0a0a0f · 300px | On a page: --chct-width:100%; --chct-bg:transparent; --chct-root-min-height:0. |
Announced messages (HTML — .chct-words) |
"{n} characters left" · "{max}-character limit reached" | {n} = characters left, {max} = limit. |
FAQ
reset, yes, automatically. After a value set by a script, fire textarea.dispatchEvent(new Event('input', {bubbles: true})): that is what the template button does in the support example.value.length, like maxlength: a common emoji counts as 2, some as more. Counter and limit therefore agree with each other, but not necessarily with a text-message carrier's count.