SVG Theming in Astro Markdown
Sep 17 25 • 6 min read
TL;DR: rehype.ts
Astro does allow you to add plugins for markdown files through remark and rehype, allowing you to do all sorts of manipulations on your markdown content.
There is a whole ecosystem built around them, but I only needed these capabilities to apply some styling to <svg>
s because I wanted to do some Excalidraw drawings for my blog posts, copy them to my markdown as svg
do the styling, and that’s it. svg
s also have the advantage of being much smaller then images for most cases, and they look crispier since they are vector-based.
// astro.config.mjs
import rehypeSvgThemeTransformer from "./src/rehype.ts";
export default defineConfig({
markdown: {
rehypePlugins: [rehypeSvgThemeTransformer], // <- custom plugin
},
});
The plugin itself basically gives you the HTML AST, so you can traverse it and handle these objects as necessary.
For example, this markdown
# A heading
some content..
<div>html stuff</div>
becomes
{
"type": "root",
"children": [
{
"type": "element",
"tagName": "h1",
"properties": {},
"children": [
{
"type": "text",
"value": "A heading",
"position": {
"start": { "line": 1, "column": 3, "offset": 2 },
"end": { "line": 1, "column": 12, "offset": 11 }
}
}
],
"position": {
"start": { "line": 1, "column": 1, "offset": 0 },
"end": { "line": 1, "column": 12, "offset": 11 }
}
},
{ "type": "text", "value": "\n" },
{
"type": "element",
"tagName": "p",
"properties": {},
"children": [
{
"type": "text",
"value": "some content..",
"position": {
"start": { "line": 3, "column": 1, "offset": 13 },
"end": { "line": 3, "column": 15, "offset": 27 }
}
}
],
"position": {
"start": { "line": 3, "column": 1, "offset": 13 },
"end": { "line": 3, "column": 15, "offset": 27 }
}
},
{ "type": "text", "value": "\n" },
{
"type": "raw",
"value": "<div>html stuff</div>",
"position": {
"start": { "line": 5, "column": 1, "offset": 29 },
"end": { "line": 5, "column": 22, "offset": 50 }
}
}
],
"position": {
"start": { "line": 1, "column": 1, "offset": 0 },
"end": { "line": 5, "column": 22, "offset": 50 }
}
}
It is a handful, but all we need to understand with this AST is, we can traverse the tree recursively to visit each node, and check if the node is what we are looking for. In our case, the node should satisfy both type === 'raw'
and value.includes('<svg')
conditions. Once we find our target, we can simply modify the value
property, which is the HTML element itself as a string.
// rehype.ts
export default function rehypeSvgThemeTransformer() {
// remark expects the transformer to return a function
// and gives back the AST
// we traverse the tree, runs a predicate function which checks all criteria,
// and run the transformer if criteria meet
return (tree: Root) => visit(tree, predicate, transformer);
}
function predicate(node: any): boolean {
return (
node.type === "raw" &&
typeof node.value === "string" &&
node.value.trim().startsWith("<svg") && // only target <svg elements
!node.value.includes("skip-rehype-all") // do not transform if svg has this property
);
}
function transformer(node: Html): void {
node.value = node.value
.replace(/\s(width|height)="[^"]*"/g, "") // remove width and height props for responsivity
.replace(/<svg([^>]*)>/, `<svg$1 role="img" aria-hidden="true"`) // accessibility
.replace(/<svg([^>]*)>/, `<svg$1 class="theme-markdown-svg">`); // add a css class for further styling
if (!node.value.includes("skip-rehype-color")) { // a handy flag in case i want to skip removing color
node.value = node.value
.replace(/\sfill="[^"]*"/g, "")
.replace(/\sstroke="[^"]*"/g, "");
}
}
function visit<T extends Node>(
node: Node,
type: string,
callback: (node: T) => void
): void {
if (Array.isArray((node as any).children)) {
for (const child of (node as any).children) {
if (predicate(child)) { // criteria meets
callback(child as T); // call the transformer
}
visit(child, type, callback); // visit next node recursively
}
}
}
And, that’s it. Transformer function can be extended to add extra functionality. Mine removes width
and height
, add a CSS class, and selectively (if the svg does not have a skip-rehype-color
property) remove fill
and stroke
so I can provide my own colors to the shapes. I added the skip-*
flags in case I want to keep orignal colors and dimensions.
This is what I do with the CSS class:
.theme-markdown-svg {
fill: currentColor;
stroke: currentColor;
max-height: 50vh;
width: auto;
height: auto;
display: block;
}
Try changing the blog theme and see how these three examples change:
Rehype (Default Behaviour)
Skip Rehype coloring (Keeps resizing): manually add skip-rehype-color
like so:
<svg skip-rehype-color version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 185.88012871831165 134.434470714165" width="371.7602574366233" height="268.86894142833" >
Skip Rehype altogether (original SVG): add skip-rehype-all