ra-markdown: Markdown Utilities For React-Admin

react-admin ≥ 5.0.2

For applications that need to display and edit Markdown content, this package offers two components:

  • <MarkdownField>: Display formatted markdown, using the Material UI theme
  • <MarkdownInput>: Edit a markdown field, with a WYSIWYG editor and a markdown source editor with live preview

The editor is based on Milkdown, and its colors and fonts follow the Material UI theme, including the dark mode.

Test it live on the Enterprise Edition Storybook, and in the e-commerce demo.

Installation

npm install --save @react-admin/ra-markdown
# or
yarn add @react-admin/ra-markdown

Tip: ra-markdown is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

<MarkdownField>

A Field component for Markdown content. To be used e.g. in Show views.

<MarkdownField> preview

Usage

import { Show, SimpleShowLayout, TextField } from 'react-admin';
import { MarkdownField } from '@react-admin/ra-markdown';

const PostShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="title" />
            <MarkdownField source="description" />
        </SimpleShowLayout>
    </Show>
);
import { Show, SimpleShowLayout, TextField } from "react-admin";
import { MarkdownField } from "@react-admin/ra-markdown";

const PostShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="title" />
            <MarkdownField source="description" />
        </SimpleShowLayout>
    </Show>
);

The content is rendered with the same styles as in <MarkdownInput>, so that it looks the same in read and edit modes.

Tip: If you want to display raw (unformatted) markdown, use <TextField component="pre"> instead:

<TextField source="description" component="pre" />
<TextField source="description" component="pre" />;

Props

Prop Required Type Default Description
source Required string The field name in the record. Supports paths, e.g. description.body
label Optional string The label. If not set, the label is inferred from the source
className Optional string The CSS class of the root element
configureMilkdown Optional (crepe: Crepe) => void Called before the editor is created. See <MarkdownInput configureMilkdown>
onReady Optional (crepe: Crepe) => void Called once the editor is created
sx Optional SxProps Custom styles

<MarkdownField> also accepts the common field props.

<MarkdownInput>

An Input component for Markdown content. To be used in Edit and Create views.

It offers two edition modes, which users switch with the tabs at the bottom of the input:

  • WYSIWYG: a rich text editor, with a toolbar on top. Users can also format the selected text with a floating toolbar, and insert blocks with the / command.
  • Markdown: the markdown source, with syntax highlighting, and a live preview. The preview is also editable.

Usage

import { Edit, SimpleForm, TextInput } from 'react-admin';
import { MarkdownInput } from '@react-admin/ra-markdown';

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="title" />
            <MarkdownInput source="description" />
        </SimpleForm>
    </Edit>
);
import { Edit, SimpleForm, TextInput } from "react-admin";
import { MarkdownInput } from "@react-admin/ra-markdown";

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="title" />
            <MarkdownInput source="description" />
        </SimpleForm>
    </Edit>
);

The editor props can be grouped in an object, e.g. to share the same configuration between several inputs:

import { Edit, SimpleForm, TextInput } from 'react-admin';
import { MarkdownInput, MarkdownInputProps } from '@react-admin/ra-markdown';

const options: Partial<MarkdownInputProps> = {
    previewStyle: 'tab',
    height: '300px',
    initialEditType: 'markdown',
    useCommandShortcut: false,
};

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="title" />
            <MarkdownInput source="description" {...options} />
        </SimpleForm>
    </Edit>
);
import { Edit, SimpleForm, TextInput } from "react-admin";
import { MarkdownInput } from "@react-admin/ra-markdown";

const options = {
    previewStyle: "tab",
    height: "300px",
    initialEditType: "markdown",
    useCommandShortcut: false,
};

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="title" />
            <MarkdownInput source="description" {...options} />
        </SimpleForm>
    </Edit>
);

Props

Prop Required Type Default Description
source Required string The field name in the record
configureMilkdown Optional (crepe: Crepe) => void Called before the editor is created, e.g. to register plugins
onReady Optional (crepe: Crepe) => void Called once the editor is created
fullWidth Optional boolean true If true, the input expands to fill the form width
height Optional string | number 512px The editor height
helperText Optional string The helper text to display under the input
initialEditType Optional 'markdown' | 'wysiwyg' wysiwyg The edition mode displayed first
label Optional string | false The label. If not set, the label is inferred from the source
previewStyle Optional 'vertical' | 'tab' vertical How the markdown mode displays the source and the preview
ref Optional Ref<MarkdownEditorRef> Gives access to the editor instance
toolbarItems Optional array The buttons of the toolbar
useCommandShortcut Optional boolean true Whether the formatting keyboard shortcuts are enabled

<MarkdownInput> also accepts the common input props.

configureMilkdown

The WYSIWYG editor is based on Milkdown. Its plugins must be registered before the editor is created, so the ref gives access to the editor too late. Use the configureMilkdown prop instead: it receives the Crepe instance before its creation, and lets you register plugins with crepe.editor.use(), or change the editor context with crepe.editor.config().

For instance, to add a remark plugin, and a ProseMirror plugin highlighting some text with decorations:

import { MarkdownInput } from '@react-admin/ra-markdown';
import { Plugin } from '@milkdown/kit/prose/state';
import { Decoration, DecorationSet } from '@milkdown/kit/prose/view';
import { $prose, $remark } from '@milkdown/kit/utils';
import remarkDirective from 'remark-directive';

const directive = $remark('remarkDirective', () => remarkDirective);

const highlight = $prose(
    () =>
        new Plugin({
            props: {
                decorations: state =>
                    DecorationSet.create(state.doc, [
                        Decoration.inline(1, 6, { class: 'highlight' }),
                    ]),
            },
        })
);

const configureMilkdown = crepe => {
    crepe.editor.use(directive).use(highlight);
};

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <MarkdownInput
                source="description"
                configureMilkdown={configureMilkdown}
            />
        </SimpleForm>
    </Edit>
);

<MarkdownField> accepts the same prop.

Tip: configureMilkdown is only read when the editor is created: changing it afterwards has no effect. It only applies to the WYSIWYG editor, not to the source of the markdown mode.

fullWidth

The input fills the form width by default. Set the fullWidth prop to false to disable it:

<MarkdownInput source="description" fullWidth={false} />

height

The editor is 512px high by default. You can change it with the height prop, using any CSS unit, or a number of pixels:

<MarkdownInput source="description" height="300px" />

When the content is longer, the editor scrolls.

helperText

If you need to display a text below the editor (usually to explain the expected data to the user), use the helperText prop.

<MarkdownInput
    source="description"
    helperText="Enter a description of the post"
/>

initialEditType

The edition mode displayed first: wysiwyg (the default) or markdown. Users can switch modes with the tabs at the bottom of the input.

<MarkdownInput source="description" initialEditType="markdown" />

label

You can customize the label by setting the label prop. It is inferred from the source prop by default. Set it to false to hide the label.

<MarkdownInput source="description" label="Explanation" />

onReady

Called once the WYSIWYG editor is created, with the Crepe instance. It replaces the onLoad option of the Toast UI editor used by ra-markdown v5.

<MarkdownInput
    source="description"
    onReady={crepe => console.log(crepe.getMarkdown())}
/>

<MarkdownField> accepts the same prop.

Tip: To register Milkdown plugins, use configureMilkdown instead: onReady is called too late for that.

previewStyle

How the markdown mode displays the source and its preview. It has no effect in the WYSIWYG mode.

  • vertical (default): the source and the preview are displayed side by side.
  • tab: the source and the preview are displayed in two tabs, Write and Preview, at the top of the input.
<MarkdownInput
    source="description"
    initialEditType="markdown"
    previewStyle="tab"
/>

ref

If you need to interact with the editor, you can access it with a ref. Its getInstance() method returns the Crepe editor instance, or undefined until the editor is created.

The following example calls the getMarkdown method of the editor to display the markdown content.

import { useRef, useState } from 'react';
import { Button, Edit, SimpleForm } from 'react-admin';
import { Alert } from '@mui/material';
import { MarkdownEditorRef, MarkdownInput } from '@react-admin/ra-markdown';

export const PostEdit = () => {
    const editorRef = useRef<MarkdownEditorRef>(null);
    const [markdown, setMarkdown] = useState<string>();
    return (
        <Edit>
            <SimpleForm>
                <MarkdownInput source="body" ref={editorRef} />
                <Button
                    label="Get markdown value"
                    onClick={() =>
                        setMarkdown(
                            editorRef.current?.getInstance()?.getMarkdown()
                        )
                    }
                />
                {markdown && (
                    <Alert severity="success">Markdown: {markdown}</Alert>
                )}
            </SimpleForm>
        </Edit>
    );
};
import { useRef, useState } from "react";
import { Button, Edit, SimpleForm } from "react-admin";
import { Alert } from "@mui/material";
import { MarkdownInput } from "@react-admin/ra-markdown";

export const PostEdit = () => {
    const editorRef = useRef(null);
    const [markdown, setMarkdown] = useState();
    return (
        <Edit>
            <SimpleForm>
                <MarkdownInput source="body" ref={editorRef} />
                <Button
                    label="Get markdown value"
                    onClick={() => setMarkdown(editorRef.current?.getInstance()?.getMarkdown())}
                />
                {markdown && <Alert severity="success">Markdown: {markdown}</Alert>}
            </SimpleForm>
        </Edit>
    );
};

Tip: To read or change the value of the input, prefer the form API (e.g. useFormContext().setValue()): the editor is updated when the form value changes.

source

Specifies the field of the record that the input should edit. It is required.

<Form record={{ id: 123, title: 'Hello, world!', body: '**Lorem Ipsum**' }}>
    <MarkdownInput source="body" />
    {/* default value is "**Lorem Ipsum**" */}
</Form>

toolbarItems

The WYSIWYG editor displays a toolbar with all the built-in buttons by default. Use the toolbarItems prop to choose its buttons, and to add your own. It accepts an array of button groups, separated by a divider in the toolbar.

The built-in buttons are referenced by name: heading, bold, italic, strike, code, ul, ol, task, link, image, table, codeblock, quote and hr.

A custom button is an object with:

  • name: a unique name
  • icon: the content of the button, as an SVG or HTML string
  • onClick: the function called on click. It receives the Milkdown context, to run editor commands
  • active (optional): a function receiving the Milkdown context, returning true to highlight the button

The following example shows a toolbar with a few built-in buttons, and a custom button that displays an alert when clicked.

import { Edit, SimpleForm } from 'react-admin';
import { MarkdownInput } from '@react-admin/ra-markdown';

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <MarkdownInput
                source="description"
                toolbarItems={[
                    ['heading', 'bold', 'italic', 'strike'],
                    [
                        {
                            name: 'custom',
                            icon: '<i>Custom Button</i>',
                            onClick: () => alert('Custom Button action'),
                        },
                    ],
                ]}
            />
        </SimpleForm>
    </Edit>
);
import { Edit, SimpleForm } from "react-admin";
import { MarkdownInput } from "@react-admin/ra-markdown";

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <MarkdownInput
                source="description"
                toolbarItems={[
                    ["heading", "bold", "italic", "strike"],
                    [
                        {
                            name: "custom",
                            icon: "<i>Custom Button</i>",
                            onClick: () => alert("Custom Button action"),
                        },
                    ],
                ]}
            />
        </SimpleForm>
    </Edit>
);

Tip: The toolbar is only displayed in the WYSIWYG editor (and in the preview of the markdown mode), not in the markdown source.

Tip: toolbarItems is only read when the editor is created: changing it afterwards has no effect.

useCommandShortcut

The editor supports keyboard shortcuts to format the content (Mod is Cmd on macOS, and Ctrl elsewhere):

Shortcut Command
Mod-b Bold
Mod-i Italic
Mod-e Inline code
Mod-Alt-x Strikethrough
Mod-Shift-b Blockquote
Mod-Alt-c Code block
Mod-Alt-0 Paragraph
Mod-Alt-1 to Mod-Alt-6 Heading 1 to 6
Mod-Alt-7 Ordered list
Mod-Alt-8 Bullet list

Set useCommandShortcut to false to disable them. The other keys (e.g. Enter, Tab or undo / redo) still work.

<MarkdownInput source="description" useCommandShortcut={false} />

Tip: useCommandShortcut is only read when the editor is created: changing it afterwards has no effect.

Styling

<MarkdownInput> renders the following CSS classes, that you can target to customize its look, e.g. with the sx prop of a parent component:

Class Element
RaMarkdownInput-editor The editor container, including the mode tabs
RaMarkdownInput-modeSwitch The bar of mode tabs, at the bottom of the editor
RaMarkdownInput-modeTab-markdown The Markdown mode tab
RaMarkdownInput-modeTab-wysiwyg The WYSIWYG mode tab
milkdown The WYSIWYG editor (use .milkdown .ProseMirror for its content)

The active mode tab has the aria-pressed="true" attribute.

<SimpleForm
    sx={{
        '& .RaMarkdownInput-editor': { borderRadius: 0 },
        '& .RaMarkdownInput-modeSwitch': { display: 'none' },
    }}
>
    <MarkdownInput source="description" />
</SimpleForm>

CHANGELOG

v6.0.1

2026-10-26

  • Add the configureMilkdown prop to <MarkdownInput>, <MarkdownField> and <MarkdownEditor>, to register Milkdown plugins before the editor creation
  • Add the onReady prop to <MarkdownInput>, <MarkdownField> and <MarkdownEditor>, called once the editor is created (replaces the Toast UI onLoad option)
  • Fix <MarkdownInput readOnly> and <MarkdownInput disabled> remaining editable

v6.0.0

2026-10-26

Breaking Changes

  • <MarkdownInput> and <MarkdownField> no longer accept the Toast UI editor and viewer options. Only height, initialEditType, previewStyle, toolbarItems and useCommandShortcut are still supported. The other options (e.g. hideModeSwitch, placeholder, plugins, language, customHTMLRenderer, linkAttributes) and the event handlers (e.g. onLoad, onKeydown) are removed.

  • The custom buttons of toolbarItems use a new format: the button content is an icon string instead of an el HTML element, and the click handler is passed as onClick. The tooltip option is removed.

-function createLastButton() {
-    const button = document.createElement('button');
-    button.innerHTML = `<i>Custom Button</i>`;
-    button.addEventListener('click', () => {
-        alert('Custom Button action');
-    });
-    return button;
-}
-
 <MarkdownInput
     source="description"
     toolbarItems={[
         ['heading', 'bold', 'italic', 'strike'],
         [
             {
-                el: createLastButton(),
-                tooltip: 'Custom Command',
                 name: 'custom',
+                icon: '<i>Custom Button</i>',
+                onClick: () => alert('Custom Button action'),
             },
         ],
     ]}
 />
  • The indent, outdent and scrollSync built-in toolbar buttons are removed.

  • The ref of <MarkdownInput> is now typed MarkdownEditorRef, and its getInstance() method returns a Crepe editor (or undefined until the editor is created) instead of a Toast UI editor. getMarkdown() still works, but the other Toast UI methods (e.g. setMarkdown(), insertText(), changeMode()) don't exist. To change the value, use the form API instead.

-import { Editor, MarkdownInput } from '@react-admin/ra-markdown';
+import { MarkdownEditorRef, MarkdownInput } from '@react-admin/ra-markdown';
+import { useFormContext } from 'react-hook-form';

-const editorRef = useRef<Editor>(null);
+const editorRef = useRef<MarkdownEditorRef>(null);
+const { setValue } = useFormContext();

-const markdown = editorRef.current.getInstance().getMarkdown();
+const markdown = editorRef.current?.getInstance()?.getMarkdown();

-editorRef.current.getInstance().setMarkdown('Hello world');
+setValue('body', 'Hello world');
  • The toastui-editor-* CSS classes no longer exist, and the RaMarkdownInput theme key is no longer supported. If you customized the editor styles, target the new RaMarkdownInput-* classes (see the Styling section) or the Milkdown classes (e.g. .milkdown .ProseMirror) instead, with the sx prop of <MarkdownField> or of a parent of <MarkdownInput>, or the RaMarkdownField and RaMarkdownEditor theme keys.

v5.2.0

2026-09-14

  • Add support for Material UI v9

v5.1.2

2025-10-28

  • Disable toast-ui telemetry for <MarkdownField> and <MarkdownInput>

v5.1.1

2025-06-24

  • Fix <MarkdownField> not updating when its value changes

v5.1.0

2025-06-16

  • Make components compatible with Material UI v7

v5.0.2

2025-03-21

  • Fix <MarkdownInput helperText> prop type

v5.0.1

2025-03-11

  • Fix <MarkdownInput> does not show its error state like other inputs

v5.0.0

2024-07-25

  • Upgrade to react-admin v5
  • [TypeScript]: Enable strictNullChecks

v4.2.0

2023-05-24

  • Upgraded to react-admin 4.10.6

v4.1.5

2023-03-02

  • (feat) Add dark mode support to MarkdownInput
  • (fix) Fix MarkdownInput should support label={false}
  • (doc) Fix MarkdownInput custom Editor options example

v4.1.4

2023-01-19

  • (fix) Fix usage with React 18

v4.1.3

2023-01-16

  • (fix) Fix MarkdownInput does not apply value when it has changed from outside the input

v4.1.2

2022-11-15

  • (fix) Fix emotion warning about potentially unsafe CSS pseudo class ":first-child" when doing SSR

v4.1.1

2022-10-18

  • (fix) Fix compatibility with Next.JS (toastui's css is now embedded in ra-markdown rather than requiring a global css import)

v4.1.0

2022-10-17

v4.0.1

2022-06-08

  • (fix) Update peer dependencies ranges (support React 18)

v4.0.0

2022-06-07

  • Upgrade to react-admin v4
  • Upgraded toast-ui/react-editor to v3

Breaking Changes

toast-ui/react-editor is now using ProseMirror. If you customized the editor or the viewer with plugins, you should follow their upgrade guide.

It also means we had to drop the default support for syntax highlighting. Indeed, the new toast-ui/react-editor requires to add a plugin for that (code-syntax-highlight) and to specify the languages to support.

import { Edit, EditProps, SimpleForm, TextInput } from 'react-admin';
import { MarkdownInput } from '@react-admin/ra-markdown';
+import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';
+import 'prismjs/themes/prism.css';
+import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';
+import Prism from 'prismjs';
+import 'prismjs/components/prism-jsx.js';

+ const options = {
+    plugins: [codeSyntaxHighlight]
+ };

const PostEdit = (props: EditProps) => (
    <Edit {...props}>
        <SimpleForm>
            <TextInput source="title" />
            <MarkdownInput
                source="description"
+                options={options}
            />
        </SimpleForm>
    </Edit>
);

v1.3.2

2021-12-20

  • (fix) MarkdownField can't process nested source

v1.3.1

2021-06-29

  • (fix) Update peer dependencies ranges (support react 17)

v1.3.0

2021-06-15

  • Add support for helperText and validation to <MarkdownInput>
  • Export the props interfaces of both <MarkdownInput> and <MarkdownField>

v1.2.0

2020-10-17

  • Upgrade to react-admin 3.15
  • (feat) Ensure MarkdownField styles are customizable through MUI theme by providing their key (RaMarkdownField).

v1.1.0

2020-10-05

  • Upgrade to react-admin 3.9

v1.0.3

2020-09-30

  • Update Readme

v1.0.2

2020-09-16

  • (deps) Upgrade dependencies

v1.0.1

2020-08-31

  • (fix) Fix fullWidth support

v1.0.0

2020-07-31

  • First release