Article navigation plugin
This plugin adds two entries to the ONLYOFFICE Apps sidebar (the article) using the ArticleNavigation scope. Clicking an entry opens a full portal page — not a dialog — and ONLYOFFICE Apps renders the entry's component on that page, inside its own React tree:
| Sidebar entry | Appears in | Visible to | Page shows |
|---|---|---|---|
| Sample files | Files section | all user types | signed-in user, a toast button |
| Sample settings | Settings section | owner, full admin | a toggle |
Because a navigation item's page renders inside the portal's own React tree, it can be a plain component built with @onlyoffice/apps-ui-kit — it gets the portal theme for free, and the React hooks reach the portal through context.
Import only from @onlyoffice/apps-ui-kit; do not use subpath imports. The portal only substitutes the exact package specifier @onlyoffice/apps-ui-kit. A plugin that imports @onlyoffice/apps-ui-kit/components/button will fail to load.
// ✅ Correct
import { Button } from "@onlyoffice/apps-ui-kit";
// ❌ Wrong — the portal cannot load this
import { Button } from "@onlyoffice/apps-ui-kit/components/button";
Full example
import { useState, type ReactNode } from "react";
import {
Button,
ButtonSize,
Heading,
HeadingLevel,
HeadingSize,
Text,
ToggleButton,
} from "@onlyoffice/apps-ui-kit";
import {
useCurrentUser,
usePluginActions,
} from "@onlyoffice/docspace-plugin-sdk/react";
import {
type IArticleNavigationItem,
type IArticleNavigationPlugin,
type IPlugin,
PluginStatus,
Section,
ToastType,
UserRole,
} from "@onlyoffice/docspace-plugin-sdk";
const Page = ({
id,
title,
description,
children,
}: {
id: string;
title: string;
description: string;
children: ReactNode;
}) => (
<div id={id} style={{ padding: "20px", maxWidth: "640px" }}>
<Heading level={HeadingLevel.h1} size={HeadingSize.medium}>
{title}
</Heading>
<Text fontSize="13px" lineHeight="20px" style={{ margin: "4px 0 24px" }}>
{description}
</Text>
{children}
</div>
);
const FilesPage = () => {
const user = useCurrentUser();
const { showToast } = usePluginActions();
return (
<Page
id="article-navigation-sample-files-page"
title="Sample files page"
description="Opened from the sidebar entry that the plugin adds to the Files section."
>
<Text fontSize="13px" lineHeight="20px" style={{ marginBottom: "16px" }}>
Signed in as <b>{user?.displayName ?? "…"}</b>.
</Text>
<Button
id="article-navigation-sample-toast-btn"
label="Show a toast"
size={ButtonSize.normal}
primary
onClick={() =>
showToast({
type: ToastType.success,
title: `Hello, ${user?.displayName ?? "there"}!`,
})
}
/>
</Page>
);
};
const SettingsPage = () => {
const { showToast } = usePluginActions();
const [enabled, setEnabled] = useState(false);
const toggle = () => {
setEnabled(!enabled);
showToast({
type: ToastType.info,
title: enabled ? "Sample feature off" : "Sample feature on",
});
};
return (
<Page
id="article-navigation-sample-settings-page"
title="Sample settings page"
description="Only portal administrators see the sidebar entry that opens this page."
>
<ToggleButton
label="Enable the sample feature"
isChecked={enabled}
onChange={toggle}
/>
</Page>
);
};
const filesItem: IArticleNavigationItem = {
key: "article-navigation-sample-files",
label: "Sample files",
icon: "plugin-icon.svg",
component: FilesPage,
appears: [Section.Files],
};
const settingsItem: IArticleNavigationItem = {
key: "article-navigation-sample-settings",
label: "Sample settings",
icon: "plugin-icon.svg",
component: SettingsPage,
appears: [Section.Settings],
usersTypes: [UserRole.owner, UserRole.fullAdmin],
};
class ArticleNavigationSample implements IPlugin, IArticleNavigationPlugin {
status: PluginStatus = PluginStatus.active;
onLoadCallback = async (): Promise<void> => {
this.addArticleNavigationItem(filesItem);
this.addArticleNavigationItem(settingsItem);
};
updateStatus = (status: PluginStatus): void => {
this.status = status;
};
getStatus = (): PluginStatus => {
return this.status;
};
setOnLoadCallback = (callback: () => Promise<void>): void => {
this.onLoadCallback = callback;
};
articleNavigationItems: Map<string, IArticleNavigationItem> = new Map();
addArticleNavigationItem = (item: IArticleNavigationItem): void => {
this.articleNavigationItems.set(item.key, item);
};
getArticleNavigationItems = (): Map<string, IArticleNavigationItem> => {
return this.articleNavigationItems;
};
updateArticleNavigationItem = (item: IArticleNavigationItem): void => {
this.articleNavigationItems.set(item.key, item);
};
}
const plugin = new ArticleNavigationSample();
export default plugin;
Before you start
Make sure you have an ONLYOFFICE Apps server running — version 4.0.0 or later, since SDK 3.0.0 plugins require minDocSpaceVersion: "4.0.0" — and install ONLYOFFICE Apps Plugins SDK globally:
npm i -g @onlyoffice/docspace-plugin-sdk
Step 1: Create a plugin
-
Initialize your plugin using the CLI:
npx create-docspace-plugin -
Fill out basic metadata: plugin name, version, author, description, logo, license, homepage.
-
Select the required scope from the list of available options. Use the arrow keys to highlight
Article navigation, pressSpaceto select it, then pressEnterto confirm and generate the plugin template.
Step 2: Confirm plugin configuration
Ensure package.json includes all the necessary fields. Most importantly, make sure it contains:
{
"scopes": ["ArticleNavigation"]
}
Step 3: Add the UI kit dependency
Install the @onlyoffice/apps-ui-kit package from npm:
npm install @onlyoffice/apps-ui-kit
This adds it to dependencies in package.json:
{
"dependencies": {
"@onlyoffice/apps-ui-kit": "^4.0.0"
}
}
The UI kit is only needed for type checking and editor support: it is not bundled into plugin.js. ONLYOFFICE Apps supplies its own copy at load time and rewrites the @onlyoffice/apps-ui-kit specifier to it. The template's vite.config.ts already marks it as external with the pattern /^@onlyoffice\/apps-ui-kit(\/.*)?$/. Import components only from the package root, as noted at the top of this page.
Step 4: Review and extend plugin code
By default, the plugin template includes a basic implementation in the src/index.ts file. To use JSX:
-
Rename
src/index.tstosrc/index.tsx:mv src/index.ts src/index.tsx -
Update the entry point in
vite.config.ts:import { defineConfig } from "vite";import react from "@vitejs/plugin-react";import { fileURLToPath } from "url";const __dirname = fileURLToPath(new URL(".", import.meta.url));export default defineConfig({plugins: [react()],define: {"process.env.NODE_ENV": JSON.stringify("production"),"process.env": "{}",},build: {lib: {entry: `${__dirname}src/index.tsx`, // Changed from index.tsformats: ["es"],fileName: () => "plugin.js",},rolldownOptions: {external: ["react","react-dom","react/jsx-runtime","@onlyoffice/docspace-plugin-sdk/react",/^@onlyoffice\/apps-ui-kit(\/.*)?$/,],output: {codeSplitting: false,assetFileNames: (assetInfo: { name?: string }) =>assetInfo.name?.endsWith(".css")? "plugin.css": (assetInfo.name ?? "asset"),},},},}); -
Implement
IPluginandIArticleNavigationPlugininsrc/index.tsx:
ArticleNavigationSample class
import {
type IArticleNavigationItem,
type IArticleNavigationPlugin,
type IPlugin,
PluginStatus,
} from "@onlyoffice/docspace-plugin-sdk";
class ArticleNavigationSample implements IPlugin, IArticleNavigationPlugin {
status: PluginStatus = PluginStatus.active;
onLoadCallback = async (): Promise<void> => {
this.addArticleNavigationItem(filesItem);
this.addArticleNavigationItem(settingsItem);
};
updateStatus = (status: PluginStatus): void => {
this.status = status;
};
getStatus = (): PluginStatus => {
return this.status;
};
setOnLoadCallback = (callback: () => Promise<void>): void => {
this.onLoadCallback = callback;
};
articleNavigationItems: Map<string, IArticleNavigationItem> = new Map();
addArticleNavigationItem = (item: IArticleNavigationItem): void => {
this.articleNavigationItems.set(item.key, item);
};
getArticleNavigationItems = (): Map<string, IArticleNavigationItem> => {
return this.articleNavigationItems;
};
updateArticleNavigationItem = (item: IArticleNavigationItem): void => {
this.articleNavigationItems.set(item.key, item);
};
}
const plugin = new ArticleNavigationSample();
// Add the pages and navigation items below the plugin initialization line
export default plugin;
The items are registered in onLoadCallback rather than right after construction, ensuring ONLYOFFICE Apps is ready to receive them.
Step 5: Build the pages with the UI kit
Each page is an ordinary React component. useCurrentUser gives the signed-in profile, and usePluginActions gives the portal UI actions — here, showing a toast that the portal draws itself:
Pages
import { useState, type ReactNode } from "react";
import { Button, ButtonSize, Heading, HeadingLevel, HeadingSize, Text, ToggleButton } from "@onlyoffice/apps-ui-kit";
import { useCurrentUser, usePluginActions } from "@onlyoffice/docspace-plugin-sdk/react";
import { ToastType } from "@onlyoffice/docspace-plugin-sdk";
const Page = ({ id, title, description, children }: { id: string; title: string; description: string; children: ReactNode }) => (
<div id={id} style={{ padding: "20px", maxWidth: "640px" }}>
<Heading level={HeadingLevel.h1} size={HeadingSize.medium}>{title}</Heading>
<Text fontSize="13px" lineHeight="20px" style={{ margin: "4px 0 24px" }}>{description}</Text>
{children}
</div>
);
const FilesPage = () => {
const user = useCurrentUser();
const { showToast } = usePluginActions();
return (
<Page id="article-navigation-sample-files-page" title="Sample files page" description="Opened from the sidebar entry that the plugin adds to the Files section.">
<Text fontSize="13px" lineHeight="20px" style={{ marginBottom: "16px" }}>
Signed in as <b>{user?.displayName ?? "…"}</b>.
</Text>
<Button
id="article-navigation-sample-toast-btn"
label="Show a toast"
size={ButtonSize.normal}
primary
onClick={() => showToast({ type: ToastType.success, title: `Hello, ${user?.displayName ?? "there"}!` })}
/>
</Page>
);
};
const SettingsPage = () => {
const { showToast } = usePluginActions();
const [enabled, setEnabled] = useState(false);
const toggle = () => {
setEnabled(!enabled);
showToast({ type: ToastType.info, title: enabled ? "Sample feature off" : "Sample feature on" });
};
return (
<Page id="article-navigation-sample-settings-page" title="Sample settings page" description="Only portal administrators see the sidebar entry that opens this page.">
<ToggleButton label="Enable the sample feature" isChecked={enabled} onChange={toggle} />
</Page>
);
};
useCurrentFile returns null on article navigation pages (surfaces with no file selection), but useCurrentUser and usePluginRuntime resolve normally here.
Step 6: Register the navigation items
Register an IArticleNavigationItem for each page. key becomes the last segment of the page URL, so it must be unique across every installed plugin. icon is an SVG file from the plugin's assets folder, drawn at 16x16. appears places the entry in a portal section, and usersTypes narrows it to a set of roles:
Navigation items
import { Section, UserRole, type IArticleNavigationItem } from "@onlyoffice/docspace-plugin-sdk";
const filesItem: IArticleNavigationItem = {
key: "article-navigation-sample-files",
label: "Sample files",
icon: "plugin-icon.svg",
component: FilesPage,
appears: [Section.Files],
};
const settingsItem: IArticleNavigationItem = {
key: "article-navigation-sample-settings",
label: "Sample settings",
icon: "plugin-icon.svg",
component: SettingsPage,
appears: [Section.Settings],
usersTypes: [UserRole.owner, UserRole.fullAdmin],
};
Step 7: Build the plugin
From the root of your plugin, run the following command:
npm run build
This runs vite build && npx build-docspace-plugin, which bundles src/index.tsx into dist/plugin.js, packs the assets folder contents, and produces dist/plugin.zip.
Step 8: Upload to ONLYOFFICE Apps
- Log in as an administrator.
- Navigate to: Settings → Integration → Plugins.
- Click Upload plugin, and select the generated
dist/plugin.zip. - Enable the plugin toggle if it is not already active.
Step 9: Test the plugin
- Go to the Files section. The sidebar shows a Sample files entry for every user type.
- Click it — the page shows your display name and a Show a toast button. Click the button and check that a success toast appears.
- As a portal owner or full admin, go to the Settings section. The sidebar shows a Sample settings entry (other user types do not see it).
- Click it, then toggle Enable the sample feature and check that an info toast appears.