npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

astro-mdx-kit

v0.2.0

Published

Astro integration for MDX directive-to-component mapping, element overrides, and auto-imports.

Readme

astro-mdx-kit

NPM Package astro-mdx-kit License: MIT CI

Astro integration for MDX directive-to-component mapping, element overrides, and auto-imports.

[!WARNING]

This project is under development. It should not be considered suitable for general use until a 1.0 release.

Overview

MDX makes it easy to embed components in your Markdown files, but this can lead to tight coupling between your content and its presentation. The directives syntax proposal has been circling the runway in the CommonMark project since 2014. It specifies implementation-agnostic syntax for defining component-like data in your Markdown. It's yet to land, but there's decent support for it in just about every major Markdown toolchain.

So instead of:

import Widget from '../components/Widget.astro'

<Widget greeting="hello" />

Let's write:

::Widget{greeting="hello"}

And then, with some help from astro-mdx-kit, easily map ::Widget to its Astro implementation outside your Markdown:

mdxKit({
  directives: {
    Widget: 'src/components/Widget.astro',
  },
})

It's not necessarily pretty, but it is comparatively decoupled and portable.

In addition to support for mapping directives to, astro-mdx-kit bundles some additional tools I end up needing most of the time:

  • Directives
    Map Markdown directive syntax (:name, ::name, :::name) to Astro components.
  • Element overrides
    Replace HTML elements (h1, img, etc.) with custom Astro components.
  • Auto-imports
    Automatically import components and assets (like images) without manual import statements.
  • Image captions
    Extract caption text adjacent to images and wrap in <figure>/<figcaption> or pass to components.
  • Attribute lists
    Kramdown-style {:key="value"} syntax for adding attributes to any Markdown element.
  • Image unwrapping
    Remove <p> wrappers from stand-alone images.
  • Phrasing unwrapping
    Remove invalid <p> elements nested inside phrasing-only HTML elements like <span>, <button>, and <label>.
  • Frontmatter injection
    Expose raw MDX source or the parsed AST tree in frontmatter.

Available as an Astro integration, a standalone remark plugin, or as individual sub-plugins for use in any unified pipeline.

Astro's architecture (currently) means that this syntax still must live in a .mdx file instead of .md, but it still helps the long term portability your Markdown content to use platform-agnostic syntax like directives instead of importing and marking up concrete components.

Getting started

Prerequisites

We'll assume you have an Astro project set up.

You will also need @astrojs/mdx (or a framework that includes it, like Starlight) for MDX file processing.

Installation

pnpm add astro-mdx-kit @astrojs/mdx

Basic setup

The simplest way to use astro-mdx-kit is as an Astro integration:

// Astro.config.ts
import mdx from '@astrojs/mdx'
import mdxKit from 'astro-mdx-kit'
import { defineConfig } from 'astro/config'

export default defineConfig({
  integrations: [
    mdxKit({
      // All options are optional — only enable what you need
      attributes: true,
      captionImages: true,
      directives: {
        // Replace `::Widget` directives
        // with `Widget.astro` component
        Widget: 'src/components/Widget.astro',
      },
      elements: {
        // Customize `# Heading` elements
        h1: 'src/components/Heading.astro',
      },
      unwrapImages: true,
    }),
    mdx(),
  ],
})

Alternative: remark plugin

For direct control over the remark plugin pipeline, use remarkMdxKit which returns a typed [plugin, options] tuple with full autocomplete on the options object:

// Astro.config.ts
import mdx from '@astrojs/mdx'
import { remarkMdxKit } from 'astro-mdx-kit'
import { defineConfig } from 'astro/config'

export default defineConfig({
  integrations: [mdx()],
  markdown: {
    remarkPlugins: [
      remarkMdxKit({
        directives: {
          /* ... */
        },
        elements: {
          /* ... */
        },
      }),
    ],
  },
})

The raw remark plugin is also available via astro-mdx-kit/remark for direct use in unified pipelines:

import remarkMdxKitPlugin from 'astro-mdx-kit/remark'
import remarkParse from 'remark-parse'
import { unified } from 'unified'

unified().use(remarkParse).use(remarkMdxKitPlugin, options)

Individual sub-plugins

Each feature is also available as a standalone remark plugin:

import {
  remarkMdxKitAttributes, // Markdown or MDX
  remarkMdxKitCaptionImages, // Markdown or MDX
  remarkMdxKitDirectives, // MDX + Astro
  remarkMdxKitElements, // MDX + Astro
  remarkMdxKitFrontmatterInject, // Markdown or MDX + Astro
  remarkMdxKitUnwrapImages, // Markdown or MDX
  remarkMdxKitUnwrapPhrasingContent, // MDX
} from 'astro-mdx-kit'

Features

Directives

Map remark-directive syntax to Astro components. All three directive forms (container, leaf, text) are supported — the type is determined automatically by how you write it in Markdown. remark-directive is wired up automatically — you don't need to add it to your remarkPlugins array manually. (It's still a peer dependency, so install it alongside astro-mdx-kit or rely on your package manager's hoisting.)

mdxKit({
  directives: {
    // With auto-import: image paths are imported as modules
    Picture: {
      autoImport: 'src',
      component: 'Picture',
      componentModule: 'astro:assets',
    },
    // Simple: map directive name to a component file
    Widget: 'src/components/Widget.astro',
  },
})

Markdown:

::Widget{icon="star"}

:::Widget{type="warning"}
Content inside the directive.
:::

::Picture{src="../assets/hero.png" alt="Hero image"}

What happens:

  • ::Widget{icon="star"} becomes <Widget icon="star" />
  • The component is automatically imported — no manual import needed
  • With autoImport: 'src', the src prop value is converted to an ESM import so Vite can process the asset

Prop remapping

Use propMap to rename directive attributes before they become component props. The original attribute name is dropped.

mdxKit({
  directives: {
    Widget: {
      component: 'src/components/Widget.astro',
      propMap: { icon: 'iconName', type: 'variant' },
    },
  },
})

::Widget{icon="star" type="warning"} becomes <Widget iconName="star" variant="warning" />. Unmapped attributes pass through as-is.

Label extraction

Directives support a [label] syntax (e.g., :::Callout[Warning Title] or ::Tag[content]). By default, this content is included in the component's children, which is consistent with the directives specification. In certain cases, it can make more sense for this content to end up elsewhere in the receiving component. Use the label option to extract it into a named prop instead:

mdxKit({
  directives: {
    Callout: {
      component: 'src/components/Callout.astro',
      label: 'title',
    },
  },
})

Markdown:

:::Callout[Watch out!]
Something important.
:::

Output: <Callout title="Watch out!">Something important.</Callout>

The label is removed from children and serialized as plain text by default. For richer formatting, use the object form:

label: { prop: 'title', format: 'rendered' }

| Format | Output | | ------------ | -------------------------------------------- | | 'plain' | title="Watch out!" (default) | | 'raw' | title="**Watch** out!" (raw Markdown) | | 'rendered' | title="<strong>Watch</strong> out!" (HTML) |

Label extraction works for all directive types:

  • Container (:::Name[label]): The [label] paragraph is extracted from children and serialized as a prop. Body content is preserved.
  • Leaf (::Name[content]): The [content] is serialized as a prop and removed from children.
  • Text (:Name[content]): Same as leaf — [content] becomes a prop.

Without the label config, all directive types preserve their default behavior (content stays in children). If no [label] / [content] is present in the markdown, the option has no effect.

Element overrides

Replace standard HTML elements rendered by Markdown with custom Astro components.

mdxKit({
  elements: {
    // Simple: override heading rendering
    h1: 'src/components/Heading.astro',
    // With auto-import: override images with Astro's Picture component
    img: {
      autoImport: 'src',
      component: 'Picture',
      componentModule: 'astro:assets',
    },
  },
})
  • Simple overrides (like h1) use MDX's export const components mechanism, covering both Markdown syntax and raw HTML/JSX
  • Auto-import overrides (like img) use direct AST transformation so that asset paths are converted to ESM imports for Vite processing

Element keys aren't limited to standard HTML element names — you can use any JSX tag name, including PascalCase custom components. This lets you auto-import components that use MDX-style markup without an explicit import statement in each file:

mdxKit({
  elements: {
    Excerpt: 'src/components/Excerpt.astro',
  },
})

Now <Excerpt /> works in any MDX file without importing it. Note that the directives syntax (e.g. ::Excerpt) is generally preferred for portability, since directives degrade gracefully in non-MDX Markdown renderers while JSX tags do not.

Auto-import prop remapping

When the source attribute name differs from the target prop name, use the { from, to } form:

mdxKit({
  elements: {
    img: {
      autoImport: { from: 'src', to: 'srcImported' },
      component: 'src/components/CustomImage.astro',
    },
  },
})

This produces <CustomImage srcImported={importedModule} src="../original/path.jpg" /> — the imported module on the to prop, with the original string preserved on the from prop.

Derived imports

autoImport accepts an array of entries to generate multiple imports from a single source path. Each entry can include a transform function that modifies the path before importing. If transform returns undefined, the derived import is skipped.

This is a bit of an edge case, but useful in cases where you want to pass multiple imported values to your component, such as generating both light and dark mode assets from the unplugin-tldraw package as illustrated below:

mdxKit({
  elements: {
    img: {
      autoImport: [
        // Primary import: import the src path as-is
        'src',
        // Derived import: generate a dark variant for .tldr files
        // Expects a srcDark prop on the receiving component...
        {
          from: 'src',
          to: 'srcDark',
          transform: (path) => (path.endsWith('.tldr') ? `${path}?dark=true&tldr` : undefined),
        },
      ],
      component: 'Picture',
      componentModule: 'astro-media-kit/components',
    },
  },
})

When ![Alt](./sketch.tldr) is processed, this generates:

import _img0 from './sketch.tldr'
import _img1 from './sketch.tldr?dark=true&tldr'
;<Picture alt="Alt" src={_img0} srcDark={_img1} />

For non-.tldr images, the transform returns undefined and the srcDark prop is omitted.

This also works on directives:

mdxKit({
  directives: {
    Picture: {
      autoImport: ['src', { from: 'src', to: 'srcDark', transform: myTransform }],
      component: 'Picture',
      componentModule: 'astro-media-kit/components',
    },
  },
})
tldraw preset

A ready-to-use derived import entry for .tldr dark mode is available as a preset:

import mdxKit, { tldrawDarkImport } from 'astro-mdx-kit'

mdxKit({
  elements: {
    img: {
      autoImport: ['src', tldrawDarkImport],
      component: 'Picture',
      componentModule: 'astro-media-kit/components',
    },
  },
})

This requires @kitschpatrol/unplugin-tldraw to be configured in your build pipeline (e.g. via astro-media-kit's tldraw: true integration option).

Astro image presets

Pre-configured element overrides for Astro's built-in <Image> and <Picture> components are available as presets:

import mdxKit, { astroImage } from 'astro-mdx-kit'

mdxKit({
  elements: { img: astroImage },
})
import mdxKit, { astroPicture } from 'astro-mdx-kit'

mdxKit({
  elements: { img: astroPicture },
})

Both presets configure autoImport: 'src' with the corresponding component from astro:assets.

Image captions

Extract text that follows an image in the same paragraph and handle it as a caption.

Global captions

Wrap all captioned images in <figure>/<figcaption>:

mdxKit({
  captionImages: true,
})

Markdown:

![Alt text](./photo.jpg)
A beautiful place out in the country.

Output:

<figure>
  <img src="..." alt="Alt text" />
  <figcaption>A beautiful place out in the country.</figcaption>
</figure>

The original image node is preserved, so Astro's built-in image optimization still applies.

Note that the <p> wrapper is always removed when adding a caption, regardless of whether the unwrapImages option is set.

Per-element captions

When using an img element override (for example), configure caption handling on the element config:

mdxKit({
  elements: {
    img: {
      autoImport: 'src',
      // Wrap in <figure>/<figcaption>
      caption: 'figure',
      component: 'src/components/FancyImage.astro',
      // Or pass caption as children of the component:
      // caption: 'children',
      // Or serialize and pass as a string prop:
      // caption: { prop: 'caption' },
      // caption: { prop: 'caption', format: 'raw' },
      // caption: { prop: 'caption', format: 'rendered' },
    },
  },
})

Caption modes:

| Mode | Output | | ----------------------------------------- | ----------------------------------------------------------------------- | | 'figure' | <figure><Picture .../><figcaption>Caption</figcaption></figure> | | 'children' | <Picture ...>Caption</Picture> | | { prop: 'caption' } | <Picture ... caption="Caption text" /> (plain text) | | { prop: 'caption', format: 'raw' } | <Picture ... caption="**Bold** caption" /> (raw Markdown) | | { prop: 'caption', format: 'rendered' } | <Picture ... caption="<p><strong>Bold</strong> caption</p>" /> (HTML) |

If both captionImages (global) and per-element caption are set, the element override takes precedence for overridden images.

This might seem a bit fussy, but it can be useful for handling the caption content differently in your custom component.

Attribute lists

Enable Kramdown-style attribute list syntax for adding attributes to Markdown elements:

mdxKit({
  attributes: true,
})

Markdown:

A paragraph with a class.
{:.highlight}

[A link](https://example.com){:target="\_blank" rel="noopener noreferrer"}

![Image](./photo.jpg){:data-lightbox="true"}

Syntax rules:

  • Block elements (headings, paragraphs, blockquotes): attributes go on the next line after the element
  • Inline elements (links, emphasis, images): attributes go directly after on the same line
  • ID: {:#my-id}, class: {:.my-class}, arbitrary: {:key="value"}

Attribute lists work with element overrides — when a Markdown element is replaced by a custom component via the elements option, any attributes set via {:key="value"} are forwarded as props to the component. For simple overrides, attributes flow through MDX's component mechanism automatically. For auto-import overrides (like img), attributes are forwarded to the final component during AST transformation.

Compatible with directive syntax — both can be used simultaneously in the same file, but using both directive and attribute list syntax on the same element is redundant and not supported.

Unwrap images

Remove the <p> wrapper that Markdown adds around stand-alone images:

mdxKit({
  unwrapImages: true,
})

By default, ![alt](src) on its own line produces <p><img ...></p>. With unwrapImages: true, the paragraph is removed so the image is a direct child of the document flow. Works with both native images and component overrides (img, Image, and Picture are recognized by default). When using remarkMdxKitUnwrapImages as a standalone plugin, pass imageComponentNames to customize which JSX element names are treated as images.

Unwrap phrasing

Remove <p> elements that Markdown incorrectly nests inside HTML elements that only allow phrasing content:

mdxKit({
  unwrapPhrasingContent: true,
})

In MDX, writing block content inside elements like <span> or <button> causes Markdown to wrap the text in <p> tags, producing invalid HTML:

<span>Some text</span>

<!-- Produces: <span><p>Some text</p></span> — invalid! -->

With unwrapPhrasingContent: true, the <p> is replaced with its children, producing valid <span>Some text</span>.

This targets all elements that cannot contain <p> per the HTML spec: span, em, strong, small, s, cite, q, dfn, abbr, code, var, samp, kbd, sub, sup, i, b, u, mark, bdi, bdo, data, time, ruby, button, label, and output. Elements with flow content models like <div> and <a> (transparent) are not affected.

Frontmatter injection

Expose the raw MDX source or the parsed AST tree in frontmatter. Useful for debugging or in layouts and components:

mdxKit({
  // Inject the MDAST tree as frontmatter.mdast
  mdast: true,
  // Or use a custom key:
  // rawMdx: 'source',
  // Inject raw source as frontmatter.rawMdx
  rawMdx: true,
  // Or use a custom key:
  // mdast: 'tree',
})
  • rawMdx captures the original file content before any transforms
  • mdast captures the AST after astro-mdx-kit transforms but before rehype/MDX compilation
  • Both use ??= so they won't overwrite existing frontmatter values

Logging

astro-mdx-kit uses lognow for logging. You can inject your own logger:

import { setLogger } from 'astro-mdx-kit'

setLogger(console)

Processing order

The plugin processes content in two phases:

Parse phase (before transforms):

  1. Directive parser — registers :::/::/: syntax extensions
  2. Attribute lists — applies {:...} attributes to nodes

Transform phase (in order):

  1. Raw MDX injection — captures original source
  2. Directive transforms — converts directives to JSX components
  3. Element overrides — replaces HTML elements with components (per-element captions handled here)
  4. Global image captions — wraps remaining captioned images in <figure>
  5. Unwrap phrasing — removes <p> from inside phrasing-only elements
  6. Unwrap images — removes <p> from stand-alone images
  7. MDAST injection — captures the transformed tree

Full configuration example

// Astro.config.ts
import mdx from '@astrojs/mdx'
import mdxKit, { tldrawDarkImport } from 'astro-mdx-kit'
import { defineConfig } from 'astro/config'

export default defineConfig({
  integrations: [
    mdxKit({
      attributes: true,
      captionImages: true,
      directives: {
        Callout: {
          component: 'src/components/Callout.astro',
          label: 'title',
          propMap: { type: 'variant' },
        },
        Picture: {
          autoImport: 'src',
          component: 'Picture',
          componentModule: 'astro:assets',
        },
      },
      elements: {
        h1: 'src/components/Heading.astro',
        img: {
          autoImport: ['src', tldrawDarkImport],
          caption: 'figure',
          component: 'Picture',
          componentModule: 'astro-media-kit/components',
        },
      },
      mdast: true,
      rawMdx: true,
      unwrapImages: true,
      unwrapPhrasingContent: true,
    }),
    mdx(),
  ],
})

MDX VS Code Plugin Integration

If you are working in VS Code with MDX files, you'll need to handle some additional configuration to help the VS Code MDX extension understand the non-standard attribute and directive syntax.

Note: If you're using @kitschpatrol/shared-config or are building from a @kitschpatrol/create-project template, skip to step 3.

  1. Install remark plugin dependencies:

    pnpm install -D remark-attribute-list remark-directive

    These dependencies must be hoisted to be discoverable by the VS Code plugin.

  2. Create or update a .remarkrc.js in your project root:

    // .remarkrc.js
    import remarkAttributeList from 'remark-attribute-list'
    import remarkDirective from 'remark-directive'
    
    export default {
      plugins: [remarkAttributeList, remarkDirective],
    }
  3. Add the remark plugins to a mdx field in your tsconfig.json:

    // tsconfig.json
    {
      "compilerOptions": {
        // ...
      },
      "mdx": {
        "plugins": ["remark-directive", "remark-attribute-list"],
      },
    }

Maintainers

kitschpatrol

Acknowledgments

This project was heavily inspired by Christian Fuss' m2dx project and tomixy's astro-mdx-directive.

Though I didn't find it until after developing astro-mdx-kit, Florian's astro-custom-embeds looks great and it looks like we both arrived at similar approaches to configuration API.

Gratitude is always due to the unified team for remark and their entire ecosystem of AST-wrangling libraries and tools.

Contributing

Issues are welcome and appreciated.

Please open an issue to discuss changes before submitting a pull request. Unsolicited PRs (especially AI-generated ones) are unlikely to be merged.

This repository uses @kitschpatrol/shared-config (via its ksc CLI) for linting and formatting, plus MDAT for readme placeholder expansion.

License

MIT © Eric Mika