30 CSS Badges20 / 30
Reading Time Indicator
Three states — not started, in progress, done. A 2px bar at the bottom fills to 100% on hover so the user can see what completion looks like. Clean type, no decoration.
Published
The code
<div class="bdg-20-stage">
<div class="bdg-20-stack">
<div class="bdg-20-badge" style="--bdg-20-progress: 0%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">2 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-unread"><span class="bdg-20-dot"></span>Not started</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 55%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">7 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-progress"><span class="bdg-20-dot"></span>In progress</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 100%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">14 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-done"><span class="bdg-20-dot"></span>Finished</div>
</div>
<div class="bdg-20-hint">↑ hover any badge to complete it</div>
</div>
</div><div class="bdg-20-stage">
<div class="bdg-20-stack">
<div class="bdg-20-badge" style="--bdg-20-progress: 0%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">2 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-unread"><span class="bdg-20-dot"></span>Not started</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 55%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">7 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-progress"><span class="bdg-20-dot"></span>In progress</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 100%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">14 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-done"><span class="bdg-20-dot"></span>Finished</div>
</div>
<div class="bdg-20-hint">↑ hover any badge to complete it</div>
</div>
</div>.bdg-20-stage {
background: #fafaf7;
padding: 60px 48px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100vh;
}
.bdg-20-stack {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.bdg-20-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 16px 10px 12px;
border: 1.5px solid #1a1612;
font-family: system-ui, "Bricolage Grotesque", sans-serif;
position: relative;
overflow: hidden;
cursor: default;
}
.bdg-20-badge::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: #1a1612;
width: var(--bdg-20-progress, 0%);
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.bdg-20-badge:hover::after {
width: 100% !important;
}
@media (prefers-reduced-motion: reduce) {
.bdg-20-badge::after {
transition: none;
}
}
.bdg-20-icon {
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.bdg-20-main {
display: flex;
flex-direction: column;
gap: 2px;
}
.bdg-20-duration {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.01em;
color: #1a1612;
line-height: 1;
}
.bdg-20-label {
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #4a4239;
line-height: 1;
}
.bdg-20-divider {
width: 1px;
height: 28px;
background: #cfc6b1;
flex-shrink: 0;
}
.bdg-20-status {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
white-space: nowrap;
line-height: 1.2;
}
.bdg-20-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
vertical-align: middle;
margin-right: 4px;
margin-bottom: 1px;
}
.bdg-20-status-done {
color: #1d8f4a;
}
.bdg-20-status-progress {
color: #b8442d;
}
.bdg-20-status-unread {
color: #aaa;
}
.bdg-20-hint {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 9px;
letter-spacing: 0.3em;
text-transform: uppercase;
color: #bbb;
text-align: center;
margin-top: 8px;
}.bdg-20-stage {
background: #fafaf7;
padding: 60px 48px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100vh;
}
.bdg-20-stack {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.bdg-20-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 16px 10px 12px;
border: 1.5px solid #1a1612;
font-family: system-ui, "Bricolage Grotesque", sans-serif;
position: relative;
overflow: hidden;
cursor: default;
}
.bdg-20-badge::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: #1a1612;
width: var(--bdg-20-progress, 0%);
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.bdg-20-badge:hover::after {
width: 100% !important;
}
@media (prefers-reduced-motion: reduce) {
.bdg-20-badge::after {
transition: none;
}
}
.bdg-20-icon {
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.bdg-20-main {
display: flex;
flex-direction: column;
gap: 2px;
}
.bdg-20-duration {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.01em;
color: #1a1612;
line-height: 1;
}
.bdg-20-label {
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #4a4239;
line-height: 1;
}
.bdg-20-divider {
width: 1px;
height: 28px;
background: #cfc6b1;
flex-shrink: 0;
}
.bdg-20-status {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
white-space: nowrap;
line-height: 1.2;
}
.bdg-20-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
vertical-align: middle;
margin-right: 4px;
margin-bottom: 1px;
}
.bdg-20-status-done {
color: #1d8f4a;
}
.bdg-20-status-progress {
color: #b8442d;
}
.bdg-20-status-unread {
color: #aaa;
}
.bdg-20-hint {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 9px;
letter-spacing: 0.3em;
text-transform: uppercase;
color: #bbb;
text-align: center;
margin-top: 8px;
}Here's a working CSS Badge from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Reading Time Indicator
Source: https://codefronts.com/snippets/css-badges/reading-time-indicator/
Three states — not started, in progress, done. A 2px bar at the bottom fills to 100% on hover so the user can see what completion looks like. Clean type, no decoration.
## HTML
```html
<div class="bdg-20-stage">
<div class="bdg-20-stack">
<div class="bdg-20-badge" style="--bdg-20-progress: 0%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">2 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-unread"><span class="bdg-20-dot"></span>Not started</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 55%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">7 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-progress"><span class="bdg-20-dot"></span>In progress</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 100%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">14 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-done"><span class="bdg-20-dot"></span>Finished</div>
</div>
<div class="bdg-20-hint">↑ hover any badge to complete it</div>
</div>
</div>
```
## CSS
```css
.bdg-20-stage {
background: #fafaf7;
padding: 60px 48px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100vh;
}
.bdg-20-stack {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.bdg-20-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 16px 10px 12px;
border: 1.5px solid #1a1612;
font-family: system-ui, "Bricolage Grotesque", sans-serif;
position: relative;
overflow: hidden;
cursor: default;
}
.bdg-20-badge::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: #1a1612;
width: var(--bdg-20-progress, 0%);
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.bdg-20-badge:hover::after {
width: 100% !important;
}
@media (prefers-reduced-motion: reduce) {
.bdg-20-badge::after {
transition: none;
}
}
.bdg-20-icon {
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.bdg-20-main {
display: flex;
flex-direction: column;
gap: 2px;
}
.bdg-20-duration {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.01em;
color: #1a1612;
line-height: 1;
}
.bdg-20-label {
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #4a4239;
line-height: 1;
}
.bdg-20-divider {
width: 1px;
height: 28px;
background: #cfc6b1;
flex-shrink: 0;
}
.bdg-20-status {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
white-space: nowrap;
line-height: 1.2;
}
.bdg-20-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
vertical-align: middle;
margin-right: 4px;
margin-bottom: 1px;
}
.bdg-20-status-done {
color: #1d8f4a;
}
.bdg-20-status-progress {
color: #b8442d;
}
.bdg-20-status-unread {
color: #aaa;
}
.bdg-20-hint {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 9px;
letter-spacing: 0.3em;
text-transform: uppercase;
color: #bbb;
text-align: center;
margin-top: 8px;
}
```Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Badge from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Reading Time Indicator
Source: https://codefronts.com/snippets/css-badges/reading-time-indicator/
Three states — not started, in progress, done. A 2px bar at the bottom fills to 100% on hover so the user can see what completion looks like. Clean type, no decoration.
## HTML
```html
<div class="bdg-20-stage">
<div class="bdg-20-stack">
<div class="bdg-20-badge" style="--bdg-20-progress: 0%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">2 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-unread"><span class="bdg-20-dot"></span>Not started</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 55%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">7 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-progress"><span class="bdg-20-dot"></span>In progress</div>
</div>
<div class="bdg-20-badge" style="--bdg-20-progress: 100%">
<span class="bdg-20-icon">⌛</span>
<div class="bdg-20-main">
<div class="bdg-20-duration">14 min</div>
<div class="bdg-20-label">reading time</div>
</div>
<div class="bdg-20-divider"></div>
<div class="bdg-20-status bdg-20-status-done"><span class="bdg-20-dot"></span>Finished</div>
</div>
<div class="bdg-20-hint">↑ hover any badge to complete it</div>
</div>
</div>
```
## CSS
```css
.bdg-20-stage {
background: #fafaf7;
padding: 60px 48px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100vh;
}
.bdg-20-stack {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.bdg-20-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 16px 10px 12px;
border: 1.5px solid #1a1612;
font-family: system-ui, "Bricolage Grotesque", sans-serif;
position: relative;
overflow: hidden;
cursor: default;
}
.bdg-20-badge::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: #1a1612;
width: var(--bdg-20-progress, 0%);
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.bdg-20-badge:hover::after {
width: 100% !important;
}
@media (prefers-reduced-motion: reduce) {
.bdg-20-badge::after {
transition: none;
}
}
.bdg-20-icon {
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.bdg-20-main {
display: flex;
flex-direction: column;
gap: 2px;
}
.bdg-20-duration {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.01em;
color: #1a1612;
line-height: 1;
}
.bdg-20-label {
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #4a4239;
line-height: 1;
}
.bdg-20-divider {
width: 1px;
height: 28px;
background: #cfc6b1;
flex-shrink: 0;
}
.bdg-20-status {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
white-space: nowrap;
line-height: 1.2;
}
.bdg-20-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
vertical-align: middle;
margin-right: 4px;
margin-bottom: 1px;
}
.bdg-20-status-done {
color: #1d8f4a;
}
.bdg-20-status-progress {
color: #b8442d;
}
.bdg-20-status-unread {
color: #aaa;
}
.bdg-20-hint {
font-family: system-ui, "Bricolage Grotesque", sans-serif;
font-size: 9px;
letter-spacing: 0.3em;
text-transform: uppercase;
color: #bbb;
text-align: center;
margin-top: 8px;
}
```How this works
A reading-time indicator badge: “5 min read” with a small book / clock / eye icon. The pattern Medium, Substack, The Atlantic, The New Yorker, Wired, and every long-form editorial site ships to set reader expectations.
Pure CSS. The badge is a small pill with an inline SVG icon + text. Reading time is computed at build time (typically Math.ceil(wordCount / 265)) and injected into the text content — no runtime JS needed.
Make it yours
- Rebrand the pill colour from
--pill-bgand--pill-fgon the wrapper. - Swap the icon — book (📖), clock (🕐), eye (👁), coffee (☕) all read as “reading time”.
- Change the words-per-minute (WPM) baseline — 200 for slow readers, 265 (default), 300 for skimmers.
- Add a “progress” variant showing how much of the article the user has read so far.
- Include estimated audio-narration time as a secondary readout for accessibility.
Gotchas — read before shipping
- WPM baselines vary by content type — dense academic vs light editorial can differ by 2×. Choose a baseline that matches your typical content.
- Round up, not down — “5 min read” sets a more honest expectation than “4.7 min read”.
- The baseline (typically 265 WPM per Nielsen Norman Group) is Western literacy — for non-Latin scripts (Chinese, Japanese, Arabic), adjust because character-per-minute differs.
- Reading time is an editorial signal, not a promise — reader speed varies 3-4×.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 45+ | 9+ | 16+ | 45+ |
CSS pills, inline SVG, and text content are universally supported.