Styling Code with Line Numbers using CSS

My team struggled for a long time with how to include code snippets and other text (such as the contents of a CSV file) in our documentation because we use a single source of content for web-based and PDF output. I have spent a good chunk of time over the past couple months searching the Internet for solutions, and now that I have one, I want to share it with the world. In a perfect world, we could meet this list of requirements:

  • Single source of content for both web-based and print-friendly output.
  • Line numbers are kept to the left of the page with the numbers columns (ones, tens, hundreds) lining up vertically, like you would see in a nice text editor or integrated development environment (IDE).
  • A line of code can wrap as-needed according to whatever container it is in (html body, pdf page margins, list item, table, etc.).
  • A wrapped line of code left-aligns with its first character.
  • Authoring in a WYSIWYG editor is simple and intuitive.
  • The content is organized semantically as much as possible.
  • The reader can copy and paste the code from our documentation, straight into their text editor without having to clean it up.
  • The reader can easily discern between a deliberate carriage return and a line of text wrapped because of space constraints.

First, special thanks goes to the following two pages for sending me in the right direction:

  • (NOTE: I did not end up using a gradient in my final product because of the lack of support in the authoring tool I use, but it worked just as well as the pseudo element approach I decided on)
    CSS-Tricks has proven to be my favorite resource for learning about CSS. I only wish they showed up on the top of my search results, over w3schools.
    Another great resource for some cool CSS usage.

A Not-so-much List

The Internet loves it some lists, so here is one of some things we tried and why they didn’t work out.

1. Word Wrap Indicator Icon

The most recent solution we used was to insert a small icon at the end of a line where text was wrapping. This option is functional, but not the most intuitive. It also requires manual upkeep with the equivalent of inline formatting; a significant change to our style guide will require, at the very least, a review of every code sample to make sure things still line up appropriately.

2. Preserving White Space

Web browsers are designed to ignore extra white space, such as tabs, carriage returns, and sequential spaces. This allows coders to format their code in a way that is easy to read without messing up what the user sees, but it can complicate coding when you need to display text as-is. HTML and CSS both have style options for preserving white space when you need to; here are the reasons they don’t work for us (a list within a list!): (1) wrapped lines don’t align with the left margin of where the line started, (2) authoring is complicated and requires manual layout manipulation, and (3) the lines are not organized semantically or at all, really.

3. Scrollable Regions

Scrollable regions in a page are pretty handy for specific cases on the web–especially with code samples. However, they do not convert well to print outputs. Since we do not produce pop-up books for our users, this is not an option.

What Worked

Lists! An ordered list works well for a few reasons:

  • The content is tagged in a reasonably semantic fashion. Each list item correlates to a line number, and typically when you want code to indent, it is a child of the previous line, which correlates well with nested lists in the code.
  • A list item can contain any flow content, which made the alignment against the line numbers possible.
  • In a WYSIWYG editor, creating and nesting lists is typically well-supported.

Here is a glorious screenshot for anyone who just cannot handle the anticipation:


There were three tricky parts to getting this to work:

  • Automatically numbering each line, including lines in a nested list, with a single sequence.
  • Styling the line numbers separately from the content of the line.
  • Keeping indentation aligned on the left side when lines wrap.

To create the line numbers, I used a CSS counter paired with the CSS pseudo-element ::before. I used the CSS counter instead of the default list style type because it is a simple way to number each list item, regardless of where it is nested, in a single sequence. It also offers more flexible styling options for the numbers, separate from the content.

Here is a snippet of the CSS used to create the line number:

ol.CodeSample li:before
counter-increment: linecounter;
/* increments the line counter 1 integer for each instance of this pseudo element */
content: counter(linecounter);
/* inserts the text of the line counter into the page content */
width: 2em;
/* ensures that there is enough space for 3 characters for the line numbers and enough space for the numbers to align to the right */
float: left;
/* floats the counter to the left so the code starts lined up next to it instead of on the next line */
text-align: right;
/* aligns the counter to the right so that the numbers columns line up properly */

You must also reset the counter at the top-level list so that two code samples in the same page keep their own line numbers, for example:

counter-reset: linecounter;
/* Restarts the counter used to generate line numbers */

We only had two requirements for styling the line numbers apart from the content. The first is that they are right-aligned, which is handled in the CSS shown above. The second is that they have a different background color from the content. I solved the latter by using another ::before pseudo-element, this time on the top-level ordered list. With this method, I was able to add a background that only filled the width of the pseudo-element container. Using absolute positioning and a negative z-index, I seat the background underneath the line numbers.

Here is a snippet of the CSS used to create the background for the line numbers:

ol.CodeSample:before /* pseudo element with a null content to create the background for the line numbers (print uses an image instead) */
content: "";
position: absolute;
/* because the parent element is set to relative, the absolute setting is contained within the parent element (ol.CodeSample) */
z-index: -1;
/* moves the background color behind the text of the line numbers */
width: 2.2em;
/* allows enough space for 3 character line numbers */
height: 100%;
/* stretches the background from the top of the ol to the bottom */
background: #CCCCCC;

For the absolute positioning to stay within the list container, you must set the list position to be relative, for example:

position: relative;
/* because there is no other position setting, this will act as if it is set to static (the default), but this is required for the child elements to stay within the ol when they are set to use absolute positioning */

To keep content properly aligned on the left requires a paragraph element to act as a container within each list item. This way, I can style the paragraphs with a margin and as lines wrap, each line respects that margin. I use complex selectors to add indentation levels based on the level in which the list item is nested. In our case, four levels are enough, but the pattern to create more is easily discernible.

Here is a snippet of the CSS used to style the paragraph margins:

ol.CodeSample > li > p /* indents the first level of code */
margin-left: 2.6em;

ol.CodeSample > li li > p /* indents the second level of code */
margin-left: 4.5em;

ol.CodeSample > li li li > p /* indents the third level of code */
margin-left: 6.3em;

ol.CodeSample > li li li li > p /* indents the fourth level of code */
margin-left: 8.2em;

ol.CodeSample p
padding-right: 0.5em;
/* adds space between the right border and text that reaches the far right boundary of the ol container */
margin: 0em;

And here is a snippet of what the HTML looks like:

<ol class="CodeSample">
<p>1st line of code.</p>
<p>2nd line of code.</p>
<p>3rd line of code, indented 1 level.</p>
<p>4th line of code.</p>
<p>5th line of code, indented 1 level.</p>
<p>6th line of code, indented 2 levels.</p>
<p>7th line of code.</p>

See the attached stylesheet and html page for a working sample. If you have anything similar you have done, any questions, or any modifications and improvements to my solution, I would love to hear from you!

Outstanding Problems

Since putting this code into production, I’ve run into the following issues:

  • Some breaking characters do not have a non-breaking counterpart, so your code samples will wrap at weird points. Underscores are an example that causes us issues.