home tools

Adding Line Numbers to HTML Code Blocks Using data- Attributes and CSS

July 2026

Numbers of Lines

I've been using the CSS counter counter function to add line numbers to code blocks forever. For example, this CSS:

.example-1 .num {
  counter-increment: example-1;
}

.example-1 .num::before {
  content: counter(example-1);
}
and this HTML:
<pre class="example-1"><span class="num"></span>fn main() {
<span class="num"></span>  println!("Hello World");
<span class="num"></span>}</pre>
Produces this output:
fn main() {
  println!("Hello World");
}

It works great for basic code blocks that start at 1 and go up uninterrupted from there.

Start Position

Starting with another number is done with the counter-resetreset property. For example, you can start the line numbers at 7 like this:

.example-2 {
  counter-reset: example-2 6;
}

.example-2 .num {
  counter-increment: example-2;
}

.example-2 .num::before {
  content: counter(example-2);
}
<pre class="example-2"><span class="num"></span>fn main() {
<span class="num"></span>  println!("Hello World");
<span class="num"></span>}</pre>
Which outputs:
fn main() {
  println!("Hello World");
}

Just remember to reset the counter value to one before the value you want to start with (e.g. 6 gets you 7).

The Skipping Problem

The problem I've been running into is how to show some parts of the code while skipping others. For example, say I've got this code block coming in from an included file:

fn main() {
  let alfa = "thing 1";
  println!("{}", alfa);

  let bravo = "thing 2";
  println!("{}", alfa);
}

But, instead of displaying the entire thing, I only want to show the opening and closing of the main function and the alfa lines. I could make another copy of the file with the lines removed. That causes two issues:

  1. I'd have to duplicate the code. That, in turn requires updating it multiple times for any changes. A pain and entirely too easy to mess up and publish incorrect code.
  2. Lines that are removed don't increment the counter. Any lines after a removal end up with different numbers than the original.

For example, the closing } in this display is marked as line 4 instead of 7:

fn main() {
  let alfa = "thing 1";
  println!("{}", alfa);
}

I want to keep the numbers the same for each line:

fn main() {
  let alfa = "thing 1";
  println!("{}", alfa);
}

I could use the counter-reset approach to set the line number after each removal. It would get complicated fast. Each block would have to be adjusted individually.

Solution Attributes

Enter the CSS attr()attr function. It's been around since 2015 and provides a better way to control line numbering. Specifically, it lets you grab the value from a data-* attribute in an HTML element for display.

Here's the code that makes it work:

.example-4 .num::before {
  content: attr(data-num);
}
<pre class="example-4"><span class="num" data-num="1"></span>fn main() {
<span class="num" data-num="2"></span>  let alfa = "thing 1";
<span class="num" data-num="3"></span>  println!("{}", alfa);
<span class="num" data-num="7"></span>}</pre>
And here's what it looks like:
fn main() {
  let alfa = "thing 1";
  println!("{}", alfa);
}

The technique requires hard coding the values. The complexity of doing that will vary between systems. It's straight forward with my setupsetup.

The hard coding opens up other opportunities as well. Placing an empty line with a dot instead of a number to replace the skipped lines, for example. So, this:

<pre class="example-4"><span class="num" data-num="1"></span>fn main() {
<span class="num" data-num="2"></span>  let alfa = "thing 1";
<span class="num" data-num="3"></span>  println!("{}", alfa);
<span class="num" data-num=""></span>
<span class="num" data-num="7"></span>}</pre>
Becomes this:
fn main() {
  let alfa = "thing 1";
  println!("{}", alfa);

}

Exactly what I'm looking for.

-a

end of line

Endnotes

Footnotes