Home / Snippets / Typography /
Line numbers
Auto-generated line numbers in any code block using CSS counters — no JavaScript, no extra markup beyond a class per line.
.card {
background: oklch(0.19 0.02 260);
border-radius: 0.75rem;
padding: 1.5rem;
color: var(--text);
}
.card:hover {
box-shadow: 0 4px 16px oklch(0 0 0 / 0.3);
}
Quick implementation
.code-lines {
counter-reset: line;
background: oklch(0.15 0.02 260);
border-radius: 0.75rem;
padding: 1.25rem 1.5rem 1.25rem 0;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
line-height: 1.7;
overflow-x: auto;
}
.code-lines .line {
display: block;
counter-increment: line;
padding-left: 1rem;
}
.code-lines .line::before {
content: counter(line);
display: inline-block;
width: 2.5rem;
margin-right: 1.25rem;
text-align: right;
color: oklch(0.45 0.03 260);
user-select: none;
border-right: 1px solid oklch(0.26 0.02 260);
padding-right: 0.75rem;
margin-left: -1rem;
}
Prompt this to your LLM
Includes role, constraints, counter mechanics, and edge cases for long lines and selection.
You are a senior frontend engineer building a code display component.
Goal: CSS-only line numbers for a <pre> code block using CSS counters and ::before pseudo-elements.
Technical constraints:
- Use counter-reset: line on the <pre> wrapper.
- Each line is wrapped in a <span class="line"> — increment with counter-increment: line.
- Use ::before with content: counter(line) to render the number.
- Numbers must be right-aligned in a fixed-width gutter (min 2.5rem).
- Separate gutter from code with a subtle vertical border: oklch(0.26 0.02 260).
- Number color must be dim and unobtrusive: oklch(0.45 0.03 260).
- Add user-select: none to the ::before so copying code doesn't include numbers.
- Use oklch() for all colors — no hex, no rgba().
Framework variants (pick one):
A) HTML/CSS — wrap each line in <span class="line">.
B) React component — split code string by newline, render each as a <span className="line">.
Edge cases to handle:
- Horizontal scroll must not break the gutter — keep the gutter fixed while code scrolls.
- Empty lines must still receive a number (display: block on .line ensures this).
- Long lines should wrap within the code column, not extend the gutter.
Return CSS only.
Why this matters in 2026
Line numbers are a staple of documentation sites, tutorials, and code review UIs. The traditional approach — a JavaScript pass that injects numbered elements — adds render-blocking work and complicates copy-to-clipboard logic. CSS counters achieve the same visual result in pure CSS: each line gets a number via a ::before pseudo-element that is automatically excluded from clipboard selection with user-select: none.
The logic
The counter-reset: line on the wrapper initialises the counter. Each .line span increments it with counter-increment: line and uses ::before with content: counter(line) to display the current value. Setting display: block on .line ensures empty lines still increment and render a number. The gutter is a fixed-width inline-block on the pseudo-element, right-aligned with text-align: right and separated from code by a thin border.
Accessibility & performance
user-select: none on the ::before prevents line numbers from being included when a user selects and copies code — a critical UX detail. Screen readers generally skip CSS content in pseudo-elements, so the numbers do not pollute the accessible text. Because this is pure CSS, there is no JavaScript execution cost and no risk of the numbers appearing after a content flash.