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).
Installation
Section titled “Installation”pnpm add @domternal/extension-detailsLive Playground
Section titled “Live Playground”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.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Plain details without the theme. The buttons above the editor call commands like setDetails(), unsetDetails(), and toggleDetails() directly.
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 buttonconst 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();});import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Details } from '@domternal/extension-details';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [Document, Paragraph, Text, Details]; content = '<details><summary>Click to expand</summary><div data-details-content><p>Hidden content here.</p></div></details>';}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>The toolbar auto-renders a details toggle button from the extension’s registered toolbar items.
import { Domternal } from '@domternal/react';import { Document, Paragraph, Text } from '@domternal/core';import { Details } from '@domternal/extension-details';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, Details]} content="<details><summary>Click to expand</summary><div data-details-content><p>Hidden content here.</p></div></details>" > <Domternal.Toolbar /> <Domternal.Content /> </Domternal> );}The <Domternal.Toolbar /> auto-renders a details toggle button from the extension’s registered toolbar items.
import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Details } from '@domternal/extension-details';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, Details],});
// Wrap current block in a details elementeditor.commands.setDetails();
// Toggle details open/closededitor.commands.toggleDetails();
// Open or close programmaticallyeditor.commands.openDetails();editor.commands.closeDetails();
// Remove details wrapper, keeping contenteditor.commands.unsetDetails();Schema
Section titled “Schema”| Property | Value |
|---|---|
| ProseMirror name | details |
| Type | Node |
| Group | block |
| Content | detailsSummary detailsContent (exactly one summary + one content) |
| Atom | No |
| Inline | No |
| Defining | Yes |
| Isolating | Yes |
| Selectable | Yes |
| Draggable | No |
| Allow gap cursor | No |
| 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.
Sub-nodes
Section titled “Sub-nodes”The Details extension automatically registers two child nodes via addExtensions(). You do not need to import or configure them separately.
DetailsSummary
Section titled “DetailsSummary”| Property | Value |
|---|---|
| ProseMirror name | detailsSummary |
| Content | inline* (text and marks only) |
| Defining | Yes |
| Isolating | Yes |
| Selectable | No |
| 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.
DetailsContent
Section titled “DetailsContent”| Property | Value |
|---|---|
| ProseMirror name | detailsContent |
| Content | block+ (one or more block nodes) |
| Defining | Yes |
| Selectable | No |
| 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.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
persist | boolean | false | Whether the open/closed state is persisted in the document data. When false, open state is UI-only and resets on reload |
openClassName | string | 'is-open' | CSS class applied to the NodeView wrapper when the details block is expanded |
HTMLAttributes | Record<string, unknown> | {} | Custom HTML attributes added to the rendered <details> element |
Persistent open state
Section titled “Persistent open state”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.
DetailsSummary options
Section titled “DetailsSummary options”| Option | Type | Default | Description |
|---|---|---|---|
HTMLAttributes | Record<string, unknown> | {} | Custom HTML attributes for the <summary> element |
DetailsContent options
Section titled “DetailsContent options”| Option | Type | Default | Description |
|---|---|---|---|
HTMLAttributes | Record<string, unknown> | {} | Custom HTML attributes for the <div data-details-content> element |
Attributes
Section titled “Attributes”| Attribute | Type | Default | Condition | Description |
|---|---|---|---|---|
open | boolean | false | Only when persist: true | Whether 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.
Commands
Section titled “Commands”setDetails
Section titled “setDetails”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 chainingeditor.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.
unsetDetails
Section titled “unsetDetails”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 chainingeditor.chain().focus().unsetDetails().run();Returns false if the cursor is not inside a details node.
toggleDetails
Section titled “toggleDetails”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 chainingeditor.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.
openDetails
Section titled “openDetails”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.
closeDetails
Section titled “closeDetails”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.
setDetailsOpen
Section titled “setDetailsOpen”Sets the open state to a specific value. Requires persist: true.
editor.commands.setDetailsOpen(true); // Openeditor.commands.setDetailsOpen(false); // CloseReturns false if persist: false, the cursor is not inside a details node, or the state is already at the target value.
Keyboard shortcuts
Section titled “Keyboard shortcuts”In summary (DetailsSummary)
Section titled “In summary (DetailsSummary)”| Key | Action |
|---|---|
Backspace | At start of summary: unwraps the details block (unsetDetails). In the middle: deletes one character (manual handling for Safari compatibility) |
Enter | If 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 |
ArrowRight | At end of summary with collapsed content: sets a GapCursor after the details block (requires Gapcursor extension) |
ArrowDown | In summary with collapsed content: sets a GapCursor after the details block (requires Gapcursor extension) |
In content (DetailsContent)
Section titled “In content (DetailsContent)”| Key | Action |
|---|---|
Enter | Double-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 |
Input rules
Section titled “Input rules”Details does not define any input rules. Use the toolbar button or commands to create details blocks.
Toolbar items
Section titled “Toolbar items”Main toolbar
Section titled “Main toolbar”Details registers a button in the toolbar with the name details in group insert at priority 100.
| Item | Type | Command | Icon | Active when |
|---|---|---|---|---|
| Toggle Details | button | toggleDetails | caretCircleRight | Cursor inside a details node |
The button acts as a toggle: clicking it wraps the selected content when outside details, or unwraps when inside details.
NodeView
Section titled “NodeView”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.
Selection plugin
Section titled “Selection plugin”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
detailsEnterOpenmeta flag (set by the Enter handler when opening collapsed content)
Nesting
Section titled “Nesting”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.
Styling
Section titled “Styling”Details styles are provided by @domternal/theme in _details.scss:
CSS custom properties
Section titled “CSS custom properties”| Property | Default | Description |
|---|---|---|
--dm-details-border | 1px solid var(--dm-border-color) | Border around the details block |
--dm-details-bg | var(--dm-surface) | Background color of the summary row |
--dm-details-summary-font-weight | 600 | Font weight of the summary text |
CSS classes
Section titled “CSS classes”| Class / Selector | Description |
|---|---|
div[data-type="details"] | Outer NodeView wrapper (CSS grid layout) |
div[data-type="details"] > button | Toggle button with CSS chevron |
div[data-type="details"] summary | Editable summary header |
div[data-type="details"] div[data-details-content] | Collapsible content area |
.is-open | Applied to outer div when expanded (rotates chevron, adjusts border-radius) |
Layout
Section titled “Layout”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).
Exports
Section titled “Exports”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';| Export | Type | Description |
|---|---|---|
Details | Node extension | The main details/accordion extension (auto-registers sub-nodes) |
DetailsSummary | Node extension | The summary header node (registered automatically by Details) |
DetailsContent | Node extension | The collapsible content node (registered automatically by Details) |
DetailsOptions | TypeScript type | Options for Details.configure() |
DetailsSummaryOptions | TypeScript type | Options for DetailsSummary |
DetailsContentOptions | TypeScript type | Options for DetailsContent |
JSON representation
Section titled “JSON representation”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." } ] } ] } ]}HTML output
Section titled “HTML output”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>Source
Section titled “Source”@domternal/extension-details - Details.ts