A Code Block Label That Stays Put
Add a language label to Astro's Shiki code blocks with one pseudo-element, and keep it in place while long lines scroll sideways.

I wanted every code block on this blog to have a small header bar saying what’s inside: bash, yaml, dockerfile. A copy button can wait. A label shouldn’t need JavaScript at all.
It turned out to take one pseudo-element, plus one surprise about position: sticky.
The language is already in the HTML
Astro highlights fenced code with Shiki. You’ll find the fence’s language in the built HTML:
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash">
That data-language attribute is all we need. CSS can print an attribute’s value with content: attr():
.prose pre[data-language]::before {
content: attr(data-language);
display: block;
}
Every block now shows a label. That includes the ones with no meaningful language. This blog has dozens of plaintext and text blocks, and each one now announced “plaintext” like a stage manager.
Skip the boring ones and draw the bar
.prose pre[data-language]:not([data-language="plaintext"], [data-language="text"]) {
padding-top: 0;
}
.prose pre[data-language]:not([data-language="plaintext"], [data-language="text"])::before {
content: attr(data-language);
display: block;
margin: 0 -20px 16px; /* pre has 20px padding; stretch to its edges */
padding: 8px 20px;
border-bottom: 1px solid rgba(250, 249, 245, 0.07);
background: #252420;
font: 0.75rem/1.5 ui-monospace, monospace;
letter-spacing: 0.05em;
color: #d97757;
}
The negative side margins pull the bar out to the pre’s edges, and padding-top: 0 lets it sit flush against the top. Short blocks look finished.
Then a long line scrolls
A pre with long lines scrolls sideways (overflow-x: auto), and the pseudo-element scrolls with everything else. Scroll a wide YAML file to the right and the label slides off the left edge, along with half the bar.
The usual fix for “stay visible while the container scrolls” is position: sticky:
position: sticky;
left: 0;
Better, but not quite. Once you scroll, the bar stops 20px short of the left edge, leaving a dark gap the width of the padding.
Sticky counts the padding
In Chrome 155, I measured it with a plain element in a 20px-padded scroll container, scrolled 300px:
left: 0 → bar starts 20px inside the box
left: -20px → bar starts flush with the box
no padding → left: 0 is flush
So the sticky edge sits inside the scroll container’s padding. The spec measures sticky insets from the edges of the scrollport and doesn’t say anything about padding, so treat this as browser behavior and check it where you care. For this layout, the fix is to cancel the padding:
position: sticky;
left: -20px; /* same as the pre's horizontal padding */
If you use a spacing token for the padding, use the same token here, for example left: calc(var(--space-5) * -1). Then the two can’t drift apart.
If you see this
- Every block says “plaintext”: add the
:not()filter for the languages you don’t want labeled. - The label scrolls away with long lines: it needs
position: stickyand aleftinset. - A gap opens on the left while scrolling: the sticky inset is fighting the
pre’s padding. Setleftto the negative padding.
References
- Astro: Syntax highlighting
- MDN: attr()
- MDN: position
- W3C: CSS Positioned Layout Module Level 3, sticky positioning
Cover photo: Unsplash.