markdown-it-container-details-heading
v1.0.2
Published
Plugin to add heading to summary of details in markdown-it-container that to show it in the outline or contents
Downloads
45
Maintainers
Readme
markdown-it-container-details-heading
A markdown-it plugin that adds headings to the <summary> element inside <details> containers, so they appear in the page outline and table of contents.
Why Use This Plugin?
This plugin is especially important for FAQ pages.
In a typical FAQ page, each question is placed inside a <details> element so that only the questions are visible when the page loads, and users click on a question to expand the answer. However, since the question text sits directly inside a <summary> element rather than a proper heading element (<h1>–<h6>), it suffers from two major discoverability problems:
Missing from the outline / table of contents. The page outline (or TOC sidebar) only lists heading elements. Because a plain
<summary>is not a heading, FAQ questions are invisible in the outline. Users cannot scan or jump directly to a specific question from the outline.Search won't take you there. Document search features (such as VitePress's built-in search or Algolia) typically match and navigate to heading elements, not body text. Even if a search query matches the text inside a
<summary>, the page will only scroll to the nearest parent heading above the<details>block — not to the question itself. The user is left scanning the page manually to find the answer they searched for.
With this plugin, you can optionally designate the summary text as a heading by prefixing it with markdown heading syntax (##, ###, etc.). This injects a real heading element inside the <summary>, making it visible in the outline and directly targetable by search — without changing the visual appearance.
[!IMPORTANT]
Using a heading inside<summary>is entirely opt-in. If you omit the#prefix, the plugin falls back to the original<details>/<summary>behavior. This means you can use heading behavior on FAQ pages where it matters, while keeping the default behavior everywhere else.
Installation
# npm
npm install markdown-it-container-details-heading
# yarn
yarn add markdown-it-container-details-heading
# pnpm
pnpm add markdown-it-container-details-headingUsage
import markdownit from "markdown-it";
import containerPlugin from "markdown-it-container";
import containerDetailsHeadingPlugin from "markdown-it-container-details-heading";
const md = markdownit();
// The `markdown-it-container` plugin is required as a dependency.
md.use(containerPlugin);
// Register this plugin after `markdown-it-container` and `markdown-it-attrs` (if used).
md.use(containerDetailsHeadingPlugin, {
/* options */
});With VitePress
// .vitepress/config.js
import containerDetailsHeadingPlugin from "markdown-it-container-details-heading";
export default {
markdown: {
config: md => {
md.use(containerDetailsHeadingPlugin);
},
},
};How It Works
Inside your markdown files, use the details container as usual. Prefix the summary text with markdown heading syntax (# through ######) to enable heading behavior:
Without heading (original behavior — no heading element, stays as-is):
::: details Plain Summary Text
Content goes here.
:::
With heading (summary becomes <h2> with an id):
::: details ## I Am a Level-2 Heading
Content goes here.
:::The heading level you specify determines the HTML tag used (# → <h1>, ## → <h2>, ..., ###### → <h6>). The heading text is automatically slugified into an id attribute, turning the heading into a navigable anchor.
Rendered HTML
<!-- Without heading: original behavior preserved -->
<details class="details custom-block">
<summary>Plain Summary Text</summary>
<p>Content goes here.</p>
</details>
<!-- With heading: a heading element is injected -->
<details class="details custom-block details-h2">
<summary><h2 id="i-am-a-level-2-heading">I Am a Level-2 Heading</h2></summary>
<p>Content goes here.</p>
</details>Markdown Inside Headings
Inline markdown syntax (bold, strikethrough, code, etc.) inside the heading text is rendered correctly:
::: details ### ~~This Is Outdated~~ and **Still Useful**
Content
:::
::: details #### Why Can't `process.env` Get Variables?
Content
:::Custom ID
You can manually specify an id using the markdown-it-attrs syntax. When provided, it takes precedence over the auto-generated slug:
::: details ## Custom Title {#my-custom-id}
Content
:::Options
All class options can be either a string or a callback function that receives the heading level and text:
type ContainerDetailsHeadingPluginClassHandler = (
headingLevel: number | null,
headingText: string | null,
rawText: string,
) => string;[!IMPORTANT]
If the<summary>of a<details>element is not a heading, theheadingLevelandheadingTextparameter will benull.
So you have to manually handle them to make sure that the class(es) of a normal<details>element should work properly.
You can import the type of the plugin options and the callback function from the package if you are using TypeScript.
detailsClass
- Type:
string | ContainerDetailsHeadingPluginClassHandler - Default:
- Heading Details:
`details custom-block h${headingLevel}` - Normal Details:
"details custom-block"
- Heading Details:
CSS class(es) for the <details> element. The default value matches the VitePress default theme, so the plugin works out of the box with VitePress. If you do not need this default class, simply set this option to an empty string ("").
// Remove the default class:
md.use(containerDetailsHeadingPlugin, { detailsClass: "" });
// Use a custom static class:
md.use(containerDetailsHeadingPlugin, { detailsClass: "my-details" });
// Compute the class dynamically:
md.use(containerDetailsHeadingPlugin, {
detailsClass: (level, text, raw) =>
level ? `custom-details h${level}` : "custom-details",
});summaryClass
- Type:
string | ContainerDetailsHeadingPluginClassHandler - Default:
""
CSS class(es) for the <summary> element.
headingClass
- Type:
string | ContainerDetailsHeadingPluginClassHandler - Default:
""
CSS class(es) for the heading element (<h1>–<h6>) inside the summary.
Compatibility with markdown-it-attrs
This plugin is fully compatible with markdown-it-attrs. You can add custom attributes to the <details> element using the {...} syntax. When a heading is used, the id attribute is automatically moved from the <details> to the heading element, while all other attributes remain on the <details>:
::: details ## Custom Title {#my-custom-id .additional-class data-custom-attr="true" open}
Content
:::<details class="additional-class details custom-block details-h2" data-custom-attr="true" open>
<summary><h2 id="my-custom-id">Custom Title</h2></summary>
<p>Content</p>
</details>Improved Theme Styles
This package ships with an improved CSS theme for the <details> element that looks better than VitePress's default styling:
import "markdown-it-container-details-heading/vitepress-theme.css";The theme features:
- Smooth expand/collapse animation with a rotating chevron icon.
- Clean divider lines between adjacent details blocks.
- Hover effects on the summary and border.
- Headings inside summaries are visually reset to inherit from the summary, so they blend in seamlessly.
You can use this stylesheet as-is or use it as a starting point for your own custom styles.
Nested Details
Nested <details> blocks are fully supported:
:::: details ## Parent Heading
Parent content
::: details ### Child Heading
Child content
:::
::::