DEV Community

Cover image for Line Numbers for <textarea> using SVG
Mads Stoumann
Mads Stoumann

Posted on

Line Numbers for <textarea> using SVG

The other day I was working on a JSON Schema Generator, and wanted to display line numbers in a <textarea>. Nothing fancy, and not considering soft line-breaks or anything remotely complicated.

I did some research, and found multiple approaches:

  1. Using a background-image (TinyMCE does that, using a PNG)
  2. Using an <ol> ordered list.

I did not like any of them! The first one didn't look crisp — and didn't match the styles I already had in place for my <textarea>-elements.

The second one required a bunch of JavaScript to maintain that ordered list: adding/removing <li>-elements dynamically, syncing scroll-events and much more.

So I ended up creating a hybrid.

It's a dynamically generated SVG, stored as a CSS Custom Property — and used as a background-image, inheriting the styles from it's parent <textarea>-element.

Line Numbers

Let's dive in.


JavaScript

First, the main method:

lineNumbers(element, numLines = 50, inline = false)
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

element is the <textarea> element to use, numLines the number of lines to render, and inline indicates whether to store the generated image on element (true), or on document.body (false).

Next, we define a prefix for the custom property:

const prefix = '--linenum-';
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

Before we continue, we check whether to re-use any existing property:

if (!inline) {
  const styleString = document.body.getAttribute('style') || '';
  const regex = new RegExp(`${prefix}[^:]*`, 'g');
  const match = styleString.match(regex);

  if (match) {
    element.style.backgroundImage = `var(${match[0]})`;
    return;
  }
}
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

Next, we extract styles from element, rendering the SVG with the same font-family, font-size, line-height etc. :

const bgColor = getComputedStyle(element).borderColor;
const fillColor = getComputedStyle(element).color;
const fontFamily = getComputedStyle(element).fontFamily;
const fontSize = parseFloat(getComputedStyle(element).fontSize);
const lineHeight = parseFloat(getComputedStyle(element).lineHeight) / fontSize;
const paddingTop = parseFloat(getComputedStyle(element).paddingTop) / 2;
const translateY = (fontSize * lineHeight).toFixed(2);
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

We need a random id for our property as well:

const id = `${prefix}${Math.random().toString(36).substr(2, 6)}`;
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

And now it's time to render the SVG:

const svg = `<svg xmlns="http://www.w3.org/2000/svg">
  <style>
    svg { background: ${bgColor}; }
    text {
      fill: hsl(from ${fillColor} h s l / 50%);
      font-family: ${fontFamily};
      font-size: ${fontSize}px;
      line-height: ${lineHeight};
      text-anchor: end;
      translate: 0 calc((var(--n) * ${translateY}px) + ${paddingTop}px);
    }
  </style>
  ${Array.from({ length: numLines }, (_, i) => `<text x="90%" style="--n:${i + 1};">${i + 1}</text>`).join("")}
</svg>`;
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

Let's break it down:

In the <style>-section we simply set the styles we extracted from the <textarea> earlier. Instead of using y and dy attributes for the <text>-elements, we simply use a --n-property to translate the text-elements using CSS.

The last part iterates an array created from numLines, and appends the <text>-elements to the main SVG.

We're almost there!


To use the generated SVG as a url()-property, we need to encode it:

const encodedURI = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

And finally, we set that property on either element or document-body:

const target = inline ? element : document.body;
target.style.setProperty(id, encodedURI);
element.style.backgroundImage = `var(${id})`;
🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini 🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini

And that's it!

Not too bad, and only 610 bytes, minified and compressed!


Demo

You can see a demo here, and dowload the full script here.

Below is a simplified Codepen, not using the inline-property logic:


Pros and Cons

Are there pros and cons? Of course there are!

Personally — for my current project — I needed a simple, crisp way of adding line numbers to a JSON-preview within a <textarea>, and this method fits the bill.

Pros

Reduced DOM Manipulation

This method does not rely on manipulating the DOM. The line numbers are generated as a single SVG, stored within a CSS Custom Property.

Automatic Synchronization

Since the line numbers are part of the background image, they automatically scroll with the text content, eliminating the need for manual synchronization logic.

Reusability Across Elements

By storing the generated SVG in a CSS Custom Property, it can be reused across multiple elements. This means that if several elements require the same line numbers, they can all reference the same custom property, avoiding redundant SVG generation.

Scalability

The vector nature of SVG ensures that the line numbers remain crisp and clear at any zoom level.

Cons

Accessibility

Ordered lists are more accessible to screen readers and assistive technologies, while SVG-based line numbers might be ignored or misinterpreted.

Customization Complexity

Styling and interacting with individual line numbers in an ordered list is straightforward. In contrast, the SVG approach makes it more difficult to customize or add interactivity to specific line numbers.

Browser Compatibility

SVG and CSS custom properties might not render consistently across all browsers — the current implementation has issues with Safari, where we need to deduct (paddingTop / 10) from translateY.

Dynamic Content Handling

Ordered lists can be more flexible for handling dynamic content updates, such as adding or removing lines, whereas the SVG approach might require regenerating and reapplying the entire background image.

Top comments (10)

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
link2twenty profile image
Andrew Bone •

Very cool!

I looked at making the numbers dynamic and redrawing on textarea input. I did a vary non-scientific stress test and starting getting major issues around line 5000 😅.

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
madsstoumann profile image
Mads Stoumann • • Edited

Interesting! 😊 I guess there must be a tiny variation in how browsers calculate line-height for textarea and svg?

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
link2twenty profile image
Andrew Bone •

Sorry I should have specified, the issues were with performance, the slow down while remaking the image for each new line.

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini Thread
 
madsstoumann profile image
Mads Stoumann •

Ah, ok!

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
dannyengelman profile image
Danny Engelman •

Cool, I am going to use this in a styled textarea

FYI, modern browsers can do: const id = crypto.randomUUID()

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
madsstoumann profile image
Mads Stoumann •

Ah, true! I used crypto on another project, forgot it on this 😉

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
julian-ismael-berger profile image
Julian Ismael Berger •

Very nice solution!

I just noticed that it does not work proper with firefox, the scrolling does not work...

And yes I know the market share is sadly not much but I just wanted to let you know ;)

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
madsstoumann profile image
Mads Stoumann •

Thanks! It looks fine in Firefox Nightly on my Mac - when I get more time I’ll look into better browser-support.

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
faaktap profile image
Fakie Tap •

Thanks for sharing. I like it.

🏆 Live Soccer TV: madsstoumann/line numbers for using svg - Pertandingan Hari Ini
 
madsstoumann profile image
Mads Stoumann •

Thanks!