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.
| Approach | What it needs |
|---|---|
| Bring your own CSS | Nothing at all |
| The shipped theme, retinted | One import plus a few custom properties |
| Utility classes | A classNames prop |
| Your own components | A 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.
## Release 1.4 Ships **today**. See the [changelog](https://example.com/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;
}## 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 |
Release 1.4
Ships today. See the changelog.
Cold render is down to 9.1 ms.
| Metric | Before | After |
|---|---|---|
| Bundle | 41 kB | 38 kB |
The renderer's properties:
| Property | Default |
|---|---|
--rmk-font-body | the system UI stack |
--rmk-font-mono | the system monospace stack |
--rmk-text | currentColor |
--rmk-muted | currentColor at 65% |
--rmk-border | currentColor at 15% |
--rmk-surface | currentColor at 5% |
--rmk-link | currentColor |
--rmk-measure | 68ch |
--rmk-space | 1rem |
--rmk-radius | 4px |
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.
## Release 1.4 Ships **today**. See the [changelog](https://example.com/changelog).
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.
Ships **today**. See the [changelog](https://example.com/changelog). Call `compileMarkdown` once, then render the document.
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.
- No styling runtime dependency in any package.
- Unstyled by default, with inline style used only for function and never for appearance.
- 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. - No global selectors and no reset. Optional CSS only matches inside
.rmk-documentor.rmk-editor. - Single-class specificity, so one class of yours wins.
- Every value in the optional CSS reads a
--rmk-*custom property. - Class names are replaceable through
classNames. - 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