Skip to content
Components, switch product

Markdown editor

<gui-markdown> is a Markdown editor. It renders a native <textarea> under a toolbar of formatting buttons, with a preview of the rendered text. Its value is the Markdown source.

Code of the example above
MarkdownBasic.tsx
import { GuiMarkdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function MarkdownBasic() {
return <GuiMarkdown label="Release notes" />;
}

The element’s entry point is @golemui/gui-components/markdown, which registers <gui-markdown> and <gui-markdown-text>, the element that draws its preview. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

NotesField.tsx
import { useState } from 'react';
import { GuiMarkdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function NotesField() {
const [notes, setNotes] = useState('');
return (
<GuiMarkdown
label="Release notes"
name="notes"
value={notes}
onGuiChange={(event) => setNotes(event.detail.value ?? '')}
/>
);
}

Properties

Property Type Default Description
localeId attribute locale-id string 'en' BCP 47 locale for formatting and parsing, such as en-US or es.
value string – The Markdown text.
tools string[] – Toolbar buttons, in order: H heading, B bold, I italic, S strikethrough, Q quote, L link, OL and UL lists, and | for a separator. All by default.
placeholder string – Text shown while the control is empty.
autocomplete string – The autocomplete hint passed to the inner native control.
counterMode attribute counter-mode 'remaining' | 'current' – With maxLength, whether the counter shows the characters left (remaining) or used (current).
minimumHeight attribute minimum-height number – Minimum height of the field, in pixels.
autoGrow attribute auto-grow boolean false Grows the field with its content instead of scrolling.
defaultOpenPreview attribute default-open-preview boolean – Opens with the preview shown.
maxLength attribute maxlength number – The number of characters the counter counts against. It only drives the counter: longer text is not blocked, and the counter marks it as over the limit.
headingTitle attribute heading-title string – Tooltip and accessible name of the heading button. An empty value keeps the default.
boldTitle attribute bold-title string – Tooltip and accessible name of the bold button. An empty value keeps the default.
italicTitle attribute italic-title string – Tooltip and accessible name of the italic button. An empty value keeps the default.
strikethroughTitle attribute strikethrough-title string – Tooltip and accessible name of the strikethrough button. An empty value keeps the default.
quoteTitle attribute quote-title string – Tooltip and accessible name of the quote button. An empty value keeps the default.
linkTitle attribute link-title string – Tooltip and accessible name of the link button. An empty value keeps the default.
orderedListTitle attribute ordered-list-title string – Tooltip and accessible name of the numbered list button. An empty value keeps the default.
unorderedListTitle attribute unordered-list-title string – Tooltip and accessible name of the bulleted list button. An empty value keeps the default.
splitViewTitle attribute split-view-title string – Tooltip and accessible name of the preview button. An empty value keeps the default.
toolbarAriaLabel attribute toolbar-aria-label string – Accessible name of the toolbar. An empty value removes it.
dependencies { markdown?: MarkdownParser } – Provides the markdown parser used by the preview. The preview inserts its HTML without sanitizing it, see MarkdownParser.
uid string – The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration.

Form control properties

Every field has these, from the base class of the form controls.

Property Type Default Description
name string – The name the value is submitted under. Without it the element stays out of its form. Reflected, like a native control's: the form reads a single value's name from the attribute.
label string – The visible label, which also names the control for assistive technology. Without one the control has no accessible name; the date, time, range, tags and file fields warn about it in development builds.
hint string – Help text shown under the label and announced as the control's description.
errors string[] [] Error messages to show, typically from your own validation. They show as soon as they are set, unless touched is false.
touched boolean – Whether the user has interacted with the control. Leave it unset unless you validate on interaction: false holds errors back until it becomes true.
required boolean false The control needs a value: it is marked as required and, with a name, blocks submission.
readOnly attribute readonly boolean – The value can be read and focused but not changed.
disabled boolean – Disabled by its own attribute or by a disabled <fieldset> around it. Setting the property sets the attribute, like a native control's.
form read-only, no attribute HTMLFormElement | null – The <form> the element belongs to, if any.
validity read-only, no attribute ValidityState – The element's validity, like a native control's.
validationMessage read-only, no attribute string – The message the browser shows for an invalid value.

Methods

Method Description
checkValidity(): boolean –
reportValidity(): boolean –

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-input The user changed the value. detail.value is the new value.
gui-change The user committed the text, on blur or with a toolbar command. detail.value is the Markdown.
gui-blur Focus left the control.

CSS custom properties

Property Default Description
--gui-md-text-color – Text color.
--gui-md-line-height – Line height.
--gui-md-heading-color – Heading color.
--gui-md-heading-weight – Heading font weight.
--gui-md-link-color – Link color.
--gui-md-link-hover-color – Link color on hover.
--gui-md-blockquote-bg – Quote background.
--gui-md-blockquote-border-color – Quote border color.
--gui-md-blockquote-color – Quote text color.
--gui-md-code-bg – Code background.
--gui-md-code-color – Code text color.
--gui-md-code-radius – Code corner radius.
--gui-md-hr-color – Horizontal rule color.

Each button writes Markdown into the text, at the caret or around the selection:

ButtonWrites
Heading# at the start of each selected line.
Bold** around the selection.
Italic_ around the selection.
Strikethrough~~ around the selection.
Quote> at the start of each selected line.
Link[ and ](url) around the selection.
Ordered List1. , 2. … at the start of each selected line.
Unordered List- at the start of each selected line.

A button shows as pressed while the caret is inside its format, and pressing it again removes the format. The last button, Split View, shows and hides the preview.

tools picks the buttons and their order, as a JSON array of H (heading), B (bold), I (italic), S (strikethrough), Q (quote), L (link), OL and UL (the lists), and | for a separator. The default is ["H", "B", "I", "S", "Q", "L", "|", "OL", "UL"]. The Split View button is always there, at the end.

Code of the example above
MarkdownTools.tsx
import { GuiMarkdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const tools = ['B', 'I', 'L', '|', 'UL'];
export function MarkdownTools() {
return <GuiMarkdown label="Comment" tools={tools} />;
}

The preview renders the Markdown as HTML. The element ships no Markdown parser: give it yours in the dependencies property, as { markdown: { parse } }, where parse takes Markdown and returns HTML. In TypeScript, parse returns SanitizedHtml: see The parser. Without one the preview stays empty. default-open-preview opens the editor with the preview shown.

Code of the example above
MarkdownPreview.tsx
import type { SanitizedHtml } from '@golemui/gui-components';
import snarkdown from 'snarkdown';
import { GuiMarkdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const dependencies = {
markdown: { parse: (md: string) => snarkdown(md) as SanitizedHtml },
};
const recipe = `## Pastel de nata
Bake at **250 °C** for _12 minutes_.
- Puff pastry
- Egg yolks
- Cinnamon`;
export function MarkdownPreview() {
return (
<GuiMarkdown
id="recipe"
label="Recipe"
defaultOpenPreview
value={recipe}
dependencies={dependencies}
/>
);
}
  • Narrow fields. While the element is under 768 pixels wide, the preview takes the place of the text instead of sitting next to it. Press Split View again to go back to the text.
  • Styles. The preview is a <gui-markdown-text>, with its text styles and --gui-md-* tokens. See Markdown text.
  • Untrusted text. The preview inserts the parser’s HTML as it is. When the text comes from other people, use a parser that escapes HTML, or sanitize its output. See Untrusted Markdown.

maxlength, counter-mode, minimum-height and auto-grow work as on the textarea: the counter counts the Markdown source, and the limit doesn’t stop the user from typing past it.

Code of the example above
MarkdownCounter.tsx
import { GuiMarkdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const tools = ['B', 'I', 'L', '|', 'UL'];
export function MarkdownCounter() {
return (
<GuiMarkdown
label="Changelog entry"
maxLength={200}
minimumHeight={80}
autoGrow
tools={tools}
/>
);
}

disabled disables the field and every button. readonly disables the formatting buttons but keeps Split View, so the user can read the preview.

With a name, <gui-markdown> submits the Markdown source, as text, under that name, and required blocks an empty submit. It never submits the rendered HTML.

Code of the example above
MarkdownNativeForm.tsx
import { useState, type SubmitEvent } from 'react';
import { GuiButton, GuiMarkdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function MarkdownNativeForm() {
const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(event.currentTarget));
setResult(`Submitted ${JSON.stringify(data)}`);
};
return (
<form id="announcement" onSubmit={onSubmit}>
<GuiMarkdown label="Announcement" name="body" required />
<GuiButton type="submit" label="Publish" />
<p id="announcement-result" role="status">
{result}
</p>
</form>
);
}

How the elements submit, validate and reset is in Native forms.

<gui-markdown> renders into the light DOM, so you can style every part with plain CSS. These are the parts and their public classes:

gui-markdown
<gui-markdown class="gui-field">
<label class="gui-label" for="notes" id="notes_label">
<span class="gui-label__text">Release notes</span>
</label>
<div class="gui-widget gui-markdown--with-preview">
<div
class="gui-markdown__toolbar"
role="toolbar"
aria-label="Text formatting"
>
<ul role="presentation">
<li role="presentation">
<button
type="button"
class="gui-markdown__toolbar-button gui-markdown__toolbar-button--active"
aria-label="Heading"
aria-pressed="true"
title="Heading"
>
<svg aria-hidden="true">…</svg>
</button>
</li>
<!-- One item per tool. A separator: -->
<li role="presentation">
<span class="gui-markdown__toolbar-separator"
><svg aria-hidden="true">…</svg></span
>
</li>
<li role="presentation">
<button
type="button"
class="gui-markdown__toolbar-button gui-markdown__toolbar-button--active"
aria-label="Split View"
aria-pressed="true"
title="Split View"
>
<svg aria-hidden="true">…</svg>
</button>
</li>
</ul>
</div>
<div class="gui-markdown__container">
<textarea
id="notes"
class="gui-widget-input"
style="min-height: 120px;"
></textarea>
<section class="gui-markdown__preview" style="min-height: 120px;">
<gui-markdown-text
><h2>Pastel de nata</h2>
…</gui-markdown-text
>
</section>
</div>
</div>
<div class="gui-markdown--validation">
<div>
<div
class="gui-validator gui-validator--empty"
id="notes_errors"
role="alert"
></div>
</div>
<div class="gui-markdown--counter">
<span>200</span>
<span> / 200</span>
</div>
</div>
</gui-markdown>
ClassPart
gui-fieldThe element itself. Lines its parts up with the fields next to it in a gui-grid row.
gui-labelThe label, with the text and the hint.
gui-label__textThe label text and the required marker.
gui-widget-hintThe hint.
gui-widgetThe box around the toolbar, the text and the preview.
gui-markdown--with-previewOn the box while the preview shows.
gui-markdown__toolbarThe toolbar.
gui-markdown__toolbar-buttonA toolbar button.
gui-markdown__toolbar-button--activeOn a button while its format is under the caret, and on Split View while it is on.
gui-markdown__toolbar-button--disabledOn the formatting buttons while the element is disabled or read-only.
gui-markdown__toolbar-separatorA separator, from | in tools.
gui-markdown__containerThe row with the text and the preview.
gui-widget-inputThe <textarea>.
gui-markdown__previewThe preview, while it shows.
gui-markdown--validationThe row under the field, with the errors and the counter.
gui-validatorThe errors container. It is always there, so the alert role announces new errors.
gui-validator--emptyOn the errors container while there are no errors.
gui-validator__errorOne error message.
gui-markdown--counterThe counter, with maxlength only.
gui-markdown--counter__errorOn the counter while the text is over the limit.

The ids derive from uid, or from a generated id such as gui-markdown-3 when you don’t set one. Select by class or by tag, not by id.

The editor reads these design tokens. Set them on :root to change every editor, or on any element to change the editors inside it:

TokenWhat it styles
--gui-radius-mdThe corner radius of the box.
--gui-border-defaultThe border of the box, under the toolbar and beside the preview.
--gui-bg-surfaceThe background of the toolbar and the text.
--gui-text-defaultThe text, the label and the toolbar icons.
--gui-font-familyThe font.
--gui-font-smThe text size of the label, the text and the counter.
--gui-font-xsThe text size of the hint and the errors.
--gui-text-secondaryThe hint.
--gui-space-1The toolbar’s top and bottom padding, the buttons’ padding and the gaps between them.
--gui-space-2The side padding of the toolbar, the text and the preview, and the space over the counter.
--gui-space-3The top and bottom padding of the text and the preview.
--gui-space-7The height of the toolbar buttons.
--gui-radius-smThe corner radius of the toolbar buttons.
--gui-bg-surface-hoverA toolbar button under the pointer.
--gui-intent-primaryThe icon of a button under the pointer, and the background of a pressed button.
--gui-intent-primary-textThe icon of a pressed button.
--gui-intent-primary-hoverA pressed button under the pointer.
--gui-border-focusThe border while the text is focused.
--gui-shadow-focusThe focus ring of the box and of the toolbar buttons.
--gui-intent-errorThe border, the messages and the counter over the limit.
--gui-shadow-focus-errorThe focus ring when there are errors.
--gui-bg-disabledThe background of the text when disabled.
--gui-text-disabledThe text when disabled.

The preview also reads the --gui-md-* tokens: see Markdown text.

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Name. The label names the text field. Without one the field has no accessible name.
  • Description and errors. The hint is the field’s description, through aria-describedby. While errors show, the textarea has aria-invalid="true" and points at them with aria-errormessage, and screen readers announce new errors as they appear.
  • Toolbar. The toolbar has the toolbar role and the name “Text formatting”. toolbar-aria-label changes it; an empty value removes it.
  • Buttons. Each button has a name and a tooltip, such as “Bold”, and aria-pressed says whether its format is under the caret, or whether the preview shows. The *-title attributes, such as bold-title and split-view-title, change them on one editor; an empty value keeps the default. To translate them for the whole app, use the keys textFormatting, heading, bold, italic, strikethrough, quote, link, orderedList, unorderedList and splitView, see i18n.
  • Focus. A formatting button puts focus back in the text after it writes its format.
  • Counter. The counter is visual only: screen readers don’t announce it.
KeyAction
TabMoves focus through the toolbar buttons, one at a time, then to the text.
Enter or Space on a buttonApplies or removes its format, or shows or hides the preview.
Enter in the textAdds a new line. Leaving the text commits the value and fires gui-change.
Every editing keyWorks as in any native textarea. There are no formatting shortcuts.

What every element shares, such as translating the browser messages, is in Accessibility.