# Editor

A client-loaded code editor with preview, toolbar actions, and fullscreen modes.

Page: https://sui.draco.dev/docs/components/editor

### Example: editor-demo

```tsx
"use client";
import { Editor } from "@workspace/ui/components/editor";
import { useState } from "react";
import type { ExampleProps } from "../types";
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const [language, setLanguage] = useState("markdown");
  const [value, setValue] = useState(
    chinese
      ? "# 项目说明\n\n编辑这里的 **Markdown**，然后切换预览。\n\n- [x] 双语文档\n- [ ] 下一次发布"
      : "# Project notes\n\nEdit this **Markdown**, then switch to preview.\n\n- [x] Bilingual docs\n- [ ] Next release",
  );
  return (
    <Editor
      value={value}
      onChange={setValue}
      language={language}
      onLanguageChange={setLanguage}
      languages={[
        { value: "markdown", label: "Markdown" },
        { value: "html", label: "HTML" },
        { value: "typescript", label: "TypeScript" },
        { value: "tsx", label: "TSX" },
        { value: "json", label: "JSON" },
      ]}
      height={320}
      toolbarTitle={chinese ? "项目说明" : "Project notes"}
      labels={
        chinese
          ? {
              copied: "已复制",
              copyFailed: "复制失败",
              loading: "正在加载编辑器",
              loadingPreview: "正在加载预览",
              language: "语言",
              error: "无法加载编辑器",
              preview: "预览",
              hidePreview: "隐藏预览",
              split: "并排",
              copy: "复制",
              fullscreen: "全屏",
              exitFullscreen: "退出全屏",
            }
          : undefined
      }
    />
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/editor
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases.

## Usage

```tsx
import { Editor } from "@workspace/ui/components/editor";
import { useState } from "react";

export function NotesEditor() {
  const [value, setValue] = useState("# Project notes");
  return <Editor value={value} onChange={setValue} language="markdown" height={320} />;
}
```

## Editing and value state

Use `value` and `onChange` for controlled editing. For internal state, omit `value` and supply `defaultValue`; `onChange` still reports edits. `disabled` makes the editor read-only. The editor is loaded in the client, with a loading placeholder during SSR and hydration. Switching language recreates the editor bridge and releases its previous model while retaining the current document value. Language services and workers load as needed. Workers are shared between mounted editors, stop after the last editor and all models are released, and are recreated when editing resumes. Editing does not execute the supplied code.

## Language selection

Supply `language` and `onLanguageChange` to show the shared Select language picker in the toolbar. Customize the candidates with `languages`, or use the common languages provided by default; the current value stays visible even when absent from that list. The caller controls the selected language, and selection preserves the document content. Set `toolbarLanguage={false}` to hide the picker. Without a callback it is hidden, and `disabled` disables selection. Language IDs are trimmed and lowercased; `ts`, `js`, `md`, and `text` normalize to `typescript`, `javascript`, `markdown`, and `plaintext`.

The first example switches between Markdown, HTML, TypeScript, TSX, and JSON. HTML and Markdown have built-in previews; the other languages retain editing mode.

## Preview modes

Markdown and HTML have built-in previews and start in `edit` mode. Toolbar actions switch between `edit`, `preview`, and `split`. The built-in HTML preview disables scripts with `sandbox="allow-same-origin"` and uses the isolated [Html Viewer](/docs/components/html-viewer), while Markdown uses [Markdown Viewer](/docs/components/markdown-viewer).

Provide `preview.component` for another renderer. It receives `content`, `language`, `scrollContainerRef`, and `onScroll`. Use `preview.defaultMode` for the initial mode, or `preview.mode` and `preview.onModeChange` for controlled mode selection. Custom previews default to split mode.

### Scroll synchronization

In split mode, scrolling either pane synchronizes the other by relative scroll progress, rather than source-line positions. Custom previews must attach `scrollContainerRef` and `onScroll` to the actual scrolling div. The built-in Markdown preview uses its outer scrolling container. The built-in HTML preview permits same-origin DOM access while keeping scripts disabled; the parent synchronizes its document scrolling container after each frame load and removes listeners on replacement or unmount.

The example supplies a long document, a custom scrolling preview, controlled preview mode, and browser fullscreen.

### Example: editor-preview

```tsx
"use client";

import {
  Editor,
  type EditorProps,
  type EditorViewMode,
} from "@workspace/ui/components/editor";
import { MarkdownViewer } from "@workspace/ui/components/markdown-viewer";
import {
  type ComponentProps,
  createContext,
  useContext,
  useState,
} from "react";
import type { ExampleProps } from "../types";

type PreviewProps = ComponentProps<
  NonNullable<EditorProps["preview"]>["component"]
>;
const PreviewLocale = createContext(false);
const sections = Array.from({ length: 24 }, (_, index) => index + 1);
function Preview({ content, scrollContainerRef, onScroll }: PreviewProps) {
  const chinese = useContext(PreviewLocale);
  return (
    <div
      ref={scrollContainerRef}
      onScroll={onScroll}
      className="h-full overflow-auto p-4"
    >
      <MarkdownViewer
        content={content}
        labels={chinese ? { empty: "没有预览内容" } : undefined}
      />
    </div>
  );
}
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const [mode, setMode] = useState<EditorViewMode>("split");
  const [value, setValue] = useState(() =>
    sections
      .map((section) =>
        chinese
          ? `## 第 ${section} 节\n\n在编辑区和预览区分别滚动，另一区域按可滚动距离的比例跟随。\n\n这一段用于产生真实的长内容。\n`
          : `## Section ${section}\n\nScroll either pane to synchronize its relative position with the other pane.\n\nThis paragraph creates real long-form content.\n`,
      )
      .join("\n"),
  );
  return (
    <PreviewLocale.Provider value={chinese}>
      <div className="grid w-full gap-3">
        <Editor
          value={value}
          onChange={setValue}
          language="markdown"
          height={360}
          toolbarTitle={chinese ? "自定义滚动预览" : "Custom scrolling preview"}
          preview={{ component: Preview, mode, onModeChange: setMode }}
          fullscreen={{ mode: "screen" }}
          labels={
            chinese
              ? {
                  copied: "已复制",
                  copyFailed: "复制失败",
                  loading: "正在加载编辑器",
                  loadingPreview: "正在加载预览",
                  error: "无法加载编辑器",
                  preview: "预览",
                  hidePreview: "隐藏预览",
                  split: "并排",
                  copy: "复制",
                  fullscreen: "全屏",
                  exitFullscreen: "退出全屏",
                }
              : undefined
          }
        />
        <output className="text-muted-foreground text-sm" aria-live="polite">
          {chinese ? "受控模式" : "Controlled mode"}: {mode}
        </output>
      </div>
    </PreviewLocale.Provider>
  );
}
```

## Toolbar and read-only editing

`toolbarTitle` and `toolbar` accept a React node or a render function. The render function receives `EditorToolbarActionContext`, including the current value, language, theme, disabled state, Monaco instance, `format()`, `setMode()`, and `setFullscreen()`. `toolbar={false}` hides the entire toolbar.

`EditorToolbarButton` is available for custom icon actions. The example adds formatting and reset actions, and lets you enable or disable editing. Formatting depends on the formatter registered for the current language.

### Example: editor-toolbar

```tsx
"use client";
import { Button } from "@workspace/ui/components/button";
import { Editor } from "@workspace/ui/components/editor";
import { useState } from "react";
import type { ExampleProps } from "../types";

const initialValue = 'export const theme = { name: "bamboo", enabled: true };';
export default function Example({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const stateLabels = chinese
    ? {
        enable: "允许编辑",
        readOnly: "设为只读",
      }
    : {
        enable: "Enable editing",
        readOnly: "Make read-only",
      };
  const [value, setValue] = useState(initialValue);
  const [disabled, setDisabled] = useState(false);
  return (
    <div className="grid w-full gap-4">
      <Button
        variant="outline"
        className="justify-self-start"
        type="button"
        onClick={() => setDisabled((current) => !current)}
      >
        {disabled ? stateLabels.enable : stateLabels.readOnly}
      </Button>
      <Editor
        value={value}
        onChange={setValue}
        disabled={disabled}
        language="typescript"
        height={260}
        toolbarTitle="theme.ts"
        toolbarMode={false}
        fullscreen={{ mode: "fixed" }}
        toolbar={({ format, disabled }) => (
          <div className="flex gap-2">
            <Button
              type="button"
              size="sm"
              variant="ghost"
              disabled={disabled}
              onClick={format}
            >
              {chinese ? "格式化" : "Format"}
            </Button>
            <Button
              type="button"
              size="sm"
              variant="ghost"
              disabled={disabled}
              onClick={() => setValue(initialValue)}
            >
              {chinese ? "恢复示例" : "Reset example"}
            </Button>
          </div>
        )}
        labels={
          chinese
            ? {
                copied: "已复制",
                copyFailed: "复制失败",
                loading: "正在加载编辑器",
                loadingPreview: "正在加载预览",
                error: "无法加载编辑器",
                preview: "预览",
                hidePreview: "隐藏预览",
                split: "并排",
                copy: "复制",
                fullscreen: "全屏",
                exitFullscreen: "退出全屏",
              }
            : undefined
        }
      />
    </div>
  );
}
```

## Fullscreen and localization

Fullscreen defaults to a fixed viewport overlay. Use `fullscreen={{ mode: "screen" }}` for the browser Fullscreen API, or `fullscreen={false}` to hide the action. `value`, `defaultValue`, and `onChange` inside the fullscreen options support controlled or uncontrolled fullscreen state. Browser fullscreen requires API support and permission; a failed request returns to the non-fullscreen state. The fixed overlay supports Escape, contains keyboard focus, and restores focus on exit. Accessible same-origin iframe controls participate in the focus cycle in DOM order; Escape inside the built-in HTML preview also exits. Frame loading and preview-mode changes refresh keyboard bindings. A cross-origin or opaque custom iframe is treated as one focus entry: the parent cannot intercept keys inside its inaccessible document, so retain an outer exit control.

Translate the `labels` for preview, split view, copy, fullscreen, loading, and failure messages. The examples keep user content in local component state; the component does not save documents or preferences for you. Monaco theme settings are shared within a page; keep multiple Editors on a consistent theme rather than relying on isolated light and dark instances.

## API reference

| Prop | Type | Default / behavior |
| --- | --- | --- |
| `value` / `defaultValue` | `string` | Controlled / initial internal content. |
| `onChange` | `(value: string) => void` | Reports content changes. |
| `language` | `string` | `"plaintext"`. |
| `onLanguageChange` | `(language: string) => void` | Requests selection; the caller updates `language`. |
| `languages` | `readonly EditorLanguageOption[]` | Common languages; each item has `value` and `label`. |
| `toolbarLanguage` | `boolean` | `true`; shows the picker when a callback is supplied. |
| `height` | `string \| number` | Controls editing area height; numbers are pixels. |
| `disabled` | `boolean` | `false`; read-only editing. |
| `toolbar`, `toolbarTitle` | `ReactNode \| render function` | Custom actions and heading; toolbar also accepts `false`. |
| `toolbarMode`, `toolbarCopy` | `boolean` | Both `true`. |
| `preview` | Preview options | Custom renderer, mode and mode-change callback. |
| `fullscreen` | `false \| object` | Fixed overlay by default. |
| `size` | Button size | `"icon-sm"` for toolbar actions. |
| `labels` | `Partial<EditorLabels>` | English feedback and action names. |
| `glass` | `boolean` | `false`; opt-in glass surface. |

`EditorProps`, `EditorLanguageOption`, `EditorLabels`, `EditorViewMode`, `EditorFullscreenMode`, and `EditorToolbarActionContext` are exported. The editor uses Monaco; see the [Monaco API](https://microsoft.github.io/monaco-editor/docs.html). For glass setup, see [Glass](/docs/components/glass).
