# Highlighted code in slides I have obsessed about this long enough, I think it's only fair I (and you!) get some content out of it. When I started writing this article, I was working on my P99 CONF slides. Those slides happen to include some bits of code. And because I'm a perfectionist, I would like this code to be syntax highlighted, like this:
let addr: SocketAddr = config.address.parse()?; let ln = TcpListener::bind(&addr).await?; info!("🦊 {}", config.base_url);
Not like this:
let addr: SocketAddr = config.address.parse()?; let ln = TcpListener::bind(&addr).await?; info!("🦊 {}", config.base_url);
Cool bear A perfectionist? What, you set up highlight.js and called it a day? Amos Oh no. No no no. Amos I have my *own* blog engine with my own Markdown processing pipeline that calls out to my tree-sitter, using my own builds of a collection of grammars I've collected over the years like a poet collects rhymes, and then I generate compact HTML markup so that your browser doesn't explode at the mere sight of one of my articles. Cool bearCool bear Well. Cool bear We all have hobbies. Unfortunately, most of the time, I see either: - Code as text (no colors) - Screenshots of their code editor (big files, looks crap on high-density displays) A few weeks ago, I started looking for better options, because surely someone else was bothered by this — there are dozens of us! Dozens! ## Not invented here And I found… some plug-ins for Google Slides (most of them only work on Google Docs, a different product) that take SEVERAL SECONDS to highlight one block of code, since the plug-in code appears to run on-demand on a shared coffee maker in Mountain View. The menu when using the Code Syntax plug-in: it goes Extensions, Code Syntax, Colorize Selection as, and I had to zoom out to have it show rust Oh and it looks absolutely dreadful because, to be fair, colors are hard, but to be real, most developers have absolutely no sense of style. The default code syntax colors, with a heinous purple for keywords (also bold), a.. greenish blue for everything else? a green that I guess might look decent somewhere else for macros, I don’t know. it’s a mess. It looks like codemirror or something. But then I noticed something.
When you copy text from a web page… and you paste it into Google Slides… it retains some of its formatting.
A screenshot of the Rust book, showing some syntax-highlighted code, then an arrow, then the slide, highlighted. In fact, I've known that forever. Everyone knows that. It's a *really annoying* feature: you have to do "clear formatting" after pasting, or you have to remember whichever keyboard combination pastes *without formatting*. Cool bear Cool Bear's hot tip: On macOS, this operation is named "Paste and match style" and it has the heinous shortcut "Option-Shift-Command-V". A macOS menu showing the “Paste and Match Style” option Bright side? Free finger yoga! We have to go *out of our way* to make sure that pasting *doesn't* retain style. The problem is… it retains a bit too much. It retains font-size for example, and it retains background color, but only sometimes? Which is something you almost definitely never want.
But if you were to feed it carefully-formatted HTML… HTML generated only for the purpose of being pasted into Google Slides, then maybe… it could work?
That idea is hardly new: you'll find slides code highlighters around the web. One of the nicer code highlighters available online. It has a code editor on the left, and the colorized result is on the right. There’s a selection of themes and fonts to choose from. It looks rather nice, and even gives you instructions on which browsers to use (Safari for Keynote, Chrome for Google Slides), and which background color to use. But I *like* the code highlighting on my website. I chose my colors (and fonts!) with love. I revel in the knowledge that, even though it's only doing "syntactic" highlighting and not "semantic" highlighting (like something like rust-analyzer can), there's still a full-on parser running behind the scenes, not just a set of regular expressions. And most importantly: I now tend to work on both articles and videos at the same time: first writing the article, and then making slides I can work into the video at editing time. This very article, opened as markdown sources in Zed, the code editor I’ve been using for a few months. So, all the code I want on the slides is already on my website, taunting me, just BEGGING to be copied into my clipboard so that I can paste them into slides. ## Adding a button So I added a button. You can't see it, but I can — I'm only showing it to logged-in admins. The copy button, as shown to only myself, in the top-right corner of any code block. Not just in text form, like a GitHub README would do, but *also* as HTML markup. That's harder than it looks though. Because when you, the user, the human interacting with a browser, when you select text, and hit Ctrl+C or Cmd+C, you're doing something that JavaScript code can't really do. Well, it can, but it has very little control over it. Technically, when you press that key combination, a copy event is dispatched.
You can add a handler for that event, which lets you override what will go in the clipboard.
Cool bear Cool Bear's hot tip: That's how some annoying websites have your clipboard filled with, "Oh, you can't copy from this website. You need to pay us." instead of the thing you copied. You can also call preventDefault() to, well, prevent anything from being copied in the system clipboard. But you can't read what's in the clipboard, because presumably security and privacy and things like that. You can also generate a synthetic copy event. However, it will not affect the system clipboard at all. An excerpt from the Clipboard API standard (a W3C Editor’s Draft), which says event handlers may write to the clipboard if the scripting thread is allowed to show a popup (in response to a click etc.), and that it may allow trusted event types to modify the clipboard, but specifically not for synthetic cut and copy events. You can use document.execCommand, which actually does the same thing that hitting the key combination does. But it's deprecated to hell, and you shouldn't use it, even though all the browsers now support it. And if you do use that, remember that you do get to override what gets written by intercepting the copy event. But you don't get to read what was there in the first place. ## The Clipboard API At this point, you may as well do the right thing and use the Clipboard API. It lets you read and write clipboard items asynchronously, and items can be made available in multiple mime types…
async function writeToClipboard() { const text = "Hello, world!"; const html = "<h1>Hello, world!</h1>"; await navigator.clipboard.write([ new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }), 'text/html': new Blob([html], { type: 'text/html' }) }) ]); console.log("Content copied to clipboard as both text and HTML."); }
…which matches the way clipboards work in real-life! On macOS, Sindre Sorhus's wonderful Pasteboard Viewer shows us what ends up in the "General pasteboard" when we run this code from Safari: The plain text version of what’s in the pasteboard: just Hello, world! The HTML version of what’s in the pasteboard: notably, the h1 tag has inline styles with a caret-color of rgb(0, 0, 0), same for color, the font-style is set to normal, font-variant-caps is set to normal, and other defaults. Finally it says Hello, world inside the h1 tag. If we execute it from Firefox, the HTML version is a bit different: Firefox’s take on what should be in HTML format, as seen by Pasteboard Viewer — this time it has the whole boilerplate of an HTML document: an html tag, a head tag, even a meta tag that sets the content-type to text/html with a charset of utf-8, then a body with the h1 tag in there. No inline styles this time.
Each browser will add its own little wrapper around our HTML payload, which is mildly upsetting, but not a huge deal in the grand scheme of things.
Chromium’s take on it, with a meta charset utf-8 outside the HTML tag, and then an empty head, and a body with just the h1. However, we have a much bigger problem. ## Computed styles The hard part isn't getting HTML into the clipboard — that works fine with the Clipboard API. The challenge is producing HTML that will actually render correctly when pasted. When you copy rich text from a web page, browsers include computed styles in the clipboard HTML. That's why pasting from one site to another usually preserves the visual appearance: you get inline styles with resolved values for font, color, and spacing. But here's the catch: those computed styles are tied to the specific rendering context. If the HTML we generate references CSS classes, the pasted content won't know anything about them. Google Slides doesn't load our stylesheet. The solution is to inline all the relevant styles on every element. That's straightforward for simple cases — you know what font-family and color you want — but it gets complicated fast. Consider syntax highlighting. Our highlighted code uses a palette of colors for tokens. Each token type (keyword, string, comment, etc.) has its own background or foreground color. Those come from CSS rules that are defined in a separate stylesheet. To make the pasted content look right, every `` in our code needs explicit style attributes: - color for the foreground - background-color where needed Cool bear That's mechanical to generate — the challenge is maintaining the exact palette across code blocks, especially when you use multiple grammars. Comment colors for JavaScript, Python, and Rust might all be different in the UI. But there's another subtlety. When pasting into Slides, block-level elements get collapsed. A `
` between two blocks of code doesn't necessarily preserve the vertical spacing. The paste handler in Slides interprets the HTML's semantic structure, translating `
`s and `

`s into text frames with line breaks — but it helps to know what markup gets flattened and what doesn't before building your exporter. ## The mechanics of copying All right, so we have our plan: 1. Build an HTML string with fully-inlined styles for our code block 2. Write multiple representations to a single ClipboardItem: `text/plain` and `text/html` 3. Store it in the clipboard via navigator.clipboard.write() Amos But wait — there's an even trickier issue: what if you want to copy just part of a highlighted code block? Your eventual users might not want the full block; they may select a single method, or a range of lines. When you intercept the copy event dispatched on a selection, you get access to a Selection object from document.getSelection(). But you can't easily map the selected DOM ranges to ranges in your original syntax tree — at least not reliably. That's where this project grew beyond a simple button. Cool bear What I ended up building wasn't a single-purpose bookmarklet, but a small library: you give it a source file and a grammar, plus a pre-built palette of token colors, and it generates both visual HTML for display and copy-ready HTML with everything inlined. ## Real-life example Here's the copy button as implemented in the blog engine's articles — rendered only for logged-in users. Clicking it invokes the following sequence: js const button = document.querySelector('button.copy-code'); button.addEventListener('click', async () => { // we disable the default browser behavior // because we're not copying the user's existing selection const range = document.createRange(); range.selectNodeContents(codeElement); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); // then let the clipboard API do the rest await copyRichText({ html: toInlineHTML(codeElement), text: codeElement.innerText }); }); Safari writes RTF to the clipboard, isn’t that fun There are caveats, naturally. Firefox wraps our payload differently than Safari, Chrome adds its own comment blocks around the fragment. But the content arrives intact — and with every style inline, the pasted code closely resembles the original site.

<head> <meta charset="UTF-8" /> </head> <div class="bottom-nav-previous" style='margin: 0px; padding: 0px; border: 0px; box-sizing: border-box; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 18.9px; font-style: normal; font-variant-caps: normal; font-weight: 300; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none;' > Looking for<span class="Apple-converted-space"> </span ><a href="http://ftl.localhost:1111/" style="margin: 0px; padding: 0px; border: 0px; box-sizing: border-box; color: light-dark(rgb(232, 12, 12), rgb(255, 116, 116)); text-decoration: none;" >the homepage</a >? </div> <br class="Apple-interchange-newline" />
Is this The Right Way? Maybe not. It's a local solution to a personal annoyance, and no script can fix the misfortune of working with Google Slides' paste handler on every quirky browser. But building it gave me a better understanding of what goes in and out of system clipboards… and why your users should not have to navigate the finger gymnastics of "paste and match style" to get what they need. Amos

Understanding computed styles

When you press Ctrl+C, Cmd+C, or invoke execCommand("copy"), the browser copies the current selection as plain text, HTML, and RTF. The HTML payload, however, comes with some questionable decisions baked in.

If you pretty-print the copied style of a link, two issues stand out. First, the browser inlines a “reset” for dimensions and the box model whenever possible. Second, it does partial style computation — it copies the color directive inline rather than relying on external CSS rules — but it stops short of resolving all values fully.

The most visible problem is that the payload contains both the dark mode and light mode colors of the link:

  • Light mode
  • Dark mode

The browser selects the right one later using the light-dark CSS function, based on the active color scheme. But when you're pasting content into a slide deck, you want the color that's active right now — not a future decision re-made by the receiving application.

To get that, you can't rely on the HTML generated by the copy event. Instead, grab the relevant node's outerHTML property. For a selected paragraph, that returns markup without any inline styles. The innerHTML property shows the children only, and contextText gives plain text — none of which carry the styling information you need.

What you want is the computed style: the values active at this exact moment, as shown in dev tools' “Computed” tab. All major browsers except Firefox provide access via computedStyleMap(). Querying a paragraph's computed style for color returns something like rgb(255, 255, 255); narrowing to a link inside it returns rgb(255, 116, 116) in dark mode or rgb(232, 12, 12) in light mode.

The full copy-as-HTML solution

With a small vanilla.js implementation, you can copy an element to the clipboard as HTML while preserving the computed text color and font-weight of every child element:

One function in that implementation — normalizeColor — deserves special attention.

Why a color normalizer exists

This works reliably until you change your site's CSS. A redesign introduced colors specified in Display P3, a wide-gamut color space using a D65 white point and the sRGB tone reproduction curve. With an HDR display, these colors render visibly more intense than their sRGB counterparts; on non-HDR hardware, browsers tone-map them to the nearest sRGB equivalent.

The same redesign used a variable-width font with an unusual font-weight of 120. With variable fonts, “normal” isn't always 400 — this particular typeface maps normal to 100 and bold to 150.

When pasting the generated HTML into Google Slides, the code appeared all black. Google Slides doesn't inject the CSS you provide directly; it parses styles and only applies the ones it understands. Display P3 colors aren't supported, so they fell back to black.

Transforming a color from outside a gamut to the closest representable color inside that gamut is — spoiler — genuinely hard. The Color.js documentation sums it up:

“The process of transforming a color outside of a given gamut to a color that is as close as possible but is inside gamut is called gamut mapping”

Entire books are written on this subject, and it's a legitimate case for pulling in a third-party dependency.

Final observations

Slides now render with the correct colors, and the fix is bundled into the site's copy button. But the underlying frustrations remain. Google Slides still doesn't accept SVG files — EPS-based workarounds are not a substitute — and it ships a keyboard shortcut for rotating elements that no one asked for. Meanwhile, Keynote handles display-p3 colors and SVG natively, which is reason enough to switch.