Editor
Inkwell is a WYSIWYG Markdown editor for React. The content you see is visually formatted, but the underlying content is always the Markdown source string. The Markdown source is the text.
The primary API is the InkwellEditor component. Give it your content string
and an onChange handler.
import { InkwellEditor } from "@railway/inkwell";import { useState } from "react";
function App() { const [content, setContent] = useState("# Hello **world**");
return ( <InkwellEditor content={content} onChange={setContent} placeholder="Write something..." /> );}content is the Markdown source string. onChange fires on every edit with
the updated content.
Editor controller
Section titled “Editor controller”Use a ref when you need imperative editor actions. The ref exposes
getState(), focus(), clear(), setContent(), and insertContent().
import { type InkwellEditorHandle, InkwellEditor } from "@railway/inkwell";import { useRef } from "react";
const editorRef = useRef<InkwellEditorHandle>(null);
<InkwellEditor ref={editorRef} content={content} onChange={setContent} />;
editorRef.current?.getState().content;editorRef.current?.focus({ at: "end" });editorRef.current?.setContent("Replacement");editorRef.current?.insertContent(" **more**");setContent() and clear() replace editor content without calling onChange.
insertContent() behaves like an edit at the current selection and flows through
normal change handling.
Formatting
Section titled “Formatting”Type Markdown syntax and the editor renders it visually as you go. Type
## at the start of a line and it becomes a heading. Wrap text in **
and it turns bold. The Markdown characters stay in the document — they’re
styled to show you what the output will look like.
All formatting is enabled by default.
Block formatting
Section titled “Block formatting”Use these Markdown patterns at the start of a line. Headings, blockquotes, and code fences also have typing triggers; block images are recognized when Markdown is loaded, pasted, or inserted. List markers stay plain text in the editor.
| Pattern | Result |
|---|---|
# through ###### | Headings (h1–h6) |
> | Blockquotes |
``` | Fenced code blocks |
 on its own line | Block image |
Inline formatting
Section titled “Inline formatting”Wrap text with these markers anywhere in a line:
| Syntax | Result |
|---|---|
**text** | Bold |
_text_ | Italic |
~~text~~ | |
`text` | Inline code |
[text](url) | Link |
https://... | Auto-linked URL |
Pasting a URL on top of a non-empty selection wraps the selection as
[selected](pasted-url). Pasting a URL with no selection drops it
in bare — the autolink decoration picks it up.
Keyboard shortcuts
Section titled “Keyboard shortcuts”| Shortcut | Action |
|---|---|
⌘B / Ctrl+B | Toggle bold |
⌘I / Ctrl+I | Toggle italic |
⌘D / Ctrl+D | Toggle strikethrough |
These shortcuts come from the built-in bubble menu plugin. You can customize or disable them.
Disabling formatting
Section titled “Disabling formatting”Use the features option to turn off specific block-level formatting.
All features are enabled by default — you only need this prop to
disable something.
<InkwellEditor content={content} onChange={setContent} features={{ codeBlocks: false, headings: { h4: false, h5: false, h6: false }, }}/>;| Feature | Default | Controls |
|---|---|---|
headings | true | Heading levels, with optional h1–h6 overrides |
blockquotes | true | Blockquotes (>) |
codeBlocks | true | Fenced code blocks (```) |
images | true | Block images ( on their own line) |
When a feature is disabled, its syntax is treated as source text with no visual formatting.
Syntax highlighting
Section titled “Syntax highlighting”Fenced code blocks are highlighted with highlight.js by default. Import a highlight.js CSS theme for colors to appear:
import "highlight.js/styles/github-dark.css";To use a different highlighter, pass it through the rehypePlugins option:
import { InkwellEditor } from "@railway/inkwell";import rehypeShiki from "@shikijs/rehype";
<InkwellEditor content={content} onChange={setContent} rehypePlugins={[[rehypeShiki, { theme: "github-dark" }]]}/>;The same rehypePlugins option is available on
InkwellRenderer.
Bubble menu
Section titled “Bubble menu”The editor includes a floating formatting toolbar by default. Select text and the toolbar appears with bold, italic, and strikethrough buttons.
To disable it:
<InkwellEditor content={content} onChange={setContent} bubbleMenu={false} />;To customize the toolbar items, see the Plugins guide.
Options reference
Section titled “Options reference”content
Section titled “content”Type: string | undefined — default: ""
The Markdown source content to display and edit.
onChange
Section titled “onChange”Type: (content: string) => void
Called on every document change with the updated content.
onStateChange
Section titled “onStateChange”Type: (state: InkwellEditorState) => void
Called with a full state snapshot when content, focus, editability, or
character count changes. The state includes content, isEmpty,
isFocused, isEditable, characterCount, characterLimit, and
overLimit.
placeholder
Section titled “placeholder”Type: string
Text shown when the editor is empty.
className
Section titled “className”Type: string
CSS class applied to the outer wrapper element. This is an alias for
classNames.root.
classNames
Section titled “classNames”Type: { root?: string; editor?: string }
CSS classes for editor slots. root applies to the outer wrapper; editor
applies to the editable surface.
styles
Section titled “styles”Type: { root?: React.CSSProperties; editor?: React.CSSProperties }
Inline styles for editor slots. Use styles.editor for sizing the editable
surface, such as height, minHeight, or width.
editable
Section titled “editable”Type: boolean — default: true
Whether users can edit the document. Pass false for read-only editor
states.
features
Section titled “features”Type: InkwellFeatures
Controls which block-level formatting the editor recognizes. All enabled by default. See Disabling formatting.
plugins
Section titled “plugins”Type: InkwellPlugin[]
Additional plugins to load. See Plugins.
bubbleMenu
Section titled “bubbleMenu”Type: boolean — default: true
Whether to include the built-in floating toolbar. Pass false to disable
it entirely, or use a custom bubble menu via plugins instead.
rehypePlugins
Section titled “rehypePlugins”Type: RehypePluginConfig[]
Custom rehype plugins for code block highlighting. Accepts a plugin
function or a tuple such as [plugin, ...options]. See
Syntax highlighting.
characterLimit
Section titled “characterLimit”Type: number
Sets a soft character budget for the editor. Typing past the limit is allowed — the editor doesn’t block input or truncate content.
When the limit is set, the editor renders a small count / limit readout
overlaying the top-right of the editor wrapper, but only once the count
reaches 80% of the limit (inclusive — at limit 50 the readout appears at
40, not 39). Under that threshold the readout stays hidden so it doesn’t
add visual noise to a near-empty editor. The readout sits on a solid
surface background and is absolutely positioned, so it visually layers
above wrapped text without ever shifting content. Below the limit the
readout is muted; above it the readout turns red, the wrapper picks up
the inkwell-editor-over-limit class, and the editor surface gains a
soft red halo (intentionally muted — not a hard ring). The overLimit
field on InkwellEditorState reflects the same condition so consumers
can disable submit buttons, etc.
Style the readout and the over-limit state with the
.inkwell-editor-character-count
classes.
onCharacterCount
Section titled “onCharacterCount”Type: (count: number, limit?: number) => void
Called whenever the editor recalculates character count.
submitOnEnter
Section titled “submitOnEnter”Type: boolean — default: false
When true, pressing Enter calls onSubmit(content) instead of inserting a
new line. Shift+Enter still inserts a line break. Useful for chat-style
surfaces.
onSubmit
Section titled “onSubmit”Type: (content: string) => void
Called when submitOnEnter handles Enter.