Skip to main content

Change how Docusaurus will create img tags

Customize the way Docusaurus generates <img> tags to better control image rendering and behavior.

· 5 min read
View raw
Christophe
Markdown, WSL and Docker lover ~ Insatiable curious.
Cet article existe aussi en français — lire la version française.

This article is over a year old but was reviewed on July 30, 2026 — the content is still accurate.

Change how Docusaurus will create img tags

⚡
TL;DR

This post explains how to customize image rendering in Docusaurus by creating a custom <Image> React component. You'll learn how to build the component, style it with CSS, and then use a custom Remark plugin to automatically replace standard Markdown images with your new component. The guide also covers how to configure the plugin to skip the first image in a post and how to register it in your Docusaurus configuration, giving you full control over how images are displayed.

I'll start a series about writing components for Docusaurus.

Since it's always nice to start with something very concrete and practical, here's what we're going to do: we'll intercept the conversion from ![Alt text](./img/example.webp) to a HTML <img> tag. We'll force Docusaurus to use instead a component <Image> from our own.

We'll customize our <Image> tag to first inject a <div> parent, we'll force our <img> with attributes like CSS, lazy loading, ... but, not for all tags in the blog post since we'll skip the first one.

Indeed, the blog post introduction image should remains unchanged; we'll start to update the other images.

And here is the result of our own, self-made, <Image> component:

A happy meerkat

As you can see, the image is centered, has a hover effect, rounded corners, ... If you look at the HTML code, you'll see the loading="lazy" attribute.

That's pretty cool: our component is working! Let's build it.

We need to do several things so let's start with the first one: we need to create our <Image> component.

1. Creation of our Image component​

Writing a component for Docusaurus is quite easy. You should create a new folder in the existing src/components one.

In this article, we'll create a Image component so let's create the src/components/Image/index.tsx file with this content:

/**
* 🖼️ Image Component
*
* A lightweight wrapper for rendering images in Docusaurus with base URL resolution.
* Ensures that image paths are correctly resolved relative to the site's base URL,
* making it ideal for static assets stored in the `static/img` directory.
*
* 🔍 Behavior:
* - Uses `useBaseUrl()` to resolve the image path
* - Applies scoped styling via CSS modules
* - Supports optional `alt` and `title` attributes for accessibility and tooltips
* - Enables lazy loading for performance optimization
*/
import type { JSX } from "react";
import styles from "./styles.module.css";
import useBaseUrl from "@docusaurus/useBaseUrl";
interface Props {
/** Path to the image (e.g. `/img/example.png`), or a `require()`d asset */
src: string | { default: string };
/** Tooltip text shown on hover */
title?: string;
/** Alternative text for accessibility (if missing, reuse the Title property) */
alt?: string;
}
export default function Image({ src, alt, title }: Props): JSX.Element {
const isAbsolutePath = typeof src === "string" && src.startsWith("/");
const resolvedUrl = useBaseUrl(isAbsolutePath ? src : undefined);
const imgSrc = (isAbsolutePath ? resolvedUrl : src) as string;
return (
<div className={styles.container}>
<img src={imgSrc} alt={alt || title} title={title} loading="lazy" />
</div>
);
}

This is a React component that wraps image rendering for Docusaurus sites. It intelligently resolves image paths relative to the site's base URL when needed, applies CSS styling, and includes accessibility features like alt text and lazy loading for performance.

Our Image component will ask three parameters but just the img one is required. We can pass too a title (title) and the alternate text (alt).

As you can easily understand, the <Image> component will just create a div where an img tag will be present.

Nothing fancy except we'll use some CSS for the look&feel and the fact we'll force a lazy loading.

Please also create the src/components/Image/styles.module.css file:

.container {
text-align: center;
padding: 10px;
background: rgb(255, 255, 255, 0.15);
border: 1px solid rgb(255, 255, 255, 0.3);
border-radius: 16px;
backdrop-filter: blur(10px) saturate(180%);
}
.container img {
border-radius: 12px;
box-shadow: 0 8px 20px rgb(0, 0, 0, 0.15);
transition:
transform 0.4s ease,
box-shadow 0.4s ease,
filter 0.4s ease;
}
.container img:hover {
transform: scale(1.05) translateY(-4px);
box-shadow: 0 12px 25px rgb(0, 0, 0, 0.25);
filter: brightness(1.05) saturate(1.1);
}

This CSS defines a glassmorphic container with a frosted-glass effect and rounded corners, plus styling for images inside it that enlarge and glow when you hover over them. The container uses a semi-transparent background with a blur effect to create a modern, layered look, while the images animate smoothly when interacted with.

2. Tell Docusaurus about our component​

Before being able to use the <Image> component, we should tell Docusaurus what to do when it'll see it.

Please also edit the src/theme/MDXComponents.js file (and if not present, please create it)

import MDXComponents from "@theme-original/MDXComponents";
import Image from "@site/src/components/Image";
export default {
// Reusing the default mapping
...MDXComponents,
Image,
};

This file exports a custom set of components for MDX (a markdown extension that lets you write React components in markdown files). It takes all the default MDX components and adds or overrides the Image component with a custom version from the project.

Everything is now in place.

3. Using the Image component​

From now, in a blog post, instead of writing

![A happy meerkat](./images/happy.webp)

we can write

<Image src={require("./images/happy.webp").default} title="A happy meerkat" />

The require part is needed because the image is located in a subfolder of the blog post. That's exactly the tag rendered at the top of this article.

If the source was something like /img/happy.jpg then the code is easier:

<Image src="/img/happy.jpg" title="A happy meerkat" />

Caution

But, uh oh, on this blog, I've more than 250 articles right now (August 2025), I'm not going to go through all my articles to replace my images, am I?

But also, maybe tomorrow I don't want this tag anymore, so I want to keep my articles in vanilla Markdown (standard language); how can I do that? The answer is: just write a plugin (I've used the very same trick for my search&replace plugin).

The plugin will be able to intercept the Markdown code of the article and manipulate it. That's exactly what we want here!

We'll intercept the conversion of images from Markdown to HTML and tell Docusaurus to use our Image tag instead of img.

4. Writing and registering a plugin (optional — only if you have hundreds of articles like me)​

If you only have a handful of posts, it's simpler to just edit them by hand and skip this section. So we need a plugin to intercept the conversion from ![Alt text](./img/example.webp) to a HTML image tag; automatically.

Caution

But let's add a complexity: we don't want to change the first article i.e. the blog post introduction image. For that first image, let Docusaurus do its job (it's doing this really fine). We'll intercept as from the second image of the blog post.

Please create the plugins/remark-image-transformer/index.cjs file:

/**
* @fileoverview
* A Remark plugin for transforming Markdown image nodes (`![alt](url)`) into MDX-compatible `<Image />` components.
* This plugin is tailored for Docusaurus environments, where image paths often need to be resolved using `require()` or `useBaseUrl()`.
* It supports:
* - Relative paths (e.g. `./image.png`) → converted to `require()` expressions
* - Absolute paths (e.g. `/img/banner.png`) → passed directly, as Docusaurus resolves them
* - External URLs → passed directly as string literals
* Additionally, it preserves `alt` and `title` attributes.
*
* @module remarkReplaceImgToImage
*/
// cspell:ignore mdxjs
const { visit } = require("unist-util-visit");
const acorn = require("acorn");
/**
* Parses a JavaScript expression string into an ESTree-compatible expression node.
*
* @param {string} expression - A valid JavaScript expression as a string.
* @returns {object} ESTree expression node representing the parsed expression.
*/
function expressionToEstree(expression) {
return acorn.parse(expression, {
ecmaVersion: 2020,
sourceType: "module",
}).body[0].expression;
}
/**
* Creates an MDX JSX attribute node with an embedded JavaScript expression.
*
* @param {string} name - The name of the JSX attribute (e.g. "src").
* @param {string} expr - A JavaScript expression string to embed in the attribute.
* @returns {object} MDX AST node representing the JSX attribute with expression value.
*/
function makeJsxAttributeExpression(name, expr) {
return {
type: "mdxJsxAttribute",
name,
value: {
type: "mdxJsxAttributeValueExpression",
value: expr,
data: {
estree: {
type: "Program",
body: [
{
type: "ExpressionStatement",
expression: expressionToEstree(expr),
},
],
sourceType: "module",
},
},
},
};
}
/**
* Remark plugin that replaces Markdown image nodes with MDX `<Image />` components.
*
* @param {object} [options={}] - Plugin configuration options.
* @param {boolean} [options.skipFirst=true] - Whether to skip transforming the first image node.
* @returns {function} Transformer function to be used by Remark.
*/
function remarkReplaceImgToImage({ skipFirst = true } = {}) {
return (tree) => {
let imageCount = 0;
visit(tree, "image", (node, index, parent) => {
imageCount++;
if (skipFirst && imageCount === 1) return;
const url = node.url || "";
const attributes = [
{ type: "mdxJsxAttribute", name: "alt", value: node.alt || "" },
];
if (node.title)
attributes.push({
type: "mdxJsxAttribute",
name: "title",
value: node.title,
});
if (/^\.+\//.test(url)) {
// Relative to MD file -> use require()
const expr = `require("${url}").default`;
attributes.unshift(makeJsxAttributeExpression("src", expr));
} else if (/^\//.test(url)) {
// Absolute -> just pass the path, Docusaurus will handle it.
attributes.unshift({
type: "mdxJsxAttribute",
name: "src",
value: url,
});
} else {
// Fallback: external URL
attributes.unshift({
type: "mdxJsxAttribute",
name: "src",
value: url,
});
}
const imageNode = {
type: "mdxJsxFlowElement",
name: "Image",
attributes,
children: [],
};
parent.children.splice(index, 1, imageNode);
});
// The useBaseUrl import is no longer needed.
};
}
module.exports = remarkReplaceImgToImage;

This is a Remark plugin that converts Markdown image syntax (`![alt](url)`) into MDX `<Image />` components, intelligently handling three types of image paths: relative paths get wrapped in `require()` calls, absolute paths are passed through as-is for Docusaurus to resolve, and external URLs are treated as string literals. It optionally skips the first image in a document.

This plugin is more complex because we need to make sure paths to images are correctly handled (we use relative paths like ![Example image](./img/example.webp) or absolute (![Example image](/img/example.webp)) or even external ones).

Once the plugin has been created, we need to update the Docusaurus configuration to load it.

To do this, edit your docusaurus.config.js file and add the highlighted lines as illustrated below.

const config = {
// [ ... ]
presets: [
[
'classic',
({
blog: {
// highlight-next-line
beforeDefaultRemarkPlugins: [require('./plugins/remark-image-transformer')],
},
// [ ... ]
}),
],
],
};
export default config;

This code exports a configuration object for what appears to be a Docusaurus site. It sets up a 'classic' preset with customizations, specifically injecting a custom remark plugin that transforms images in blog posts before any of the default image processing happens.

Now restart your Docusaurus server (so the plugin can be registered).

If everything is correctly in place, open any of your previous blog post and you'll see it's working.

Conclusion​

Now, up to you to edit the src/components/Image/styles.module.css file and use your own CSS.

And once every image goes through your own component, it becomes easy to audit them: see Running some checks on your Docusaurus images.


🦋 Join the conversation on BlueskyRequires a free Bluesky account to like, repost or comment. This block talks to Bluesky's public API directly from your browser (no cookies, no sign-in) — that means your IP address reaches Bluesky's servers just by this section loading.

Loading comments…

Was this article helpful?
Did you try the steps in this article?