Styling
Style Markdown output with renderer config, Tailwind Typography, directive themes, and stable hooks.
Styling
The renderer ships with practical defaults and supports customization through three separate surfaces:
configfor Markdown wrapper and typography presentationthemesfor named directive themescodefor code fence rendering behavior
Tailwind Typography
For best defaults, enable Tailwind Typography and scan the package output:
1pnpm add @tailwindcss/typography
1@import "tailwindcss";2@plugin "@tailwindcss/typography";3 4@source "../node_modules/payload-markdown/dist";
Installed the scoped package? Point @source at ../node_modules/@valkyrianlabs/payload-markdown/dist instead.
If your site switches between light and dark with a class or attribute, tell Tailwind, so the Markdown output switches with it (see Light And Dark):
1@custom-variant dark (&:where(.dark, .dark *));2/* or, for an attribute: (&:where([data-theme=dark], [data-theme=dark] *)) */
If you define custom directive themes with Tailwind classes, keep those class strings in source/config so Tailwind can scan them.
Renderer Config
1payloadMarkdown({2 config: {3 className: '[&_li::marker]:text-cyan-200/90',4 enableGutter: true,5 mutedHeadings: true,6 size: 'lg',7 variant: 'blog',8 wrapperClassName: 'max-w-4xl',9 },10})
config controls the Markdown wrapper and typography preset. It is not the directive theme registry and it is not code block configuration.
Variants And Sizes
Variants:
blog: larger article-style headings and prosedocs: denser documentation typographycompact: smaller vertical rhythm for panels and previewsunstyled: omits the plugin typography classes and uses yourclassName
Sizes:
lgmdsm
Directive Themes
Use themes for directive styling:
1payloadMarkdown({2 themes: {3 card: {4 items: [5 {6 name: 'forge',7 classes: 'rounded-2xl border border-cyan-400/40 bg-cyan-950/30 p-5',8 },9 ],10 },11 },12})
Then authors select the theme in Markdown:
1:::card[Custom Card]{theme="forge"}2Portable Markdown content.3:::
The renderer emits stable hooks such as:
data-directive="card"data-theme="forge"vl-md-cardvl-md-card--theme-forge
Light And Dark
Directive defaults (cards, callouts, tabs, steps, details, the table of contents, buttons) come in a light and a dark palette, and the typography follows the same switch. colorScheme picks how:
auto(default) follows Tailwind'sdark:variant, which you compile, so it tracks whatever your app uses: a.darkclass, adata-themeattribute, or the OS preference if you haven't declared a variant. Declare one with@custom-variant darkas shown above if your site has a theme switcher.lightordarkpins the palette regardless of the page theme: usedarkfor a site that is always dark.
1payloadMarkdown({2 config: {3 colorScheme: 'dark',4 },5})
It can also be set per collection, per block scope, or on MarkdownRenderer directly. Code blocks keep their Shiki theme's surface in both schemes.
Design tokens
Every directive colour reads a public CSS custom property first. The plugin never sets these, so a value you set on any ancestor (:root, a layout wrapper, config.className) always wins:
| Token | Used for |
|---|---|
--pmd-surface | Card, panel, and callout surfaces |
--pmd-surface-muted | muted themes |
--pmd-border | Panel borders and dividers |
--pmd-accent | Links, hovered titles, focus rings |
--pmd-accent-hover | Hovered links and titles |
--pmd-code-bg | Code block background, including code inside directives |
When a token is unset, surfaces and borders use your Tailwind theme's --color-card and --color-border if it defines them (as shadcn-style themes do), so directives match your app's cards. Otherwise they use the plugin palette for the active scheme.
Tokens apply to both schemes, so set them per scheme:
1:root {2 --pmd-surface: #fdf2f8;3 --pmd-accent: #be185d;4}5 6.dark {7 --pmd-surface: #2a0f1d;8 --pmd-accent: #f9a8d4;9}
renderMarkdown output without MarkdownRenderer uses the dark palette unless a container sets the scheme. Apply PMD_COLOR_SCHEME_CLASS_NAMES.auto (or .light, .dark) from payload-markdown/render to that container.
Custom directive themes replace the default classes for their name, so they keep full control: add dark: variants to a custom theme's classes when it should differ between schemes.
Directive Content
Markdown inside a card, callout, details block, tab panel, or step renders with the same Tailwind Typography styles as the rest of the document, so lists, links, inline code, tables, and quotes look the same everywhere. Typography overrides in config.className (for example prose-a:text-pink-400) apply inside directives too. Directive chrome such as card titles, eyebrows, tab triggers, and the table of contents is marked not-prose, and directive bodies carry data-directive-body.
Code blocks read their background from the --pmd-code-bg custom property (default #18191c). Code inside a directive body uses a slightly darker inset well unless you set the token. Set it on any element to change the code background everywhere inside it, nested code included:
1payloadMarkdown({2 config: {3 className: '[--pmd-code-bg:#0d1117]',4 },5})
Deprecated Class Aliases
These still work but should not be the preferred customization path:
sectionClassNamecolumnClassName
Prefer directive themes:
1payloadMarkdown({2 themes: {3 columns: {4 items: [5 {6 name: 'panel',7 classes: 'grid grid-cols-1 gap-8 rounded-2xl border border-border/60 p-4',8 },9 ],10 },11 section: {12 items: [13 {14 name: 'panel',15 classes: 'my-12 rounded-2xl border border-border bg-card/70 p-6 shadow-sm',16 },17 ],18 },19 },20})
Legacy aliases remain available for older projects:
1payloadMarkdown({2 config: {3 columnClassName: 'gap-8 md:gap-12',4 sectionClassName: 'rounded-2xl border border-white/10 bg-white/5 p-6',5 },6})
Override Order
1plugin -> collection -> field/block scope -> direct renderer props
Later layers win for scalar config values. Class name values are joined.
className and wrapperClassName are merged over the renderer defaults: a class that sets the same utility as a default replaces it. For example, prose-a:font-bold replaces the default prose-a:font-medium, and prose-a:text-pink-600 replaces the default link colour. Without this, two classes of equal specificity resolve by their order in the stylesheet, not in the class list, so the override could silently lose.