Toggling line wrap in code blocks

Yesterday I added some longer code blocks, which made me reconsider white-space: pre and white-space: pre-line for those. Since this is a user preference, I added a simple toggle button just now. Here's a quick post on how I built this with Progressive Enhancement in mind, using the new element-scoped view transitions and some other things I learned recently. Buzzword galore for your enjoyment. Or mine.

A screenshot of an HTML code block showing “<picture> <source type="image/avif" srcset="/_img/2026/05/cssday-” in white on a dark background. The code, right before wrapping to the next line, then fades out into gray and gets cross-faded with other code below, making it harder to read. In the top right there's a button saying “Toggle wrapping”.

Showing code will be a big part of my blog, so I'm tweaking tiny bits on a daily basis. Yes, syntax highlighting is on my todo list. And yes, I know I can include GitHub gists or whatever. And that this is a solved problem already. But I guess you sort of know where this is going: there's joy in playing with this.

So, the problem was quite obvious: what to do with line-wrapping in code? I couldn't figure it out, and I wanted to add some interactive elements to code blocks anyway, so I started off with this little thing:

const codes = document.querySelectorAll('[class|="language"]');
codes.forEach(function(code) {
	if (code.scrollWidth <= code.offsetWidth) {
		return;
	}
	const button = document.createElement('button');
	button.textContent = 'Toggle wrapping';
	button.addEventListener('click', function() {
		const preEl = this.parentNode;
		const codeEl = this.parentNode.querySelector(':scope code');
		const toggle = function() {
			preEl.style.whiteSpace = getComputedStyle(preEl).whiteSpace == 'pre-wrap' ? 'pre' : 'pre-wrap';
		};
		if (codeEl.startViewTransition) {
			codeEl.startViewTransition(toggle);
		} else {
			toggle();
		}
	});
	code.parentNode.prepend(button);
});

Some notes, in random order:

I only “render” this script in a <script type="module"> when a code block is used somewhere on the page. View Source to make sure. No code blocks? No useless querySelectoring going on. I know this is a pretty oldskool style—or bad way—of doing things, but I'll get back to how I do this on larger sites at a later point. Or maybe I'll implement a different way at that same point as well.

I'm using the |= operator attribute selector, since the server always renders <pre><code class="language-..."> for these blocks. I chose this markup, because there's a line in the HTML specification saying:

There is no formal way to indicate the language of computer code being marked up. Authors who wish to mark code elements with the language used, e.g. so that syntax highlighting scripts can use the right rules, can use the class attribute, e.g. by adding a class prefixed with "language-" to the element.

If the code block isn't overflowing when it's rendered, just skip the rest of the code. If it is, create a button, add some text and a click handler and prepend it in the pre element. I should probably put that button in a <menu>, since it's now a toolbar. Next time. Just like adding a ResizeObserver for when the code blocks start or stops overflowing after a resize. There's so many details to take into account, you'd almost not ship anything at all. But that's not how I roll.

And when clicking that button, it toggles an inline style, which changes the wrapping. Could've used a custom property and setProperty here, but that would still change an inline style, so I think this is simpler.

Maybe Bert Bos was right after all, about “variables” in CSS being harmful. Most developers think they're just global constants om some magic :root incantation anyway. But I digress.

The last bits involve getting back the <code> element from the clicked button. I only recently learned about querySelector not doing what I always assumed, but needing :scope in there for that. Never too old to learn, I guess.

Oh, right, and there's a new element-scoped view transition in there. You can read the explainer about those, but they work just like normal view transitions. Except they are, well, scoped to elements. First time playing with those, and apart from using the inevitable Jake Archibald material, I guess you can use these new view transitions without view-transition-name in this case. No idea if it's smart to use in production already, but hey.

code[class|="language"]::view-transition-old(root),
code[class|="language"]::view-transition-new(root) {
	height: 100%;
	object-fit: none;
	overflow: clip;
	object-position: top;
}

I don't have Google Chrome installed, but it works great-ish in Vivaldi Snapshot, which uses Chromium 147. Still need to dive into figuring out the inner workins of these animations, because they're a bit wonky now. Next time.

Add a comment