Skip to content

Customization

Galley is intentionally style-agnostic. The package ships a base stylesheet, but the editor surface is controlled through CSS variables and semantic classes so it can sit inside your product design system.

import '@inkyquill/galley-editor/style.css';

The stylesheet covers:

  • CodeMirror layout inside the Galley shell.
  • Toolbar and footer structure.
  • Light and dark CSS variables.
  • Markdown semantic classes such as ge-h1, ge-bold, ge-code-fence, and ge-table.
  • Lezer token classes such as tok-keyword, tok-string, and tok-comment.

You can omit the stylesheet, but then your app must provide the CodeMirror and ge-* styles itself.

<GalleyEditor theme="auto" />
<GalleyEditor theme="light" />
<GalleyEditor theme="dark" />
<GalleyEditor theme="inherit" />

auto follows the user’s system color scheme. inherit leaves the wrapper without a local data-theme palette and follows the host’s root theme class or data-theme attribute (falling back to the system preference). Use it when an app owns the --ge-* variables. Override variables on the editor wrapper or a parent container:

.article-editor {
--ge-color-bg: #ffffff;
--ge-color-text: #111827;
--ge-color-border: #d8dee8;
--ge-color-link: #2454d6;
--ge-radius-editor: 8px;
--ge-content-padding: 32px 40px;
}

Galley applies semantic classes to rendered Markdown decorations:

ClassElement
ge-heading, ge-h1 through ge-h6Headings
ge-bold, ge-italic, ge-strikethroughInline emphasis
ge-code-inline, ge-code-fenceInline and fenced code
ge-linkLinks
ge-blockquoteBlockquotes
ge-tableTables
ge-image-frameImage widgets
ge-divider, ge-divider-widgetHorizontal rules
ge-checkbox, ge-completed-taskTask lists
ge-list-markerList markers

Rename classes when your system has established class names:

<GalleyEditor
classNames={{
h1: 'docs-heading-xl',
bold: 'docs-strong',
blockCode: 'docs-code-block',
}}
/>

Use className, editorClassName, and surface for layout-level styling:

<GalleyEditor
className="article-editor"
editorClassName="article-editor-cm"
surface={{
className: 'article-editor-shell',
contentPadding: '28px 36px',
toolbarPadding: '8px 12px',
}}
/>

Set surface.contentPadding when your host surface needs a different measure. The built-in toolbar uses Lucide icons; its accessible labels and effective shortcut tooltips remain available.

Keep toolbar buttons at least 44 by 44 CSS pixels when replacing chrome in touch-heavy interfaces.

Galley provides SVG icons for the built-in commands. Use toolbar.icons when you want Galley’s built-in toolbar behavior but your product’s icon set. The built-in buttons keep their command behavior, disabled states, accessible labels, and effective shortcut titles; the icon value only replaces the visible button contents.

import { Bold, Code2, Eye, Italic, Link } from 'lucide-react';
import { GalleyEditor } from '@inkyquill/galley-editor';
<GalleyEditor
toolbar={{
icons: {
bold: <Bold size={16} aria-hidden="true" />,
italic: <Italic size={16} aria-hidden="true" />,
link: <Link size={16} aria-hidden="true" />,
mode: ({ mode }) =>
mode === 'preview'
? <Eye size={16} aria-hidden="true" />
: <Code2 size={16} aria-hidden="true" />,
},
}}
/>;

Icon values can be React nodes or render functions. Use a render function when the icon depends on toolbar state, such as the current editor mode:

<GalleyEditor
toolbar={{
icons: {
mode: ({ mode }) => <span>{mode === 'markdown' ? 'MD' : mode}</span>,
},
}}
/>;

Use aria-hidden="true" on decorative icon components because Galley already supplies the button labels.

Use toolbar and footer slots when you want to keep Galley’s built-in controls and add app-owned actions, status pills, save buttons, or workflow state.

<GalleyEditor
toolbar={{
before: <span className="ge-status-pill">Draft</span>,
after: ({ execCommand, canEdit }) => (
<>
<button
type="button"
className="ge-toolbar-button"
disabled={!canEdit}
onMouseDown={(event) => event.preventDefault()}
onClick={() => execCommand('insertHr')}
>
Section
</button>
<button
type="button"
className="ge-toolbar-button"
onMouseDown={(event) => event.preventDefault()}
onClick={saveDraft}
>
Save
</button>
</>
),
}}
footer={{
before: <span className="ge-status-pill">Local draft</span>,
after: ({ mode, wordCount }) => (
<span>
{mode} - {wordCount} words
</span>
),
}}
/>;

Call event.preventDefault() from mouse-down handlers on toolbar buttons so clicking a control does not steal the editor selection before the command runs.

For a fully custom toolbar, disable the built-in toolbar and call commands from your own controls:

const editor = useRef<GalleyHandle>(null);
<div className="app-toolbar">
<button
type="button"
aria-label="Bold"
onMouseDown={(event) => event.preventDefault()}
onClick={() => editor.current?.execCommand('toggleBold')}
>
<Bold size={16} aria-hidden="true" />
</button>
<button
type="button"
onMouseDown={(event) => event.preventDefault()}
onClick={() => editor.current?.execCommand('insertTable')}
>
Insert table
</button>
</div>
<GalleyEditor ref={editor} toolbar={false} />;

Use real buttons with visible focus states and accessible names. Do not make icon-only controls without aria-label.

Use caseGuide
Replace rendered table block controlsPlugins and Renderers
Build a full command toolbarCommands
Track selection for contextual controlsAPI Reference
Theme the shell and toolbarSurface Hooks