Skip to content

Details

The Details extension adds collapsible accordion blocks using semantic <details>/<summary> HTML. Each block has a clickable summary header and an expandable content area that can hold any block-level content (paragraphs, lists, code blocks, tables, etc.). The extension ships as a separate package (@domternal/extension-details).

Terminal window
pnpm add @domternal/extension-details

Click the toggle button (chevron) to expand/collapse a details block, or use the toolbar button to wrap/unwrap content.

With the default theme enabled. Click the details toolbar button to toggle wrapping, or use keyboard shortcuts inside the accordion.

Click to try it out
import { Editor, Document, Paragraph, Text, defaultIcons } from '@domternal/core';
import { Details } from '@domternal/extension-details';
import '@domternal/theme';
const editorEl = document.getElementById('editor')!;
// Toolbar with details toggle button
const toolbar = document.createElement('div');
toolbar.className = 'dm-toolbar';
toolbar.innerHTML = `<div class="dm-toolbar-group">
<button class="dm-toolbar-button" id="toggle-details">${defaultIcons.caretRight}</button>
</div>`;
editorEl.before(toolbar);
const editor = new Editor({
element: editorEl,
extensions: [Document, Paragraph, Text, Details],
content: '<details><summary>Click to expand</summary><div data-details-content><p>Hidden content here.</p></div></details>',
});
document.getElementById('toggle-details')!.addEventListener('click', () => {
editor.chain().focus().toggleDetails().run();
});
PropertyValue
ProseMirror namedetails
TypeNode
Groupblock
ContentdetailsSummary detailsContent (exactly one summary + one content)
AtomNo
InlineNo
DefiningYes
IsolatingYes
SelectableYes
DraggableNo
Allow gap cursorNo
HTML tag<details> (parses <details> and <div data-type="details">)

Details is a block-level container node. It enforces a strict content model: exactly one detailsSummary followed by exactly one detailsContent. The isolating: true property prevents selections from crossing the details boundary.

The Details extension automatically registers two child nodes via addExtensions(). You do not need to import or configure them separately.

PropertyValue
ProseMirror namedetailsSummary
Contentinline* (text and marks only)
DefiningYes
IsolatingYes
SelectableNo
HTML tag<summary>

The clickable header of the accordion. Supports inline formatting (bold, italic, underline, code, links, etc.) but cannot contain block-level content like paragraphs or lists.

PropertyValue
ProseMirror namedetailsContent
Contentblock+ (one or more block nodes)
DefiningYes
SelectableNo
HTML tag<div data-details-content> (parses div[data-details-content] and div[data-type="detailsContent"])

The collapsible body of the accordion. Can contain any block-level content: paragraphs, lists, code blocks, tables, images, and even other extensions. Hidden by default via a [hidden] attribute; toggled visible by a custom DOM event dispatched from the parent NodeView.

OptionTypeDefaultDescription
persistbooleanfalseWhether the open/closed state is persisted in the document data. When false, open state is UI-only and resets on reload
openClassNamestring'is-open'CSS class applied to the NodeView wrapper when the details block is expanded
HTMLAttributesRecord<string, unknown>{}Custom HTML attributes added to the rendered <details> element

By default, the open/closed state is visual only and not stored in the document. When persist: true, the open attribute is saved as part of the document data:

import { Details } from '@domternal/extension-details';
const PersistentDetails = Details.configure({
persist: true,
});

This enables the openDetails, closeDetails, and setDetailsOpen commands, and ensures the open state survives serialization/deserialization.

OptionTypeDefaultDescription
HTMLAttributesRecord<string, unknown>{}Custom HTML attributes for the <summary> element
OptionTypeDefaultDescription
HTMLAttributesRecord<string, unknown>{}Custom HTML attributes for the <div data-details-content> element
AttributeTypeDefaultConditionDescription
openbooleanfalseOnly when persist: trueWhether the details block is expanded. Rendered as the HTML boolean open attribute

When persist: false (default), no attributes are stored. The open state is managed entirely through DOM classes and events in the NodeView.

Wraps the selected block(s) in a details structure. Creates a new details node with an empty summary and moves the selected content into the content area.

editor.commands.setDetails();
// With chaining
editor.chain().focus().setDetails().run();

Returns false if:

  • No block range is selected
  • The cursor is already inside a details node (nesting is not allowed)

After wrapping, the cursor is placed inside the empty summary for immediate editing.

Lifts content out of a details block. If the summary has text, it becomes a paragraph (or the parent’s default block type). All content blocks from the body are placed after it.

editor.commands.unsetDetails();
// With chaining
editor.chain().focus().unsetDetails().run();

Returns false if the cursor is not inside a details node.

Wraps or unwraps depending on the current context. If the cursor is inside a details node, it unwraps. If outside, it wraps the selected block(s).

editor.commands.toggleDetails();
// With chaining
editor.chain().focus().toggleDetails().run();

Handles multi-range selections (e.g., table cell selections): if all selected cells contain details, all are unwrapped. Otherwise, cells without details are wrapped.

Programmatically opens the details block at the cursor position. Requires persist: true.

editor.commands.openDetails();

Returns false if persist: false or the cursor is not inside a details node.

Programmatically closes the details block at the cursor position. Requires persist: true.

editor.commands.closeDetails();

Returns false if persist: false or the cursor is not inside a details node.

Sets the open state to a specific value. Requires persist: true.

editor.commands.setDetailsOpen(true); // Open
editor.commands.setDetailsOpen(false); // Close

Returns false if persist: false, the cursor is not inside a details node, or the state is already at the target value.

KeyAction
BackspaceAt start of summary: unwraps the details block (unsetDetails). In the middle: deletes one character (manual handling for Safari compatibility)
EnterIf content is collapsed: opens it and creates a new block inside. If content is visible: creates a new block at the start of the content area
ArrowRightAt end of summary with collapsed content: sets a GapCursor after the details block (requires Gapcursor extension)
ArrowDownIn summary with collapsed content: sets a GapCursor after the details block (requires Gapcursor extension)
KeyAction
EnterDouble-Enter escape: when pressing Enter on the last empty block inside content, the empty block is removed and a new block is created after the details node

Details does not define any input rules. Use the toolbar button or commands to create details blocks.

Details registers a button in the toolbar with the name details in group insert at priority 100.

ItemTypeCommandIconActive when
Toggle DetailsbuttontoggleDetailscaretCircleRightCursor inside a details node

The button acts as a toggle: clicking it wraps the selected content when outside details, or unwraps when inside details.

The Details extension uses a custom NodeView for interactive rendering. The NodeView creates the following DOM structure:

<div data-type="details">
<button type="button" aria-label="Toggle details">
<!-- CSS chevron via ::before pseudo-element -->
</button>
<div><!-- contentDOM -->
<summary>Summary text here</summary>
<div data-details-content hidden="hidden">
<p>Content blocks here...</p>
</div>
</div>
</div>

The toggle button dispatches a toggleDetailsContent custom event to the content area, which toggles the [hidden] attribute. The .is-open class on the outer div controls the chevron rotation via CSS.

A ProseMirror appendTransaction plugin prevents the cursor from landing inside hidden (collapsed) content. When a selection change places the cursor in a hidden detailsContent, the plugin redirects it to the nearest visible detailsSummary:

  • Forward navigation (e.g., ArrowDown into collapsed content): cursor moves to the start of the summary
  • Backward navigation (e.g., ArrowUp into collapsed content): cursor moves to the end of the summary

The plugin skips correction when:

  • The editor is composing (IME input)
  • The transaction has the detailsEnterOpen meta flag (set by the Enter handler when opening collapsed content)

Details blocks cannot be nested. The setDetails and toggleDetails commands check the ancestor chain and return false if the cursor is already inside a details node. Clicking the toolbar button while inside a details block will unwrap it rather than create a nested one.

Details styles are provided by @domternal/theme in _details.scss:

PropertyDefaultDescription
--dm-details-border1px solid var(--dm-border-color)Border around the details block
--dm-details-bgvar(--dm-surface)Background color of the summary row
--dm-details-summary-font-weight600Font weight of the summary text
Class / SelectorDescription
div[data-type="details"]Outer NodeView wrapper (CSS grid layout)
div[data-type="details"] > buttonToggle button with CSS chevron
div[data-type="details"] summaryEditable summary header
div[data-type="details"] div[data-details-content]Collapsible content area
.is-openApplied to outer div when expanded (rotates chevron, adjusts border-radius)

The details block uses CSS grid with two columns (content + button) and two rows (summary + content). The summary row background is rendered via a ::before pseudo-element that spans both columns, so the toggle button area also receives the surface color.

All components support light and dark themes automatically via CSS custom properties (--dm-surface, --dm-border-color, --dm-text, --dm-hover, --dm-muted).

import { Details } from '@domternal/extension-details';
import { DetailsSummary } from '@domternal/extension-details';
import { DetailsContent } from '@domternal/extension-details';
import type {
DetailsOptions,
DetailsSummaryOptions,
DetailsContentOptions,
} from '@domternal/extension-details';
ExportTypeDescription
DetailsNode extensionThe main details/accordion extension (auto-registers sub-nodes)
DetailsSummaryNode extensionThe summary header node (registered automatically by Details)
DetailsContentNode extensionThe collapsible content node (registered automatically by Details)
DetailsOptionsTypeScript typeOptions for Details.configure()
DetailsSummaryOptionsTypeScript typeOptions for DetailsSummary
DetailsContentOptionsTypeScript typeOptions for DetailsContent

A details block:

{
"type": "details",
"content": [
{
"type": "detailsSummary",
"content": [
{ "type": "text", "text": "Click to expand" }
]
},
{
"type": "detailsContent",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Hidden content here." }
]
}
]
}
]
}

A details block with formatted summary and multiple content blocks:

{
"type": "details",
"content": [
{
"type": "detailsSummary",
"content": [
{
"type": "text",
"marks": [{ "type": "bold" }],
"text": "Important information"
}
]
},
{
"type": "detailsContent",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "First paragraph of hidden content." }
]
},
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "List item inside details" }
]
}
]
}
]
}
]
}
]
}

A details block with persisted open state:

{
"type": "details",
"attrs": {
"open": true
},
"content": [
{
"type": "detailsSummary",
"content": [
{ "type": "text", "text": "Always open on load" }
]
},
{
"type": "detailsContent",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "This content is visible by default." }
]
}
]
}
]
}

The extension renders semantic HTML:

<details>
<summary>Click to expand</summary>
<div data-details-content>
<p>Hidden content here.</p>
</div>
</details>

When persist: true and the block is open:

<details open>
<summary>Click to expand</summary>
<div data-details-content>
<p>Visible content.</p>
</div>
</details>

@domternal/extension-details - Details.ts