Getting Started
Build interactive text with custom markup - quick start in minutes.
Installation
Section titled “Installation”Install Markput using your preferred package manager:
# Reactnpm install @markput/react# Vuenpm install @markput/vue# Reactpnpm add @markput/react# Vuepnpm add @markput/vue# Reactyarn add @markput/react# Vueyarn add @markput/vue# Reactbun add @markput/react# Vuebun add @markput/vueRequirements: React 19+ for @markput/react, Vue 3.5+ for @markput/vue.
The tutorial below uses React; the Vue API mirrors it — see the Vue Storybook for live Vue examples.
Your First Editor
Section titled “Your First Editor”Let’s build a marked text editor with autocomplete in three steps.
Step 1: Render Marks
Section titled “Step 1: Render Marks”Here’s a basic editor rendering marked text. Try clicking the highlighted text:
import {MarkedInput} from '@markput/react'
export const Step1Demo = () => ( <MarkedInput Mark={({value, meta}) => <mark onClick={() => alert(meta)}>{value}</mark>} defaultValue="Hello @[World](123)!" />)How it works:
Markput uses a special markup syntax to represent interactive elements as plain text:
- Markup — a text pattern that encodes structured data:
@[__value__](__meta__) - Value — the visible text shown to users (e.g.,
World) - Meta — hidden metadata for your app (e.g.,
123- user ID) - Mark — a React component that renders the markup visually
When Markput encounters @[World](123) in the text:
- Parses the markup and extracts:
value="World",meta="123" - Renders your
Markcomponent with these props:<mark onClick={...}>{props.value}</mark> - Preserves the original text as a simple string — easy to save or send to any backend
Since Mark is a regular React component, you can style it, add click handlers (like the onClick that shows an alert), or use any React features.
Step 2: Add Autocomplete
Section titled “Step 2: Add Autocomplete”Add the options prop to enable autocomplete suggestions:
import {MarkedInput} from '@markput/react'
export const Step2Demo = () => ( <MarkedInput Mark={({value}) => <mark>@{value}</mark>} defaultValue="Type @ to mention someone!" options={[ { markup: '@[__value__]', overlay: { trigger: '@', data: ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'], }, }, ]} />)How it works:
The options prop configures autocomplete behavior:
markup— the pattern the choice is written as. A markup must not begin with a placeholder, so there is always a literal opener;@[__value__]turnsAliceinto@[Alice]overlay— configuration for the built-inSuggestionscomponent:trigger— character that opens the overlay (here:@)data— array of suggestions to display
Mark— the component that renders what was inserted
When you type the trigger character @:
- Markput detects the trigger and shows the built-in
Suggestionscomponent - As you type, suggestions are filtered based on your input
- When you select an item (e.g.,
'Alice'), Markput writes@[Alice]into the value and renders it throughMark
Keyboard navigation is built-in (↑↓ to navigate, Enter to select, Esc to close).
Step 3: Custom Overlay
Section titled “Step 3: Custom Overlay”The built-in Suggestions component is convenient, but sometimes you need full control over the UI. The Overlay prop lets you render a completely custom component, and the useOverlay hook provides all the state and actions you need.
Here’s a custom mention UI that fetches GitHub users and displays avatars:
import {MarkedInput, useOverlay} from '@markput/react'import {type RefObject, useEffect, useState} from 'react'
const userStyle: React.CSSProperties = {display: 'inline-flex', gap: '0.5rem'}const userClickableStyle: React.CSSProperties = {display: 'flex', padding: '0.5rem', cursor: 'pointer'}const avatarStyle: React.CSSProperties = {height: '1.5rem', width: '1.5rem', borderRadius: '9999px'}const overlayStyle: React.CSSProperties = { position: 'fixed', zIndex: 10, border: '1px solid #e5e7eb', background: 'white', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',}
const User = ({avatar, login, onClick}: {avatar?: string; login?: string; onClick?: () => void}) => ( <span style={onClick ? userClickableStyle : userStyle} onClick={onClick}> <img src={avatar} alt="" style={avatarStyle} /> {login} </span>)
const CustomOverlay = () => { const {select, match, style, ref} = useOverlay() const [users, setUsers] = useState<{login: string; avatar_url: string}[]>([])
useEffect(() => { if (!match?.value) return fetch(`https://api.github.com/search/users?q=${match.value}`) .then(res => res.json()) .then(data => setUsers(data.items?.slice(0, 10) || [])) }, [match?.value])
return ( <div ref={ref as RefObject<HTMLDivElement>} style={{...overlayStyle, top: style.top, left: style.left}}> {users?.map(user => ( <User key={user.login} avatar={user.avatar_url} login={user.login} onClick={() => select({value: user.login, meta: user.avatar_url})} /> ))} </div> )}
export const Step3Demo = () => ( <MarkedInput defaultValue="Type @ to mention someone!" Mark={({value, meta}) => <User avatar={meta} login={value} />} Overlay={CustomOverlay} />)How it works:
The useOverlay hook returns an object with everything you need to build a custom overlay:
match— current search state withmatch.valuecontaining what the user typed after the triggerselect— function to insert markup:select({value: string, meta?: string})close— function to dismiss the overlay without selectingstyle— object withtopandleftcoordinates for positioning near the cursorref— React ref to attach to your overlay element for proper event handling
The hook handles all the complexity: detecting triggers, tracking the search query, and positioning the overlay. When you call select({value, meta}), it inserts the markup and closes the overlay.
In this example, we fetch GitHub users and store the username as value and avatar URL as meta. The Mark component then uses meta to display the avatar. You can use any UI library or custom component. Markput doesn’t impose styling constraints.
Rows Come For Free
Section titled “Rows Come For Free”The editor above is already a multi-row document: separator defaults to '\n', so every line is a
row with its own drag grip and its own place in the tree. Enter opens a row, Tab nests one under
another, Esc selects one, and Ctrl/Cmd+Z undoes — the editor keeps its own history stack.
An option that declares row turns its markup into a row kind, matched only at a row’s own
start:
import {MarkedInput} from '@markput/react'
<MarkedInput defaultValue={'# Launch plan\n- ship it'} draggable options={[ {overlay: {trigger: '/'}}, {markup: '# __slot__', row: {Component: Heading}, menu: {label: 'Heading 1'}}, {markup: '- __slot__', row: {Component: Bullet, continues: true, indents: true}, menu: {label: 'Bulleted list'}}, ]}/>That is the whole wiring for a / menu too: an option that declares a menu IS in it. Read
Rows and Nesting next, then Row Kinds.
Pass separator={null} if you want the old behaviour — one document, no rows, no row controls.
Try It Live
Section titled “Try It Live”Explore these interactive examples on CodeSandbox:
- Static Marks — Basic example with clickable marks
- Dynamic Marks — Editable and removable marks with
useMark - Custom Overlay — Building your own suggestion UI