A visual character counter web component with a circular progress indicator, inspired by Bluesky's post composer.
- No Shadow DOM
- ARIA labels and screen reader support
npm i -S @substrate-system/character-counterThis calls the global function customElements.define. Just import,
then use the tag in your HTML.
Note
The tag name is exposed at CharacterCounter.TAG
import { CharacterCounter } from '@substrate-system/character-counter'
document.querySelector(CharacterCounter.TAG) // 'character-counter' tagimport '@substrate-system/character-counter'import '@substrate-system/character-counter/css'Or minified:
import '@substrate-system/character-counter/min/css'<character-counter max="300" count="50"></character-counter>Update the count attribute as the user types:
const textarea = document.querySelector('textarea')
const counter = document.querySelector('character-counter')
textarea.addEventListener('input', () => {
counter.setAttribute('count', textarea.value.length)
})Or use the setter:
counter.count = textarea.value.length| Attribute | Type | Default | Description |
|---|---|---|---|
max |
number | 300 |
Maximum character count |
count |
number | 0 |
Current character count |
hide-count |
boolean | false |
When present, never shows the remaining count text |
warn |
boolean | number | false |
When present as boolean, shows count when within 20 of limit; when set to a number, shows count when that many characters remain (ignored if hide-count is present) |
The component's text display behavior depends on these attributes:
- Default (no attributes): Always shows the remaining count
- With
warn(boolean): Only shows count when 20 or fewer characters remain - With
warn="N"(number): Only shows count when N or fewer characters remain - With
hide-count: Never shows the count (takes precedence overwarn)
<!-- Always show count -->
<character-counter max="300" count="50"></character-counter>
<!-- Only show count when near limit (20 characters) -->
<character-counter max="300" count="285" warn></character-counter>
<!-- Only show count when 50 or fewer characters remain -->
<character-counter max="300" count="200" warn="50"></character-counter>
<!-- Never show count -->
<character-counter max="300" count="50" hide-count></character-counter>Sets data attributes based on state:
| Attribute | Description |
|---|---|
data-over-limit |
Present when count exceeds max |
data-hide-count |
Present when count text should be hidden (uses visibility: hidden to prevent layout shift) |
Customize the appearance using CSS variables:
| Property | Default | Description |
|---|---|---|
--counter-diameter |
2rem |
Circle diameter (supports any CSS unit) |
--counter-stroke-width |
3 |
Circle stroke width in pixels |
--counter-track-color |
#e0e0e0 |
Background ring color |
--counter-normal-color |
#1d9bf0 |
Progress color when under limit |
--counter-warning-color |
#f4212e |
Progress color when over limit |
--counter-text-color |
#536471 |
Remaining count text color |
character-counter {
--counter-diameter: 3rem;
--counter-stroke-width: 5;
}character-counter {
--counter-normal-color: #059669;
--counter-warning-color: #dc2626;
--counter-track-color: #f3f4f6;
}<character-counter
max="280"
count="0"
style="--counter-diameter: 32px; --counter-normal-color: purple"
>
</character-counter>- Progress indicator: The circular ring fills as
countapproachesmax - Number display: The remaining count appears to the left of the circle (controlled by
hide-countandwarnattributes) - Over-limit state: When
count>max, the component turns red and shows a negative number (if count display is enabled) - Accessibility: Announces character count to screen readers via ARIA live regions
This exposes ESM and common JS via
package.json exports field.
import '@substrate-system/character-counter'
// Named import
import { CharacterCounter } from '@substrate-system/character-counter'require('@substrate-system/character-counter')The component includes TypeScript definitions and extends the
global HTMLElementTagNameMap.
// Type-safe querySelector
const counter = document.querySelector('character-counter')
// counter is typed as CharacterCounter | null
// Set count via property
counter.count = 42
// Get computed values
console.log(counter.remaining) // number
console.log(counter.isOverLimit) // boolean
console.log(counter.hideCount) // boolean
console.log(counter.warn) // boolean | number
console.log(counter.shouldShowCount) // booleanThis package exposes minified JS and CSS files. Copy them to a location accessible to your web server, then link to them in HTML.
cp ./node_modules/@substrate-system/character-counter/dist/index.min.js ./public/character-counter.min.js
cp ./node_modules/@substrate-system/character-counter/dist/style.min.css ./public/character-counter.css<head>
<link rel="stylesheet" href="./character-counter.css">
</head>
<body>
<character-counter max="300" count="0"></character-counter>
<script type="module" src="./character-counter.min.js"></script>
</body>