Valkyrian Labs logo

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:

  • config for Markdown wrapper and typography presentation
  • themes for named directive themes
  • code for 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 prose
  • docs: denser documentation typography
  • compact: smaller vertical rhythm for panels and previews
  • unstyled: omits the plugin typography classes and uses your className

Sizes:

  • lg
  • md
  • sm

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-card
  • vl-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's dark: variant, which you compile, so it tracks whatever your app uses: a .dark class, a data-theme attribute, or the OS preference if you haven't declared a variant. Declare one with @custom-variant dark as shown above if your site has a theme switcher.
  • light or dark pins the palette regardless of the page theme: use dark for 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:

TokenUsed for
--pmd-surfaceCard, panel, and callout surfaces
--pmd-surface-mutedmuted themes
--pmd-borderPanel borders and dividers
--pmd-accentLinks, hovered titles, focus rings
--pmd-accent-hoverHovered links and titles
--pmd-code-bgCode 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:

  • sectionClassName
  • columnClassName

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.