Components
Markdown Editor
A markdown text editor with formatting toolbar and live preview. Built-in English/French language toggle with per-language value storage — or single-language mode for simpler use cases. Uses `marked` for accurate markdown rendering.
Installation
bunx @happlyui/cli@latest add markdown-editor
Dependencies
npm packages
@remixicon/reactmarked
Registry dependencies
These are automatically installed when you add this component.
form-field-contextuse-form-field-bindingswitch-toggletextareatooltiphapply-ui-utils
Usage
import * as MarkdownEditor from "@/components/ui/markdown-editor"
// Multi-language (default) — value is { en: "", fr: "" }
<MarkdownEditor.Composed
placeholder="Start writing..."
onChange={(values) => console.log(values)}
/>
// Single-language — value is a plain string
<MarkdownEditor.Composed
toggleItems={false}
placeholder="Start writing..."
onChange={(md) => console.log(md)}
/>
Examples
Default
Multi-language editor with built-in English/French toggle. Manages `{ en: "", fr: "" }` state internally.
<MarkdownEditor.Composed
placeholder="Describe your ideal successor..."
/>
Single Language
No language toggle. Value is a plain markdown string.
<MarkdownEditor.Composed
toggleItems={false}
placeholder="Write in a single language..."
/>
Controlled Multi-Language
Parent owns the per-language values object. The component handles language switching internally.
{
"en": "Hello **world**",
"fr": "Bonjour **le monde**"
}const [values, setValues] = useState<LocalizedValue>({
en: '',
fr: '',
});
<MarkdownEditor.Composed
value={values}
onChange={setValues}
/>
Custom Toggle
Override the default English/French toggle with custom language items.
<MarkdownEditor.Composed
toggleItems={[
{ value: 'en', label: 'EN' },
{ value: 'fr', label: 'FR' },
{ value: 'es', label: 'ES' },
]}
defaultToggleValue="en"
/>
Compound
Full compound mode with custom toolbar layout using the useMarkdownFormatting hook.
const [previewing, setPreviewing] = useState(false);
const [value, setValue] = useState('');
const ref = useRef<HTMLTextAreaElement>(null);
const format = MarkdownEditor.useMarkdownFormatting(ref, setValue);
<MarkdownEditor.Root previewing={previewing}>
<MarkdownEditor.Toolbar>
<MarkdownEditor.ToolbarGroup>
<MarkdownEditor.ToolbarButton onClick={() => format('bold')}>B</MarkdownEditor.ToolbarButton>
</MarkdownEditor.ToolbarGroup>
<MarkdownEditor.Toggle items={MarkdownEditor.DEFAULT_TOGGLE_ITEMS} defaultValue="en" />
</MarkdownEditor.Toolbar>
<MarkdownEditor.Content ref={ref} value={value} onChange={(e) => setValue(e.target.value)} />
</MarkdownEditor.Root>
With FormField
Inside a FormField with label, required indicator, and hint.
<FormField.Root label="Description" required hint="Describe in detail.">
<MarkdownEditor.Composed />
</FormField.Root>
With Error
Error state inside a FormField.
<FormField.Root label="Description" error="Required.">
<MarkdownEditor.Composed hasError />
</FormField.Root>
Disabled
Disabled editor with muted toolbar and content.
<FormField.Root label="Description" disabled>
<MarkdownEditor.Composed disabled />
</FormField.Root>
With Default Content
Editor pre-filled with per-language markdown content. Toggle between languages to see different content. Click preview to see rendered markdown.
<MarkdownEditor.Composed
defaultValue={{
en: '# Hello\n\nThis is **bold** text.',
fr: '# Bonjour\n\nCeci est du texte en **gras**.',
}}
/>
API Reference
MarkdownEditor.Root
The root container. Provides context for hasError, disabled, and previewing state.
| Prop | Type | Default | Description |
|---|---|---|---|
hasError | boolean | false | Error state applied to the content area. |
disabled | boolean | false | Disables toolbar and content. |
previewing | boolean | false | Whether the editor is in preview mode. |
MarkdownEditor.Toolbar
The toolbar container with role='toolbar'.
| Prop | Type | Default | Description |
|---|
MarkdownEditor.ToolbarButton
An individual toolbar button with hover and active states.
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Whether the button is active/pressed. |
tooltip | string | - | Tooltip label shown on hover (xsmall size). |
MarkdownEditor.ToolbarGroup
Groups toolbar buttons with consistent spacing.
| Prop | Type | Default | Description |
|---|
MarkdownEditor.Toggle
Built-in SwitchToggle.Group for the toolbar. Used in compound mode.
| Prop | Type | Default | Description |
|---|---|---|---|
items | SwitchToggleGroupItem[] | - | Array of toggle items. |
value | string | - | Controlled active value. |
defaultValue | string | - | Default active value. |
onValueChange | (value: string) => void | - | Callback on change. |
MarkdownEditor.Content
The editor content area. Renders a textarea in edit mode and rendered HTML in preview mode.
| Prop | Type | Default | Description |
|---|---|---|---|
hasError | boolean | - | Override error state from context. |
height | string | '200px' | Min-height of the editor content area. |
value | string | - | The markdown content string for the current language. |
MarkdownEditor.Composed
Props-driven composed mode with two variants: multi-language (default, with English/French toggle) and single-language (toggleItems={false}). In multi-language mode, value/onChange use a `LocalizedValue` object (`{ en: "", fr: "" }`). In single-language mode, value/onChange use a plain string.
| Prop | Type | Default | Description |
|---|---|---|---|
toggleItems | SwitchToggleGroupItem[] | false | DEFAULT_TOGGLE_ITEMS | Pass false to hide the toggle (single-language mode). Pass an array to override the default English/French items. Omit for default English/French toggle. |
value | LocalizedValue | string | - | Controlled value. Object when toggle is shown, string when toggleItems={false}. |
defaultValue | LocalizedValue | string | - | Initial uncontrolled value. Object when toggle is shown, string when toggleItems={false}. |
onChange | (values: LocalizedValue) => void | (markdown: string) => void | - | Callback on change. Receives the full values object in multi-language mode, or a plain string in single-language mode. |
placeholder | string | - | Placeholder text for the textarea. |
hasError | boolean | false | Error state. |
disabled | boolean | false | Disabled state. |
toggleValue | string | - | Controlled active language (multi-language mode only). |
defaultToggleValue | string | 'en' | Default active language (multi-language mode only). |
onToggleChange | (value: string) => void | - | Callback when the active language changes (multi-language mode only). |
containerClassName | string | - | ClassName for the root container. |
contentClassName | string | - | ClassName for the content area. |
height | string | '200px' | Min-height of the editor content area. |
useMarkdownFormatting
Hook that returns a function to apply markdown formatting actions to a textarea. Use with compound mode for custom toolbar buttons.
| Prop | Type | Default | Description |
|---|---|---|---|
textareaRef | RefObject<HTMLTextAreaElement | null> | - | Ref to the textarea element. |
onChange | (value: string) => void | - | Callback when the value changes from formatting. |
renderMarkdown
Converts a markdown string to HTML using marked with GFM support. Used internally for preview mode but exported for custom use.
| Prop | Type | Default | Description |
|---|---|---|---|
md | string | - | The markdown string to render. |