Install
Paste the snippet below and you are done. It runs from a single 82KB file with no dependencies.
<div id="typdit-editor"></div>
<script src="https://typdit.com/embed/v1.js"></script>
<script>
const editor = Typdit.create(document.getElementById('typdit-editor'), {
placeholder: 'Write here',
licenseKey: 'YOUR_KEY',
onChange: (snapshot) => console.log(snapshot),
});
</script>Take it straight from the editor page
You do not have to write the install code by hand. Set the mode and theme the way you want on the editor page, scroll down, and the install code with exactly those settings is waiting there. Options that match the defaults are left out, so what is special about your setup stands out.
Copy with document also carries what is on screen as the snapshot option. Use it when you opened a template, liked the skeleton, and want to take the skeleton with you.
All options
Everything goes in the second argument of Typdit.create(element, options). All fields are optional; defaults are noted below.
| Option | What it does |
|---|---|
snapshot | Initial document (JSON snapshot, see the format section). Omit to start empty |
theme: 'light' | 'dark' | Editor theme (default light) |
mode: 'notion' | Notion-style preset: hides the toolbar and turns on block handles (see below). Default classic |
handles: true | Toggle block handles on their own (overrides the mode preset) |
writingMode: 'paper' | Writing mode. One of plain, focus, book, paper, essay, script, techdoc, or a preset object. It switches setting, counts, outline, footnotes and screenplay flow together |
writingMode: { id, extends } | Override a few fields on a builtin to make your own mode. Leave out extends and you start from a bare base |
writingLabels | Replace the mode UI strings (metric names, outline, footnotes, goal). Partial is fine; the rest fall back to English |
writingSlashLabels | Strings for the mode specific slash entries (footnote, screenplay blocks) |
handleLabels | Replace block handle labels (partial objects allowed) |
toolbar: false | Hide the top toolbar (shown by default) |
menus: false | Disable the slash command and selection bubble menus (on by default) |
markdown: false | Disable markdown input shortcuts like # headings and - lists (on by default) |
typography: false | Disable smart quotes and ellipsis substitution (on by default) |
placeholder | Hint text shown in an empty document |
autofocus: true | Focus the editor right after creation (off by default) |
toolbarLabels | Replace toolbar labels (partial objects allowed, see below) |
licenseKey | Plus license key. Removes the watermark after domain verification |
onChange(snapshot) | Called with the latest snapshot on every content change |
onRequestImage() | Image uploader hook. Resolve {src, alt?} to insert, null to cancel. Without it the toolbar falls back to URL input |
onRequestFile() | File attachment hook returning {src, name?, size?}. The file button renders only when this hook is provided |
onRequestVideo() | Video upload hook returning {src}. Adds an upload button next to the video link input (link input always works) |
Notion-style mode
mode: 'notion' hides the toolbar and turns on block handles. Hover a block and an add (+) and a menu (⋮⋮) button appear on its left; the menu moves the block up or down, duplicates, deletes, or turns it into another block type. Formatting stays available through slash commands and the selection bubble menu.
Without a toolbar, the image, video and file items also drop out of the slash menu (their input UI belongs to the toolbar). If you need both handles and attachments, pass toolbar: true alongside; handles and the toolbar coexist.
Typdit.create(el, {
mode: 'notion',
handleLabels: { moveUp: '위로 이동', moveDown: '아래로 이동', delete: '삭제' },
});Instance methods
The handle returned by Typdit.create() reads and writes the document. Storage is entirely up to the host site.
| Method | What it does |
|---|---|
getSnapshot() | Returns the current document as a JSON snapshot |
setSnapshot(json) | Replaces the whole document with a snapshot (load) |
getText() | Returns plain text without formatting (for search indexing or counting) |
focus() | Moves focus into the editor |
destroy() | Removes the editor and detaches listeners. Call on unmount in an SPA |
version | SDK version string (currently 1.0.0) |
The snapshot format
A snapshot is flat JSON you can store and hand back as is. Each line of text maps 1:1 to an entry in blocks, and marks are offset ranges into text. Unknown block or mark types are silently skipped on load, so a snapshot written by a newer version still opens in an older one.
{
"version": 1,
"text": "제목\n첫 문단입니다.",
"blocks": [
{ "type": "heading", "attrs": { "level": 1 } },
{ "type": "paragraph" }
],
"marks": [
{ "type": "bold", "from": 3, "to": 7 }
]
}Wiring up autosave
onChange fires on every keystroke. The usual pattern is to debounce and send to your server once typing pauses.
let timer;
const editor = Typdit.create(el, {
onChange: (snapshot) => {
clearTimeout(timer);
timer = setTimeout(() => {
fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(snapshot),
});
}, 800);
},
});Hooking up image and file uploads
The editor never uploads files itself. Your hook handles picking and uploading, returns a URL, and the editor does the inserting and rendering. Without hooks, images still work via URL input, and the file button appears only when onRequestFile is provided.
Typdit.create(el, {
onRequestImage: async () => {
const file = await pickFile('image/*'); // 파일 선택 UI는 호스트 구현
if (!file) return null; // null = 취소
const src = await uploadToMyServer(file); // 업로드도 호스트 구현
return { src, alt: file.name };
},
onRequestFile: async () => {
const file = await pickFile('*/*');
if (!file) return null;
const src = await uploadToMyServer(file);
return { src, name: file.name, size: file.size };
},
onRequestVideo: async () => {
const file = await pickFile('video/*');
return file ? { src: await uploadToMyServer(file) } : null;
},
});Changing toolbar labels
Default labels are English. Pass only the keys you want to change in toolbarLabels; the rest keep their defaults. Full key list: text, quote, code, divider, table, image, video, file, upload, font, fontDefault, color, highlight, none, alignLeft, alignCenter, alignRight, imageUrl, videoUrl, link, linkApply, linkRemove, undo, redo.
Typdit.create(el, {
toolbarLabels: {
text: '본문', quote: '인용', code: '코드', divider: '구분선',
image: '이미지', video: '영상', link: '링크',
undo: '되돌리기', redo: '다시 실행',
},
});Using it with React
Grab the container with a ref, create on mount, and call destroy() on unmount.
import { useEffect, useRef } from 'react';
function TypditEditor({ onChange }) {
const ref = useRef(null);
useEffect(() => {
const editor = window.Typdit.create(ref.current, { onChange });
return () => editor.destroy();
}, []);
return <div ref={ref} />;
}Using it with Vue
Create in onMounted and call destroy() in onBeforeUnmount.
<template><div ref="host"></div></template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue';
const host = ref(null);
let editor;
onMounted(() => { editor = window.Typdit.create(host.value, {}); });
onBeforeUnmount(() => editor?.destroy());
</script>Using it with SSR frameworks (Next.js etc.)
The script runs only in the browser. There is no window during server rendering, so load the script and create the editor inside a client-only component.
'use client';
import { useEffect, useRef } from 'react';
export default function Editor() {
const ref = useRef(null);
useEffect(() => {
let editor;
const s = document.createElement('script');
s.src = 'https://typdit.com/embed/v1.js';
s.onload = () => { editor = window.Typdit.create(ref.current, {}); };
document.head.appendChild(s);
return () => editor?.destroy();
}, []);
return <div ref={ref} />;
}Multiple editors on one page
Every Typdit.create() call makes an independent instance, so a page can hold as many as you need. Styles are injected once as a single style tag on first creation, and each instance is removed individually via its own destroy().
License and watermark
- Without a key the editor is fully functional, with a small watermark at the bottom.
- Plus subscribers issue per-domain keys from the Embed card on the editor page. A key is valid on its registered domains and their subdomains (writing *.example.com means the same thing).
- A key expires with the subscription term it was issued under. After extending your plan, issue a new key.
- A failed check or blocked network never disables the editor. The watermark just stays; no error is thrown.
Network and CSP
- Document content is never transmitted anywhere. The editor makes no network requests for content; storage is entirely the host site's.
- The only request is a single license check when licenseKey is set (read-only, to firestore.googleapis.com).
- Sites with a CSP allow typdit.com in script-src, and firestore.googleapis.com in connect-src if a key is used. Images and videos placed in the document are separate: their origins must be open in img-src and frame-src (YouTube embeds use youtube-nocookie.com).
- Styles arrive as one style tag scoped by td- prefixed classes, so they do not mix with host CSS.
Versioning
/embed/v1.js is a major-version-pinned URL. Backward compatibility holds within the v1 URL; breaking changes ship as a new URL (v2). Check the loaded version at runtime with Typdit.version.