Open KnowledgeOpen Knowledge
Guides

Component blocks

Insert, edit, and troubleshoot the five component block primitives (Callout, Image, Video, Audio, Accordion) inside the Open Knowledge editor.

Open Knowledge ships a WYSIWYG editor that renders MDX components as live React components. You write your markdown or MDX on disk (or via the MCP write tools); the editor parses it, renders each component with its real implementation, and lets you edit props and children without leaving the visual canvas.

The 5-pack foundation

The editor ships five built-in component primitives, each with a researched prop surface and a DIY React renderer using Open Knowledge's own brand (shadcn/Tailwind). Every descriptor pairs an MDX form with a markdown form where a standard parse path exists, and round-trips byte-identically on pristine save.

ComponentCategoryMarkdown formMDX form
CalloutContent> [!NOTE]\nText (GFM alerts; +/- suffix for foldable)<Callout type="note" title="…" icon="lucide:…" color="#…" collapsible defaultOpen>…</Callout>
imgMedia![alt](src) (standard CommonMark)<img src="…" alt="…" width={…} height={…} loading="lazy" srcset="…" sizes="…" />
videoMedia(no markdown form — HTML5 <video> passthrough)<video src="…" controls autoplay muted loop playsinline poster="…" preload="…" width={…} height={…} />
audioMedia(no markdown form — HTML5 <audio> passthrough)<audio src="…" controls autoplay loop muted preload="…" />
AccordionContent<details open><summary>X</summary>Y</details><Accordion title="X" defaultOpen icon="lucide:…" description="…" id="…" name="…">Y</Accordion>

Prop counts in this section include children for components that accept it (Callout and Accordion). The Props (N) count matches the number of typed entries in the component's prop panel + the children slot, so you can read it as "everything that lands in the editor surface." img, video, and audio have no children slot (they're self-closing leaves), so their counts reflect only typed props. The PropPanel partitions HTML-native attrs into a collapsed Advanced section by default — common props (e.g. src, alt, width, height) stay flat; srcset / sizes / decoding / fetchpriority / crossorigin / referrerpolicy live under Advanced.

Callout

GFM-style alert / admonition with five type variants and optional foldable chrome.

Common props (2): type (note | tip | important | warning | caution, default note), title (string).

Advanced props (4, collapsed in PropPanel by default): icon (namespaced lucide like lucide:Lightbulb), color (hex accent override), collapsible (boolean), defaultOpen (boolean, default true when collapsible).

Plus children (the body content).

Static Callout
<Callout type="warning">
Always run tests.
</Callout>
GFM alerts — same rendered shape
> [!WARNING]
> Always run tests.
Obsidian foldable — collapsed by default
> [!NOTE]-
> Click to reveal.
Obsidian foldable — open by default
> [!TIP]+
> Visible on first render.

The foldable marker - maps to collapsible: true, defaultOpen: false; + maps to collapsible: true, defaultOpen: true. No suffix renders a static Callout. The +/- marker is supported only inside the five GFM types; broader Obsidian type syntax ([!success], [!idea], etc.) folds to the closest GFM type via the parser alias map but does not trigger foldable rendering unless the type is one of the GFM five.

Parser alias map. Common Obsidian / Mintlify types fold on disk to their GFM-family closest (successtip, dangercaution, ideatip, infonote, etc.). The fold is lossy-on-edit: the authored form survives byte-identical on pristine save (the editor keeps your original bytes as long as you haven't touched the block), but an edit through the WYSIWYG / prop panel canonicalizes to the folded GFM type. A broader alias-preserving type set is on the roadmap and lands additively when it does — your existing content keeps working.

Unknown type values fall back to note in both authoring forms. Whether authored as <Callout type="custom"> MDX JSX or > [!custom] GFM-alert, unrecognized type tokens render with note styling. The authored token survives byte-identical on disk, and the alias-coerced path (e.g. successtip) additionally remembers the original token — so when a future release extends the type set, content authored today under the narrow surface automatically upgrades to richer rendering without an edit pass.

Multi-word blockquote openers like > [!DOWN FOR MAINT] (tokens with spaces) do NOT match the GFM-alert shape and stay as plain blockquotes — an intentional carve-out for legitimate non-callout uses of bracketed notation.

Image

Lowercase HTML <img> canonical with click-to-zoom always on.

Common props (2): src (required), alt.

Advanced props (10, collapsed in PropPanel by default): width (number), height (number), srcset, sizes, loading ('eager' | 'lazy', default 'lazy'), title (native HTML title tooltip), decoding ('sync' | 'async' | 'auto', default 'auto'), fetchpriority ('high' | 'low' | 'auto', default 'auto'), crossorigin ('anonymous' | 'use-credentials'), referrerpolicy. Pixel width / height are layout-shift-prevention specialists — most authors lay out images via CSS or container width. Attribute names are HTML-spec lowercase so emitted MDX matches the HTML spec exactly; React translates to camelCase at the JSX boundary.

img with width + lazy load
<img
  src="/assets/diagram.png"
  alt="Architecture"
  width={640}
  loading="lazy"
/>
Responsive img with srcset + sizes
<img
  src="/assets/diagram.png"
  alt="Architecture"
  srcset="/assets/diagram@1x.png 1x, /assets/diagram@2x.png 2x"
  sizes="(max-width: 600px) 400px, 800px"
/>
Standard CommonMark still round-trips
![Architecture](/assets/diagram.png)

Click-to-zoom uses react-medium-image-zoom with a native <dialog> modal; the library honors prefers-reduced-motion internally. Zoom is always on at the descriptor level — a future Frame v2 wrapper (<Frame zoom={false}><img/></Frame>) is the planned opt-out path.

Authoring forms. Both CommonMark ![alt](src) and the lowercase <img> MDX form render through descriptors — ![alt](src) parses through the read-only CommonMarkImage compat descriptor and round- trips byte-identical, while <img> is the canonical lowercase form with the full HTML-attribute surface. Click-to-zoom is on by default in both cases. To convert an existing CommonMark image and unlock the full attribute surface, click the image, open the PropPanel, and press Convert to Image — the PM node flips to the canonical img descriptor on save. Both forms round-trip byte-identical until the moment you edit. Obsidian ![[file.ext]] wiki-embed is a separate parse path with its own renderer (out of scope for this guide).

Video

Pure HTML5 <video> wrapper with native controls. Self-closing leaf (symmetric with <img>). Matches Mintlify's explicit-iframe pattern for embedded services: no YouTube / Vimeo URL auto-promotion, no iframe emission. Authors embedding service videos write a raw <iframe> in MDX directly.

Common props (1): src (required).

Advanced props (10, collapsed in PropPanel by default): controls (default true), autoplay, poster, width (number), height (number), title (tooltip), muted, loop, playsinline, preload ('none' | 'metadata' | 'auto'). The fresh-insert form is a single src field; toggle controls / autoplay / etc. from Advanced if needed. Attribute names are HTML-spec lowercase (autoplay, playsinline); React maps to camelCase (autoPlay, playsInline) at the JSX boundary.

video with poster + autoplay
<video src="/clip.mp4" poster="/p.jpg" autoplay muted loop />

Captions and codec fallback. For <track> caption sources or <source> codec fallbacks, write a raw HTML5 <video> element with children in MDX directly:

Raw HTML5 video with captions (flows through rawMdxFallback)
<video src="/clip.mp4" controls>
  <track kind="captions" src="/clip.en.vtt" srclang="en" label="English" default />
  <source src="/clip.webm" type="video/webm" />
</video>

This takes the rawMdxFallback path — bytes preserved on disk, editable as MDX source, renders natively in the browser. A typed tracks + sources array prop on the video descriptor is tracked as future work when ProseMirror's descriptor system grows an array-of-structured- records prop type.

Audio

HTML5 <audio> wrapper. Self-closing leaf (symmetric with <video>).

Common props (1): src (required).

Advanced props (6, collapsed in PropPanel by default): controls (default true), autoplay, title, muted, loop, preload ('none' | 'metadata' | 'auto'). Set controls={false} for chrome-less playback (rare, mostly for hero loops). Attribute names are HTML-spec lowercase; React maps autoplay to autoPlay at the JSX boundary.

audio with preload hint
<audio src="/clip.mp3" autoplay loop preload="metadata" />

For <source> codec fallback elements, write a raw <audio> element in MDX (same escape hatch as <video> above) — descriptor-dispatched <audio> has no children slot.

Accordion

Standalone expand/collapse built on native HTML5 <details> / <summary>. No wrapper parent required; cross-browser exclusive grouping via HTML5 <details name="…"> (Chrome 120+, Safari 17.2+, Firefox 130+).

Common props (2): title (required), defaultOpen (boolean).

Advanced props (4, collapsed in PropPanel by default): icon (namespaced lucide), description (subtitle), id (for deep-linking), name (exclusive-group identifier).

Plus children (the body content).

Standalone accordion
<Accordion title="Advanced options" defaultOpen id="advanced">
- `--fetch-retry-maxtimeout=60000`
- `--loglevel=warn`
</Accordion>
HTML5 details renders the same descriptor
<details open>
<summary>Advanced options</summary>

- `--fetch-retry-maxtimeout=60000`
- `--loglevel=warn`

</details>
Exclusive-accordion group via HTML5 name
<Accordion title="One" name="group-a">First answer</Accordion>
<Accordion title="Two" name="group-a">Second answer</Accordion>
<Accordion title="Three" name="group-a">Third answer</Accordion>

Opening any accordion in the group closes the others via the browser's native behavior — no JavaScript state.

The slash menu

Type / at the start of an empty line to open the insertion menu. Start typing to filter; pick an entry with the arrow keys and press Enter to insert.

Every entry inserts a fully-formed MDX block with sensible defaults. For components with editable props, the settings panel opens automatically so you can fill in required fields before you move on.

When a media block (img, video, audio) is inserted without a source, it renders as an "Add an image" / "Add a video" / "Add audio" pill instead of a broken placeholder. Click the pill to reopen the settings panel — typing a URL replaces it with the rendered media.

Editing props

When a component is selected, a hover chrome bar appears at the top-right with three groups of actions (rendered as up to four icon buttons depending on the block's position within its parent):

  1. Move up / Move down — reorder the component within its parent (one button per direction; only shown when the move is meaningful).
  2. Delete — remove the component and its children.
  3. Settings (gear) — open the floating settings panel.

The settings panel is auto-generated from the component's prop descriptor:

  • String props render as text inputs.
  • Boolean props render as toggle switches.
  • Enum props render as <select> dropdowns with the valid values.
  • Number props render as numeric inputs.
  • React-node props (children) are edited in the canvas, not the panel.

Your edits write to the underlying MDX immediately — storage is the CRDT, and every keystroke propagates to other connected clients.

Keyboard shortcuts

ActionShortcut
Open settings panel (when a component is selected)Enter or Space
Close settings panel and return focus to the componentEsc
Move selection between blocks /
Move a top-level block up / downCmd+Shift+ / (macOS) · Ctrl+Shift+ / (Linux / Windows)

Accessibility

Every built-in component renders with accessibility primitives baked into the selection layer rather than retrofitted per-block:

  • Selection announcer. Selecting a block emits an aria-live="polite" announcement with the block's descriptor name so screen readers narrate the focused element.
  • Breadcrumb ancestry. The selection footer renders an aria-label-tagged list of ancestor components so keyboard users can jump to any enclosing container.
  • Halo palette. The block selection halo degrades to a solid outline under Windows High Contrast Mode (forced-colors: active) — no color information is lost.
  • Motion. Halo transitions honor prefers-reduced-motion and collapse to instant state changes when the OS setting is on. Accordion's disclosure-triangle rotation honors the same preference.
  • Keyboard parity. Every hover-chrome action (move up / move down / delete / settings) is reachable via TabEnter from the selected block.

The block-selection a11y contract is enforced in the selection-state plugin rather than per-renderer, so new descriptors inherit it without opt-in code.

Authoring-form preservation

Components round-trip byte-identical on pristine save — content you haven't edited since parse serializes back to whatever form you wrote on disk. The editor's hybrid serialization preserves the authoring shape:

  • > [!NOTE] stays > [!NOTE].
  • > [!WARNING]- stays > [!WARNING]- with the +/- marker intact.
  • <details><summary>X</summary>Y</details> stays in that form.
  • <Callout>…</Callout> stays in that form.
  • ![alt](src) stays as a standard CommonMark image.

When you edit a component in WYSIWYG (title change, prop toggle, …), the save canonicalizes to the MDX JSX form with the new props. The original form is only reconstructed if all props match and no children were touched.

Unknown attributes round-trip losslessly. Attrs not declared by the descriptor (e.g. <Accordion variant="accent">, <Callout severity="critical">) are preserved on disk, ignored by the renderer, and hidden from the prop panel. Narrowing a descriptor never corrupts content authored against a broader descriptor elsewhere.

Unregistered and broken components

Open Knowledge's rule: all user content stays visible and editable. If a component cannot be rendered live, it falls through to an editable source-code block instead of disappearing.

Two situations trigger the fallback:

  1. Unregistered component name. The editor ships live React renderers only for the 5-pack. Unknown names (your own project-specific components, imported libraries we don't bundle, or cut primitives like <Tabs> / <Card> / <Steps> that lived in earlier iterations) render as an editable source block — a nested CodeMirror editor containing the raw source. Fix the source; on blur the editor re-parses and, if the source is valid MDX again, replaces the fallback with a live component.

  2. Render errors. A valid component that throws during React render (bad prop, missing required value, runtime crash) is caught by an error boundary and auto-converts to the same editable source block. The error message is shown in the chrome bar so you can diagnose what needs fixing.

If the auto-convert itself can't land after a few retries (rare, but possible under unusual conditions), the block surfaces a stuck-state banner with Copy source and Delete buttons. Use Copy source to move the MDX into a fresh location, then Delete the failed block. Either way: your MDX bytes are preserved exactly as you authored them. The raw source never disappears.

<Mermaid /> and other deprecated placeholders

Earlier versions shipped non-functional placeholders for <Mermaid /> and <AudioPlaceholder />. Those stubs were removed; existing content auto-converts to the editable source fallback on first open. Rename <AudioPlaceholder /> to <audio /> to pick up the built-in audio-player descriptor.

Authoring via MCP / agents

Agents writing to the document through MCP's write_document / edit_document tools produce markdown and MDX exactly as above. AI agents tend to emit GFM alerts (> [!NOTE]) and standard MDX JSX natively from their training distribution; both forms parse to the same descriptors and round-trip faithfully.

If an agent writes an unregistered component name, the editor behaves exactly as for a human-authored unknown tag: the component appears as an editable source fallback, the MDX source is preserved, and on blur the fallback upgrades to a live component if the source now parses.

Future work

The 5-pack is the current foundation. A compound tier (Tabs / Tab, an <Accordions> grouping wrapper for coordinated animation + shared chrome, Steps / Step) is not built in today. Compound parent-child primitives get re-added when dev-docs or help-center authoring demand surfaces; no public API will change for existing 5-pack consumers.

Other names that don't have live renderers today: Card / CardGroup, Banner, Files-tree, TypeTable, InlineTOC, Math / KaTeX, Mermaid. All of these round-trip through your files byte-identical and render as editable source blocks until (and unless) a live renderer lands. User-registered custom components and live-rendered inline-component editing remain out of scope.

See also