UI kit & theming
@nextgensoftwares/folio-react ships two levels of UI:
<FolioApp>, a complete editor app you drop in with a few lines;- the parts it is made of (
FolioView,FolioToolbar, menus, the bubble bar, inspector controls,SlotTabs,ZoomControl,FolioReaderBar…), for hosts that build their own layout.
Both share one design language driven by --folio-* CSS variables.
<FolioApp>
import { createFolioApp, FolioApp, FolioStyles } from '@nextgensoftwares/folio-react';
const app = createFolioApp({ doc, measurer: fonts, plugins: [calloutPlugin(), mathPlugin({ katex })] });
export function Editor() {
const [title, setTitle] = useState('Quarterly report');
return (
<>
<FolioStyles />
<FolioApp app={app} title={title} onTitleChange={setTitle} status="saved" style={{ height: '100vh' }} />
</>
);
}createFolioApp(options) composes the plugins once, wires incremental layout (a LayoutCache and the previous layout on every call) and creates the FolioEditor. It returns { editor, composed, metrics, renderMath, bodySize, layouts }; you can also skip it and pass editor, composed, metrics to <FolioApp> yourself.
createFolioApp option | |
|---|---|
doc | the document (ProseMirror-shaped JSON) |
measurer | a FontEngine (its metrics also aligns painted baselines) |
plugins | FolioPlugin[], composed in order |
schema | base schema (default standardSchema) |
layout | extra LayoutOptions (theme, math, header/footer…) |
renderMath | KaTeX (or other) renderer for math painters |
reflow | 'auto' (default: phones reflow the text to the screen, Word's mobile view), 'print' (always pages) or false (no layout switch) |
Regions
┌ app bar ─ [◧] mark Title ● Saved ············ people · appbar slot · actions · ☀ · [◨] ┐
├ toolbar (FolioToolbar) ───────────────────────────────────────────────────────────────┤
│ left panel │ page canvas (FolioView) │ right panel │
│ "navigation" │ │ "sidebar" │
│ slot as tabs │ │ tabs + Settings │
├ status bar: page · words · "statusbar" slot ······························ zoom ───────┤| Prop | What it does |
|---|---|
app | createFolioApp(...)'s result (or editor + composed + metrics + renderMath + bodySize) |
layout | shown read-only when there's no editor |
title, onTitleChange, titlePlaceholder | the app-bar title; editable when onTitleChange is given (Enter or blur commits, Escape reverts) |
status | 'saved' | 'saving' | 'unsaved' | 'offline' | 'error' or { label, tone, title } (tone: muted, busy, good, warn, bad) |
brand | logo at the start of the bar (default the Folio mark; false hides it) |
people | AppPerson[] ({ name, color?, image?, status? }, shown with <FolioAvatars>) or any node |
actions | buttons at the end of the bar; use FolioAppButton (icon) or the folio-app-cta class (primary). Toolbar items routed to the appbar surface (ui: { route: { export: ['appbar'] } }) also show here as labelled buttons, collapsed into one menu on narrow bars |
left, right | { slot?, label?, width?, defaultOpen? } or false. Defaults: navigation / 260 px and sidebar / 320 px |
settings | slot whose items become collapsible sections of a Settings tab in the right panel (default settings) |
statusbar | status-bar slot (default statusbar), false hides the bar |
dock | { title, open, onClose, children, width? }: an extra panel docked after the right one (the playground's Developer panel) |
slotProps | extra props for every slot component (they also get editor and item) |
theme, onThemeChange, themeToggle | light / dark / system; uncontrolled by default with a button in the bar |
zoom, onZoomChange, defaultZoom, onTwoPages | zoom (default 100 %, shrinking to fit narrow screens) |
toolbar | FolioToolbar props (fonts, defaultFontSize, contextBar…) or false |
fonts | families for the toolbar's font picker and the phone bar |
banner | anything under the toolbar (progress, read-only notices) |
view | every other FolioView prop (rulers, reader display, page access, pageComponent…) |
viewRef | receives the FolioViewHandle |
wrapCanvas | wraps the view (e.g. <FolioProtect>) |
canvas, viewHandle | replace the canvas entirely (two panes side by side) and tell the status bar which view to follow |
tooltips | styled tooltips for every title inside the app (default true) |
Responsive behaviour
<FolioApp> measures its own box, not the window, so an embedded app (like the examples in these docs) adapts to its container:
- panels dock while the page area keeps at least
MIN_MAIN(820 px, so print layout stays above the 720 px reflow breakpoint); past that they become drawers that slide over the page (Escape or the scrim closes them). The dock docks first, then the right panel, then the left one; - under 600 px (
data-size="phone") the status bar, brand and avatars hide, and in reflow layout the formatting toolbar becomes the phone bar above the keyboard (FolioView'smobileBar); data-size(wide,medium,narrow,phone) is on the root for your own CSS;useAppWidth,appSizeandusePanelsare exported for custom shells.
Slots
Every region is a slot, so a plugin's UI appears without host code:
| Slot | Where <FolioApp> renders it | Component props |
|---|---|---|
navigation | left panel, one tab per item | slotProps + editor, item |
sidebar | right panel, one tab per item | same |
settings | right panel's Settings tab, one collapsible section per item (open: false starts collapsed) | same |
statusbar | status bar cells, after the built-in app.page and app.words | AppStatusProps (layout, view) + slotProps |
appbar | app bar, before actions | slotProps |
toolbar, overlay, reader | as in any host (Customizing the UI) |
A panel whose slot is empty hides itself and its toggle. Built-in cells are slot items: hide one with hide: ['statusbar:app.words'] or replace it by id.
const notesPlugin: FolioPlugin = {
name: 'notes',
ui: {
slots: [
{ id: 'notes', slot: 'navigation', label: 'Notes', icon: 'bookmark', component: NotesPanel },
{ id: 'notes.count', slot: 'statusbar', label: 'Notes', order: 20, component: NoteCount },
{ id: 'notes.share', slot: 'appbar', component: ShareButton },
],
},
};Theming tokens
All styling reads CSS variables declared on .folio-ui (the app root, the toolbar, popovers and menus all carry that class). Light is the default; dark applies under [data-folio-theme=dark] (set by setFolioTheme('dark') or the theme prop), and system follows the OS.
| Token | Role |
|---|---|
--folio-bg, --folio-bg-2, --folio-chrome | surfaces: panels and popovers, subtle fills, app bar / toolbar / status bar |
--folio-canvas | the desk behind the pages |
--folio-text, --folio-muted | text, secondary text and icons |
--folio-border, --folio-border-strong | hairlines, input borders |
--folio-hover, --folio-press | hover and pressed / selected fills |
--folio-accent, --folio-accent-fg, --folio-focus | primary actions, focus rings |
--folio-on-bg, --folio-on-fg | active toggles (bold on, current option) |
--folio-danger, --folio-success, --folio-warning | status colours |
--folio-shadow-sm, --folio-pop-shadow, --folio-shadow-lg, --folio-scrim | elevation: cards, popovers and menus, drawers, the drawer scrim |
--folio-page-bg, --folio-page-shadow, --folio-page-radius | the paper |
--folio-selection, --folio-caret | selection and caret on the page |
--folio-radius-sm, --folio-radius, --folio-radius-lg | 6 / 8 / 10 px: controls, cards, popovers |
--folio-control-h | button and input height (30 px) |
--folio-text-xs … --folio-text-lg | 11 / 12 / 13 / 15 px type scale |
--folio-space-1 … --folio-space-5 | 4 / 8 / 12 / 16 / 24 px spacing |
--folio-font, --folio-font-mono, --folio-ease | UI font stacks, motion curve |
Override them on .folio-ui (every Folio element carries the class, so a selector on an ancestor alone is not enough):
.folio-ui {
--folio-accent: #0f766e;
--folio-focus: rgba(15, 118, 110, 0.4);
--folio-on-bg: #ccfbf1;
--folio-on-fg: #115e59;
--folio-font: 'IBM Plex Sans', system-ui, sans-serif;
--folio-radius-sm: 4px;
}
[data-folio-theme='dark'] .folio-ui { --folio-accent: #5eead4; --folio-accent-fg: #042f2e; }Plugins' own UI (comment cards, the import dialog, the equation editor, callout bars) reads the same tokens, so one override restyles everything.
Overriding styles
- Class hooks. Every part has a stable
folio-*class:folio-app-bar,folio-app-title,folio-app-panel(+folio-app-left/-right/-dock,[data-drawer],[data-open]),folio-app-tabs,folio-app-statusbar,folio-app-cell[data-id],folio-btn,folio-popover,folio-menu-item,folio-bubble,folio-tooltip,folio-c-*(inspector controls). PassclassNameto<FolioApp>to scope overrides. - Without the injected CSS. Import
@nextgensoftwares/folio-react/styles.cssinstead of<FolioStyles />, or ship your own stylesheet built fromfolioCss. - Replace pieces.
brand,actions,people,bannerandcanvastake any node; slots take any component;FolioAppBar,FolioAppPanel,FolioAppStatusBar,FolioAvatars,SaveIndicatorandFolioTooltipsare exported to assemble a different shell from the same parts.
Component set
One set of primitives covers every surface, so plugins look native:
| Need | Use |
|---|---|
| icon button | ToolButton (toolbars), FolioAppButton (app bar) |
| menu or picker | Dropdown + OptionList, Menu, Popover |
| settings form | Field, Section, Select, Switch, Segmented, Slider, NumberInput, CommitInput, ColorInput, Button, FileButton |
| tabs of plugin panels | SlotTabs |
| tooltip | any title inside .folio-ui (with <FolioTooltips> mounted, which <FolioApp> does) |
| toast, dialog | the folio-clip-toast and folio-popover cards (role="dialog"), as the clipboard, import and equation UIs do |
The playground is built on <FolioApp>; its engine internals live in a Developer panel passed as dock.