Skip to content

Styling

<Markdown>{source}</Markdown>
.rmk-document h1 { font-size: 2rem; }

React Markdown Kit doesn't ship a styling dependency. There's no design system, Tailwind, CSS-in-JS runtime or theme provider to install. The only peer dependencies are react and react-dom, and a packaging test fails the build if that ever changes.

If you don't import any CSS, the renderer outputs plain semantic HTML. It won't have any colours, fonts, radii or shadows until you add them.

Four approaches#

All of these work without patching the library.

ApproachWhat it needs
Bring your own CSSNothing at all
The shipped theme, retintedOne import plus a few custom properties
Utility classesA classNames prop
Your own componentsA components prop

1. Bring your own CSS#

The default DOM is the same semantic HTML react-markdown outputs, without any kit-specific classes. You can style it with element selectors under a class of your own.

Unstyled output
Markdown
## Release 1.4

Ships **today**. See the [changelog](https://example.com/changelog).

- Faster cold render
- Smaller bundle
Rendered

Release 1.4

Ships today. See the changelog.

  • Faster cold render
  • Smaller bundle
.article h2 { font-size: 1.5rem; letter-spacing: -0.01em; }
.article a { color: rebeccapurple; }

2. The shipped theme, retinted#

Import the optional stylesheet and put .rmk-document on a wrapper.

import '@react-markdown-kit/renderer/styles.css'


<div className="rmk-document">
  <Markdown>{source}</Markdown>
</div>

Every colour, spacing value, radius and font in that file reads a --rmk-* custom property. If you override a property on any ancestor, everything below it picks up the change.

.rmk-document {
  --rmk-text: #111;
  --rmk-link: rebeccapurple;
  --rmk-font-body: 'Inter', system-ui, sans-serif;
}
Same component, three custom properties
Markdown
## Release 1.4

Ships **today**. See the [changelog](https://example.com/changelog).

> Cold render is down to 9.1 ms.

| Metric | Before | After |
| --- | ---: | ---: |
| Bundle | 41 kB | 38 kB |
Rendered

Release 1.4

Ships today. See the changelog.

Cold render is down to 9.1 ms.

MetricBeforeAfter
Bundle41 kB38 kB

The renderer's properties:

PropertyDefault
--rmk-font-bodythe system UI stack
--rmk-font-monothe system monospace stack
--rmk-textcurrentColor
--rmk-mutedcurrentColor at 65%
--rmk-bordercurrentColor at 15%
--rmk-surfacecurrentColor at 5%
--rmk-linkcurrentColor
--rmk-measure68ch
--rmk-space1rem
--rmk-radius4px

The editor stylesheet reads the same names plus --rmk-focus, --rmk-surface-active and --rmk-content-min-height. The defaults are built from currentColor and color-mix, so the theme follows your dark mode and you don't need a second theme block.

3. Utility classes#

You can pass your own classes for each part. Tailwind, CSS Modules, BEM and Infima all work the same way.

<Markdown
  classNames={{
    root: 'prose prose-slate max-w-none',
    code: 'rounded bg-slate-100 px-1 py-0.5 font-mono text-sm',
  }}
>
  {source}
</Markdown>

A class you pass replaces the kit's default for that part (the two don't get merged), so you won't need !important or have to fight specificity. Passing an empty string removes the class entirely.

classNames, using this site's own utility classes
Markdown
## Release 1.4

Ships **today**. See the [changelog](https://example.com/changelog).
Rendered

Release 1.4

Ships today. See the changelog.

Those three are Tailwind utilities from this site's stylesheet. If you swapped them for your own classes, the rest of the call would stay the same.

Renderer parts: root, paragraph, heading, link, image, list, listItem, taskListItem, blockquote, code, codeBlock, pre, table, thead, tbody, tr, th, td, hr, footnotes, footnoteRef, variable, plus diagram (set by the mermaid plugin) and deck, slide, slideNotes (set by the slides plugin).

Editor parts: root, toolbar, toolbarGroup, toolbarButton, toolbarButtonActive, toolbarDivider, content, placeholder, sourceTextarea, preview, statusBar, variableChip, plus modeGroup, opaque, image, codeBlock, table and the inline mark parts.

<MarkdownEditor
  classNames={{
    root: 'rounded-lg border border-slate-200',
    toolbar: 'flex gap-1 border-b p-2',
    content: 'min-h-64 p-4 focus:outline-none',
  }}
  value={value}
  onChange={setValue}
/>

4. Your own components#

components maps any element to one of your own components. This always works as a fallback, and it's how you'd bring in a design system from your application if you want one.

Two element overrides
Markdown
Ships **today**. See the [changelog](https://example.com/changelog).

Call `compileMarkdown` once, then render the document.
Rendered

Ships today. See the changelog.

Call compileMarkdown once, then render the document.

<Markdown components={{ a: AppLink, img: AppImage, code: AppCode }}>
  {source}
</Markdown>

You could swap those for ZUI, shadcn/ui or MUI components and the rest of the call stays the same. The library never imports them itself.

The contract#

We hold the packages to these rules, and scripts/check-css-scope.mjs checks the CSS-related ones on every build.

  1. No styling runtime dependency in any package.
  2. Unstyled by default, with inline style used only for function and never for appearance.
  3. Clean DOM by default. Kit classes appear when you ask for them through classNames, or on parts with no semantic element of their own, such as a variable chip or a task-list item.
  4. No global selectors and no reset. Optional CSS only matches inside .rmk-document or .rmk-editor.
  5. Single-class specificity, so one class of yours wins.
  6. Every value in the optional CSS reads a --rmk-* custom property.
  7. Class names are replaceable through classNames.
  8. Components are replaceable through components.

The optional CSS is kept small on purpose. renderer/styles.css only covers typography (measure, heading scale, list indentation, code and table defaults). editor/styles.css only covers the editor chrome, meaning the content box, toolbar layout, focus ring, placeholder and the editing affordances that don't have a semantic HTML equivalent.

Editor chrome#

The default editor comes with a toolbar so <MarkdownEditor value onChange /> is usable on its own. It uses <button type="button"> with accessible names, and the icons are inline SVGs in the package, so there's no icon font or icon dependency.

<MarkdownEditor value={value} onChange={setValue} toolbar={false} />

toolbar={false} removes it, and passing a function replaces it. If you want full control, use the headless parts and build your own chrome:

const editor = useMarkdownEditor({ value, onChange })


<MarkdownEditorProvider editor={editor}>
  <MyOwnToolbar />
  <MarkdownEditorContent />
</MarkdownEditorProvider>

The editor's behaviour doesn't depend on its own CSS. Without @react-markdown-kit/editor/styles.css, the editor is unstyled but still works (typing, shortcuts, mode switching and serialization). A packaging test checks this.

See also#

  • The styling contract in the repository.
  • examples/styling-approaches, which renders one document all four ways in one page.

Last updated on