A Word-document editor for the browser. It opens a .docx, lets you edit
it, and writes a .docx back — preserving pagination, tables, headers and
footers, tracked changes, and footnotes.
The OOXML parser, document model, and page-layout engine are React-free, so they run on a server or under any framework. The React editor is one layer on top.
Part of stella, an open-source legal workspace.
This is a bun workspace monorepo with two published packages:
| Package | What it is |
|---|---|
@stll/folio-core |
the headless, framework-neutral core — OOXML parsing, the document model, the ProseMirror integration, and the layout engine; no React in the import graph, so non-React adapters can build on it |
@stll/folio-react |
the React editor and its components, built on @stll/folio-core |
# the React editor (pulls in @stll/folio-core)
bun add @stll/folio-react react react-dom use-intl
# or just the headless engine
bun add @stll/folio-coreimport { DocxEditor } from "@stll/folio-react";
import "@stll/folio-react/standalone.css";
export function Editor({ docx }: { docx: ArrayBuffer }) {
return <DocxEditor documentBuffer={docx} onSave={(out) => download(out)} />;
}The editor renders to the DOM; under SSR, load it from a client-only/dynamic import.
The editor's chrome (toolbar, menus, dialogs) is authored with Tailwind utility classes and semantic design tokens. Pick the stylesheet that matches your app.
1. standalone.css — no Tailwind required (default). A single,
self-contained import: the bundled document/ProseMirror styles plus a
pre-compiled copy of every utility folio's own components use. The utilities are
scoped under .folio-root, so they cannot leak into or restyle your app, and
the design tokens ship as low-specificity fallbacks you can override.
import "@stll/folio-react/standalone.css";Override the theme by setting the tokens on .folio-root (a normal class rule
outranks the shipped :where(.folio-root) fallbacks):
.folio-root {
--background: #fdfdfc;
--foreground: #1c1c1a;
--primary: #3b5bdb;
/* ...only the tokens you want to change... */
}For dark mode, add a .dark class to an ancestor (e.g. <html>); the editor
and its body-portalled overlays theme themselves from it.
2. editor.css — you already run Tailwind. Import only the bundled document
- chrome styles and let your own Tailwind build generate the utilities. Point a
@sourceat folio's shipped code so the classes its components use are scanned, and supply the semantic tokens (--background,--foreground,--popover, …) from your design system:
/* your app's Tailwind entry */
@import "tailwindcss";
@source "../node_modules/@stll/folio-react/dist/**/*.js";import "@stll/folio-react/editor.css";Use this mode when your app's tokens and folio's should stay in lockstep. Do not
import both stylesheets: standalone.css already contains everything
editor.css does.
The editor reads its UI strings from use-intl
under the folio.* namespace, so it must be wrapped in an IntlProvider. folio
bundles its own translations for that namespace, so a consumer only merges
folio's catalog and sets the locale — the editor localizes itself:
import { IntlProvider } from "use-intl";
import { DocxEditor } from "@stll/folio-react";
import { FOLIO_LOCALES, getFolioMessages } from "@stll/folio-react/messages";
import "@stll/folio-react/editor.css";
export function Editor({ docx, locale }: { docx: ArrayBuffer; locale: string }) {
return (
<IntlProvider locale={locale} messages={getFolioMessages(locale)}>
<DocxEditor documentBuffer={docx} />
</IntlProvider>
);
}@stll/folio-react/messages exports:
getFolioMessages(locale: string): FolioMessages— the bundled{ folio: … }catalog forlocale, falling back to English for any locale folio does not ship.FOLIO_LOCALES— the bundled locales as a readonly tuple (also aFolioLocaletype and anisFolioLocaleguard).
Bundled locales: en, de, fr, es, cs, ar, et, he, hi, hu,
lt, lv, pl, pt-BR, sk, tr, zh-CN. Arabic (ar) and Hebrew
(he) are right-to-left: set dir="rtl" on a container around the editor for
those locales.
Merging with your own app messages. folio owns exactly one top-level
namespace (folio.*), so a shallow spread merges cleanly with your app's other
namespaces. Put folio first so your app wins on any intentional override:
const messages = { ...getFolioMessages(locale), ...appMessages[locale] };Do not re-declare the folio.* keys in your own catalog: let folio's bundled
catalog be the source of truth for that namespace (otherwise a stale app copy
would win the merge). The playground (packages/playground) wires this up with a
language switcher you can use to preview every bundled locale.
bun install
bun run build # builds both packages (core first)
bun run typecheck
bun run test # unit suite for both packages
bun run lint
bun run validate-dist # clean-room publish-shape validation for both packagesReleases are driven by Changesets. The two packages are versioned independently.
Every PR that edits packages/core/src or packages/react/src must add a
changeset. CI enforces this (bun run changeset:check):
bunx changeset # pick packages + bump level, write a summaryFor a source change that genuinely needs no release (comments, internal-only refactor), record that explicitly instead:
bunx changeset --emptyCommit the generated .changeset/*.md file with your PR.
How a version reaches npm:
- PRs merge to
main, each carrying its changeset(s). release-pr.ymlmaintains a "Version Packages" PR that applies the pending changesets — bumping the affectedpackage.jsonversions, updating changelogs, and re-syncingbun.lock.- Merging that PR lands the version bumps on
main, which tripspublish.yml'spackages/{core,react}/package.jsonpath filter and runs the hardened OIDC publish + GitHub Release for the bumped package(s).
Changesets never publishes; publish.yml is the sole publish mechanism.
folio began as a private fork of Eigenpal's
docx-editor by
Jedr Blaszyk. The code has since been extended
(mostly to match the needs of stella). After the upstream
repository was taken down, we are publishing the folio fork as an independently maintained
continuation. The original license and copyright are preserved in
NOTICE.md.
