--- title: "Code listings with nice line wrapping and line numbers from plain CSS" date: 2025-07-23T23:42:00+01:00 summary: > Code listings in web pages are often a bit of a pain to use. Usually, they don't wrap on small screens. Also, copy-pasting code from a code listing often copies the line numbers along with the code. Finally, many implementations use heavyweight HTML and/or javascript, making them slow to render. For this blog, I wrote a little CSS hack that renders nice, wrapping code blocks with line continuation markers in plain CSS without any JS. --- Code listings in web pages are often a bit of a pain to use. Often, they don't wrap on small screens. Also, copy-pasting code from a code listing often copies the line numbers along with the code. Finally, many implementations use heavyweight HTML and/or javascript, making them slow to render (looking at you, gitlab). For this blog, I wrote an implementation that renders HTML code listings entirely without JavaScript, renders line numbers using plain CSS such that they don't get selected with the code, and that works with the browser to wrap in a natural way while still supporting the little line continuation arrows that are used to show that a line was soft wrapped in text editors. This blog is rendered as a static site using Hugo_ from a pile of RestructuredText_ documents. RestructuredText renders code listings using Pygments_ by default. Pygments hard-bakes the line numbers into the generated HTML, so I am using a `monkey-patched`_ hook that changes the line number rendering to just a bunch of empty ```` elements. The resulting HTML for a code block then looks like this: .. code:: html
        
        
            The code!
        
        
    
You can find the (rather short) source of the ``rst2html`` wrapper `below <#rst2html-wrapper>`_. The CSS ------- This modified HTML structure of the code listing gets accompanied by some CSS to make it flow nicely. Here is a listing of the complete CSS controlling the listing. The only bit that isn't included here is the actual syntax styling rules for the pygments tokens. .. code:: css /*****************************************************/ /* Code block formatting / syntax highlighting rules */ /*****************************************************/ .code { font-family: "Fira Code"; font-size: 13px; text-align: left; /* Override default content "justify" alignment */ white-space: pre-wrap; word-wrap: break-word; overflow-x: auto; display: grid; align-items: start; grid-template-columns: min-content 1fr; } .code > .line { padding-left: calc(2em + 5px); text-indent: -2em; padding-top: 2px; min-width: 15em; } /* Make individual syntax tokens wrap anywhere */ .code > .line > span { overflow-wrap: anywhere; white-space: pre-wrap; } /* We render line numbers in CSS! */ .code > .lineno { counter-increment: lineno; word-break: keep-all; margin: 0; padding-left: 15px; padding-right: 5px; overflow: clip; position: relative; text-align: right; color: var(--c-text-muted); border-right: 1px solid var(--c-fg-highlight); align-self: stretch; } /* We also handle line continuation markers in CSS. */ .code > .lineno::after { position: absolute; right: 5px; content: "\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳"; white-space: pre; color: var(--c-text-muted); } /* Insert the actual line number */ .code > .lineno::before { content: counter(lineno); } .code::before { counter-reset: lineno; } .code .hll {} /* Following are about 50 lines that define the styling of each kind of pygments syntax highlight token. These lines all look like the following: */ .code .c { color: var(--c-text); font-weight: 400 } /* Comment */ This CSS does a few things: 1. It renders the ``
`` code listing element using a two-column CSS ``display: grid`` layout. The left column is
    used for the line numbers, and the right column is used for the code lines.
 2. It numbers the lines using a `CSS Counter`_. CSS counters are meant for things like numbering headings and such, but
    they are a perfect fit for our purpose.
 3. It inserts the counter value as the line number into the ```` element's ``::before``
    pseudo-element. A side effect of using the ``::before`` pseudo-element is that without doing anything extra, the
    line numbers will remain outside of the normal text selection so they will neither be highlighted when selecting
    listing content, nor will they be copied when copy/pasting the listing content.
 4. It inserts a string of ``"\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳"`` into the line number span's
    ``::after`` pseudo-element. This string evaluates to a sequence of unicode arrows separated by line breaks, and
    starting with an empty line. The ``::after`` pseudo-element is positioned using ``position: absolute``, and the
    parent ```` has ``position: relative`` set. This way, the arrow pseudo-element gets placed on
    top of the lineno span without affecting the layout at all. By setting ``overflow: clip`` on the parent ````, the arrow pseudo-element gets cut off vertically wherever the parent lineno element naturally
    ends.

The line number span is inserted into the parent ``
`` element's CSS grid using ``align-self: stretch``, which
causes it to vertically stretch to fill the available space. Since the line number span only contains the line number,
its minimum height is a single line. As a result, it will stretch higher only when the corresponding code line in the
right grid column stretches vertically because of line wrapping. When that happens, part of the arrow pseudo-element
starts showing through from behind the ``overflow: clip`` of the line number span, and one arrow gets rendered for each
wrapped listing line.

When the page is too narrow, we don't want the code listing's lines to wrapp into a column of single characters. To
prevent that, we simply set a ``min-width`` on the ```` in the right column, and set ``overflow-x:
auto`` on the listing ``
``. This results in a horizontal scroll bar appearing whenever the listing gets too narrow.

You can try out the line wrapping by resizing this page!

rst2html wrapper
----------------

Here is the python ``rst2html`` wrapper that monkey-patches code rendering. I made hugo invoke this while building the
page by simply overriding the ``PATH`` environment variable.

.. code:: python

    #!/usr/bin/env python3
    # Based on https://gist.github.com/mastbaum/2655700 for the basic plugin scaffolding

    import sys
    import re

    import docutils.core
    from docutils.transforms import Transform
    from docutils.nodes import TextElement, Inline, Text
    from docutils.parsers.rst import Directive, directives
    from docutils.writers.html4css1 import Writer, HTMLTranslator


    class UnfuckedHTMLTranslator(HTMLTranslator):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.in_literal_block = False

        def visit_literal_block(self, node):
            # Insert an empty "lineno" span before each line. We insert the line numbers using pure CSS in a ::before
            # pseudo-element. This has the added advantage that the line numbers don't get included in text selection.
            # These line number spans are also used to show line continuation markers when a line is wrapped.
            self.in_literal_block = True
            self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
            self.body.append('')

        def depart_literal_block(self, node):
            self.in_literal_block = False
            self.body.append('\n
\n') def visit_Text(self, node): if self.in_literal_block: for match in re.finditer('([^\n]*)(\n|$)', node.astext()): text, end = match.groups() if text: super().visit_Text(Text(text)) if end == '\n': if isinstance(node.parent, Inline): self.depart_inline(node.parent) self.body.append(f'
\n') if isinstance(node.parent, Inline): self.visit_inline(node.parent) else: super().visit_Text(node) html_writer = Writer() html_writer.translator_class = UnfuckedHTMLTranslator docutils.core.publish_cmdline(writer=html_writer) .. _Hugo: https://gohugo.io/ .. _RestructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html .. _Pygments: https://pygments.org/ .. _`monkey-patched`: https://en.wikipedia.org/wiki/Monkey_patch .. _`CSS Counter`: https://developer.mozilla.org/en-US/docs/Web/CSS/counter