Code block line numbering using CSS counters
Astro comes pre-bundled with Shiki for syntax highlighting code. However, Shiki does not come with built-in support for line numbering in code blocks. I wondered if the counter() could be used to show line numbers in code blocks since Shiki parses the code into individual line blocks while rendering. Here’s how it looks like with some styling:
The highlighted CSS enables line numbering utilizing the counter() function. The idea is this; for each code block the line-number counter is reset, and for each line inside the code block, the counter is incremented by 1. The ::before pseudo-element is then used to display the current value of the counter before each line.
pre {
border: 1px solid var(--color-border);
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
border-radius: var(--space-0);
}
pre > code {
background-color: unset;
padding: 0;
border-radius: unset;
font-size: unset;
counter-reset: line-number;
}
pre > code .line {
counter-increment: line-number;
}
pre > code .line::before {
content: counter(line-number);
display: inline-block;
width: 2em;
padding: 0 var(--space-1);
background-color: var(--color-bg-1);
border-right: 1px solid var(--color-border);
margin-right: var(--space-2);
text-align: right;
color: var(--color-body-tertiary);
}
Looks great! Until there’s line wrapping involved:
Line wrapping is a function of the viewport width, and so you are probably seeing it even in earlier examples especially on smaller screens like a mobile phone. The problem with the above approach is that the line number is inline with the rest of the parsed tokens.
We will update the CSS with following changes to fix this:
pre {
border: 1px solid var(--color-border);
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
border-radius: var(--space-0);
}
pre > code {
background-color: unset;
padding: 0;
border-radius: unset;
font-size: unset;
counter-reset: line-number;
display: flex;
flex-direction: column;
gap: 0;
}
pre > code .line {
counter-increment: line-number;
display: grid;
grid-template-columns: auto 1fr;
}
pre > code .line > span {
display: contents;
}
pre > code .line::before {
grid-column: 1;
display: inline-block;
content: counter(line-number);
width: 2em;
padding: 0 var(--space-1);
border-right: 1px solid var(--color-border);
margin-right: var(--space-2);
text-align: right;
color: var(--color-body-tertiary);
}
Finally, the output looks like this:
function helloWorld() {
console.log("This is a very long line that will probably wrap around to the next line depending on the width of the container.");
}
Looking much better except for one minor issue; the wraparound line does not use the same spacing after being wrapped. This is because the line’s indentation is part of the first span and not a separate token. This will be solved in this post’s subsequent update.