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)
Enter fullscreen mode Exit fullscreen mode

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-';
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

We need a random id for our property as well:

const id = `${prefix}${Math.random().toString(36).substr(2, 6)}`;
Enter fullscreen mode Exit fullscreen mode

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>`;
Enter fullscreen mode Exit fullscreen mode

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)}")`;
Enter fullscreen mode Exit fullscreen mode

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})`;
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
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 😅.

Collapse
 
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?

Collapse
 
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.

Thread Thread
 
madsstoumann profile image
Mads Stoumann

Ah, ok!

Collapse
 
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()

Collapse
 
madsstoumann profile image
Mads Stoumann

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

Collapse
 
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 ;)

Collapse
 
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.

Collapse
 
faaktap profile image
Fakie Tap

Thanks for sharing. I like it.

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!