Sqwig documentation
Sqwig is a drop-in rich text box for web apps — a Word-tier formatting toolbar, clean paste, and dictionary spellcheck that runs entirely on the device. Text never leaves the browser: the engine, its Web Worker, and the dictionary are static files served from your origin, and the component makes zero calls to Sqwig servers.
Installation & assets
# core editor (framework-agnostic)
npm i @sqwig/core
# optional: the React wrapper
npm i @sqwig/react
The one rule every stack shares
Sqwig ships three runtime assets in @sqwig/core/dist: the WASM engine (spell_wasm_bg.wasm), its Web Worker (spell.worker.js), and the dictionary chunks (dict/). They must be served files at runtime — a bundler cannot inline them. So every integration is the same two steps:
- Copy those files into a folder your app serves statically (e.g.
/sqwig/). - Point the spellchecker at it:
createSpellChecker({ assetBaseUrl: "/sqwig/" }).
dist/ folder, the module and its assets stay co-located and you can omit assetBaseUrl entirely — assets resolve beside the module by default.Quickstart
Any ES-module page — no bundler required. This produces a working, spellchecking editor:
<div id="editor"></div>
<script type="module">
import { createEditor, createSpellChecker } from "/sqwig/index.js";
const editor = createEditor(document.getElementById("editor"), {
placeholder: "Say something…",
spell: createSpellChecker(), // assets resolve beside index.js
trustIndicator: { wordCount: true },
});
editor.on("change", () => console.log(editor.getHTML()));
</script>
Here /sqwig/ is a wholesale copy of node_modules/@sqwig/core/dist/. Using React, Angular, or .NET? See Frameworks.
Spellcheck & suggestions
On-device checking, live vs. manual modes, and the suggestion popover.
Sqwig checks spelling with a WebAssembly engine that runs in a Web Worker, so building the ~380K-term index never blocks your page. Misspelled words get the red wavy underline; clicking or tapping one opens a popover with up to three case-matched suggestions plus Ignore (this instance) and Add to dictionary (document-wide).
Live vs. manual
Checking is live as the user types by default. Pass mode: "manual" to sweep only when you call editor.checkNow().
Observing state
The "spell-state" event reports "checking-on-device" or "plain-input" (the degraded mode when the dictionary could not load).
Paste from Word / Google Docs
What survives a paste, what converts, and how tables and images are handled.
- Survives: bold, italic, lists, links, paragraphs, headings.
- Converts: H4–H6 → H3; fonts/colors → clean default; vendor markup stripped.
- Tables: render as clean tables with editable cell text (no table authoring in v1 — structure commands are disabled inside cells, and pastes into a cell insert as text).
- Images: dropped with an [image] placeholder in v1.
Ctrl/Cmd+Shift+V pastes as plain text. Output is always the clean subset in the output contract — never garbage.
Mobile
iOS Safari and Android Chrome are first-class, release-blocking targets — verified on real devices.
- Native autocorrect interplay is handled: the OS keyboard neither substitutes words under Sqwig's squiggles nor paints a second underline.
- On touch, the toolbar becomes a single horizontally scrollable row (like Word and Docs on a phone) with ≥40px tap targets.
- Popovers measure the visual viewport, so the on-screen keyboard never hides a suggestion — they clamp to the screen and flip above the word when space below runs out.
No configuration required for any of this.
Styling & theming
Every visual knob is a CSS custom property on .sqwig-editor.
.sqwig-editor {
--sqwig-accent: #7c3aed; /* brand color: focus, active states, links */
--sqwig-radius: 10px;
--sqwig-font: "Inter", sans-serif;
}
| Variable | Role |
|---|---|
| --sqwig-accent | Focus border, active controls, links (--sqwig-focus derives from it). |
| --sqwig-border / --sqwig-radius | Component and control borders. |
| --sqwig-font / --sqwig-text / --sqwig-bg / --sqwig-muted | Typography and surfaces. |
| --sqwig-toolbar-bg / --sqwig-toolbar-fg | Footer ground; toolbar icon/label ink. |
| --sqwig-hover-bg / --sqwig-active-bg | Control hover pill and pressed tint. |
| --sqwig-focus-ring | The soft halo while the editor has focus. |
| --sqwig-squiggle | The red. Change at your own risk — it's the brand. |
| --sqwig-warn | maxLength counter's warning color. |
Dark mode
Pass theme: "dark", or stamp data-sqwig-theme="dark" on any ancestor (e.g. <html>) when your app toggles at runtime. The preset re-pins every variable and sets color-scheme: dark; your individual overrides still win.
Privacy
What the component sends to Sqwig servers: nothing.
User text never leaves the browser — and the component makes no network calls to Sqwig at all. Your license key is a signed token verified offline, on the device; the only fetches are your own origin's static assets (engine, worker, dictionary). You can verify this in your browser's network tab. See the full privacy policy.
React
@sqwig/react exports <SqwigEditor>: a thin, uncontrolled wrapper. Every core option is a prop (read at mount); onChange / onSelectionChange / onSpellState forward the core events; the ref exposes the full core Editor. StrictMode-safe.
import { useMemo, useRef } from "react";
import { SqwigEditor } from "@sqwig/react";
import { createSpellChecker, type Editor } from "@sqwig/core";
export function Compose() {
const editor = useRef<Editor>(null);
// Create ONCE — not inline in JSX (that would spawn a Worker per render).
const spell = useMemo(() => createSpellChecker({ assetBaseUrl: "/sqwig/" }), []);
return (
<SqwigEditor ref={editor} spell={spell}
placeholder="Say something…"
onChange={(html) => save(html)} />
);
}
With Vite, copy the three assets into public/sqwig/. Vue, Svelte, and web-component wrappers are planned fast-follows on the same core.
Angular
Wrap createEditor in a standalone component: create in ngAfterViewInit, destroy in ngOnDestroy. Publish the three assets to /sqwig/ (copy into public/, or per-glob from node_modules via angular.json).
ngAfterViewInit(): void {
this.editor = createEditor(this.host.nativeElement, {
placeholder: "Say something…",
spell: createSpellChecker({ assetBaseUrl: "/sqwig/" }),
});
this.editor.on("change", () => this.changed.emit(this.editor!.getHTML()));
}
ngOnDestroy(): void { this.editor?.destroy(); }
ASP.NET Core (Razor Pages / MVC)
No bundler needed — @sqwig/core ships browser-ready ES modules. Copy node_modules/@sqwig/core/dist/ → wwwroot/sqwig/ and import directly; co-location means no assetBaseUrl.
dist/ plus license and third-party notices) — and unzip its dist/ to wwwroot/sqwig/.<div id="editor"></div>
<input type="hidden" name="Body" id="body-field" />
<script type="module">
import { createEditor, createSpellChecker } from "/sqwig/index.js";
const editor = createEditor(document.getElementById("editor"), { spell: createSpellChecker() });
editor.on("change", () => { document.getElementById("body-field").value = editor.getHTML(); });
</script>
Kestrel serves .wasm with the right MIME type out of the box. Hosting under IIS, add <mimeMap fileExtension=".wasm" mimeType="application/wasm" /> to web.config. Treat the posted HTML like any rich text input server-side (it's the whitelisted subset, but defense-in-depth is yours).
Blazor (Server or WebAssembly)
Drive Sqwig through a small JS interop shim — a module in wwwroot exposing mount / getHTML / setHTML / destroy over a Map of editor ids — loaded with IJSRuntime.InvokeAsync<IJSObjectReference>("import", "/sqwig-interop.js") in OnAfterRenderAsync, disposed via IAsyncDisposable. Assets go in wwwroot/sqwig/ exactly as above.
The complete shim and component are in the developer guide that ships with the repo.
createEditor options
createEditor(host, options) → Editor. All options are optional.
| Option | Type / default | What it does |
|---|---|---|
| placeholder | string | Ghost text when empty; also the region's accessible label. |
| initialHTML | string | Seeds the document (sanitized through the whitelist on the way in). |
| toolbar | boolean = true | The batteries-included formatting toolbar. |
| trustIndicator | boolean | { text?, wordCount? } = true | Footer with the privacy note; wordCount: true adds live counts. |
| theme | "light" | "dark" = "light" | "dark" applies the built-in dark preset (see theming). |
| spell | SpellCheckerLike | Promise | null | From createSpellChecker(...); null disables spellcheck. A failed load degrades to plain input. |
| mode | "live" | "manual" = "live" | Live squiggles, or sweep on demand via checkNow(). |
| readOnly | boolean = false | Start read-only (toggle later with setReadOnly). |
| maxLength | number | Hard character cap: typing blocked past it, pastes truncated, counter warns as it fills. |
| license | string | Your signed key. Omit = Free tier (badge shown). Verified offline; invalid keys fall back to Free — never throw, never phone home. |
Editor methods & events
| Member | Notes |
|---|---|
| getHTML() / setHTML(html) / getText() | Output is always the whitelisted subset. |
| exec(command, arg?) / queryState(command) | Programmatic formatting — the same commands the toolbar uses ("bold"; "heading", "h2"; "link", url; …). |
| on(event, handler) → unsubscribe | Events: "change", "selectionchange", "spell-state" (payload "checking-on-device" | "plain-input"). |
| checkNow() | Manual-mode sweep (no-op in live mode). |
| focus() | Focus the editing region. |
| setReadOnly(bool) / isReadOnly() | Runtime read-only toggle: region uneditable, toolbar disabled, popovers suppressed. |
| destroy() | Unmount and remove all listeners. |
| region | The contenteditable element (advanced integrations). |
createSpellChecker options
| Option | Default | Notes |
|---|---|---|
| assetBaseUrl | module-relative | Directory your copied assets are served from. |
| workerUrl | ./spell.worker.js | Explicit worker URL; overrides assetBaseUrl. |
| wasm | ./spell_wasm_bg.wasm | Explicit engine source. |
| coreDict / tailDict | ./dict/en_US.core.txt / .tail.txt | Dictionary chunks; tailDict: null skips the tail (smaller download, slightly lower recall). |
Output contract
getHTML() emits only: p, h1–h3, strong, em, u, s, sub, sup, span[style: font-family/font-size], ul, ol, li, a[href], br, table, tr, td — with block text-align/line-height/margin-left, list list-style-type, and scheme-checked hrefs. Every attribute is re-validated at the exit boundary, so the output is safe to store and re-render as-is.
License keys
Without a key Sqwig runs in the Free tier and shows a small "Powered by Sqwig" badge in the footer. Paid keys remove it:
createEditor(host, { license: "sqwig-lk1.…" });
Keys are Ed25519-signed tokens verified offline inside the bundle — domain-locked, version-windowed in your favor (versions released during an active subscription keep working forever), and fail-open: any verification problem means Free tier plus a single console note, never a broken editor and never a network call. No per-developer fees, no usage metering. Pricing: sqwig.com.
Content-Security-Policy
Everything is same-origin static files, so a typical strict CSP needs only script-src 'self' 'wasm-unsafe-eval' (WebAssembly compilation) and worker-src 'self'. Sqwig makes no third-party requests of any kind.
Changelog
0.1.0 — pre-release
- Word-tier toolbar (always-active), live squiggles with suggestion popover, clean Word/Google-Docs paste, autoformat, autolink, read-only mode, maxLength.
- Spell engine in a Web Worker (no main-thread jank); split dictionary; degrades to plain input if assets fail.
- Mobile: iOS Safari + Android Chrome verified on real devices; scrollable touch toolbar; keyboard-aware popovers.
- Light + dark themes; full CSS-variable theming; offline license keys with the Free-tier badge.
@sqwig/reactwrapper.