Change how Docusaurus will create img tags
Customize the way Docusaurus generates <img> tags to better control image rendering and behavior.
This article is over a year old but was reviewed on July 30, 2026 — the content is still accurate.
This article is part of the Creating Docusaurus components series:
- Change how Docusaurus will create img tags
- Overrides the generation of img tags with Docusaurus
- Organize Your Docusaurus Content with a Custom Series Component
- Providing a go to the top button in Docusaurus posts
- Creating a search&replace plugin for Docusaurus
- A component for showing code snippets in a Docusaurus blog
- A Docusaurus component that alerts readers the article is over a year old
- Showing the changelog of your post
- Inject ASCII Art in any HTML pages rendered by Docusaurus
- Best Practice - Customizing the Docusaurus RSS Feed for Full Content & Images
- Running some checks on your Docusaurus images
- Tags management in Docusaurus
- Automating TL;DR Summaries with Gemini AI
- Introducing the ProjectSetup Component - A Standardized Way to Share Project Structures
- How to indicate AI-assisted content in a Docusaurus blog
- Bring Your CLI Tutorials to Life with a Typewriter Terminal
- Meerkat Mischief: Sprinkling Easter Eggs Across my Blog
- AI-Powered Code Tooltips in Docusaurus — Explain Like I'm Five
- Adding Reader Reactions to Your Docusaurus Blog
- Your Blog Is Unreadable to an AI. Here Is the Fix.
- Shake Your Phone, Startle the Meerkat
- Add a "Copy as Markdown" Button to Your Docusaurus Blog
- Ask My Blog: Turning 259 Articles Into 2,160 Searchable Questions
- The Third Door: A Floating Chat Bubble, and the Feature Nobody Could Find
- Drawing a Map of My Own Blog
- Let Readers Flag Issues — A Multi-Type Feedback Widget for Docusaurus

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  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:

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

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  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 (``) 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 (``) 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  or absolute () 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.
This article is part of the Creating Docusaurus components series:
- Change how Docusaurus will create img tags
- Overrides the generation of img tags with Docusaurus
- Organize Your Docusaurus Content with a Custom Series Component
- Providing a go to the top button in Docusaurus posts
- Creating a search&replace plugin for Docusaurus
- A component for showing code snippets in a Docusaurus blog
- A Docusaurus component that alerts readers the article is over a year old
- Showing the changelog of your post
- Inject ASCII Art in any HTML pages rendered by Docusaurus
- Best Practice - Customizing the Docusaurus RSS Feed for Full Content & Images
- Running some checks on your Docusaurus images
- Tags management in Docusaurus
- Automating TL;DR Summaries with Gemini AI
- Introducing the ProjectSetup Component - A Standardized Way to Share Project Structures
- How to indicate AI-assisted content in a Docusaurus blog
- Bring Your CLI Tutorials to Life with a Typewriter Terminal
- Meerkat Mischief: Sprinkling Easter Eggs Across my Blog
- AI-Powered Code Tooltips in Docusaurus — Explain Like I'm Five
- Adding Reader Reactions to Your Docusaurus Blog
- Your Blog Is Unreadable to an AI. Here Is the Fix.
- Shake Your Phone, Startle the Meerkat
- Add a "Copy as Markdown" Button to Your Docusaurus Blog
- Ask My Blog: Turning 259 Articles Into 2,160 Searchable Questions
- The Third Door: A Floating Chat Bubble, and the Feature Nobody Could Find
- Drawing a Map of My Own Blog
- Let Readers Flag Issues — A Multi-Type Feedback Widget for Docusaurus
Loading comments…





