Customization

Dialogs

Customize PPTX editor dialogs and popups with named parts, application renderers, draft state, validation, and CSS tokens.

Register your component in the popups map on PptxEditor.Root. Your wrapper keeps the packaged draft, validation, and focus handling. For every key and field, see the dialog reference.

Register a custom dialog

This wrapper replaces the appearance field of page setup and its OK button. Forward the supplied props to keep the session.

import { useState } from 'react';
import { PptxEditor, usePageSetupDialog, type PptxEditorPopups } from '@pptx-editor.dev/react';

function AppearanceSelect() {
  const { values, setValue } = usePageSetupDialog();
  return (
    <label>
      Appearance
      <select
        value={values.theme}
        onChange={(event) => setValue('theme', event.target.value === 'dark' ? 'dark' : 'light')}
      >
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    </label>
  );
}

export function DeckEditor({ bytes }: { bytes: Uint8Array }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const popups: PptxEditorPopups = {
    pageSetup: (props) => (
      <PptxEditor.PageSetupDialog {...props} theme={theme} onThemeChange={setTheme}>
        <PptxEditor.PageSetupDialog.Field name="theme">
          <AppearanceSelect />
        </PptxEditor.PageSetupDialog.Field>
        <PptxEditor.PageSetupDialog.Apply asChild>
          <button type="button" className="acme-primary">
            Save
          </button>
        </PptxEditor.PageSetupDialog.Apply>
      </PptxEditor.PageSetupDialog>
    ),
  };
  return (
    <PptxEditor.Root document={bytes} popups={popups} colorMode={theme}>
      <PptxEditor.Viewport>
        <PptxEditor.Content />
      </PptxEditor.Viewport>
    </PptxEditor.Root>
  );
}

Content inside a Field replaces the field's content. A draft hook called inside the dialog reads and writes the draft. asChild on Apply keeps OK's wiring on your own button: validation, committing the draft, and closing. Page setup reports the appearance through onThemeChange, and you apply it through colorMode.

To register an ordinary component, wrap it in definePopup(). Define the component outside the parent render function, so it keeps its state across renders.

Dialog parts and draft state

Packaged dialogs expose Header, Title, Body, Footer, Field, Error, Apply, and Cancel. The Background dialog also exposes Reset, which picks the layout's background. Put a named part directly inside the dialog to replace it, or pass hidden to remove it.

<PptxEditor.FontDialog {...props}>
  <PptxEditor.FontDialog.Field name="kerningAt" hidden />
</PptxEditor.FontDialog>
Draft hook memberUse
values, setValue, errorsDraft fields and their translated validation messages, one for each field.
isEnabledWhether Apply can run.
apply, cancelValidate and commit the draft, or discard it. A refusal keeps the dialog open with the engine's reason in the Error part.
resetuseBackgroundDialog only.

Each dialog has its own hook, such as useFontDialog or useParagraphDialog. Set preset={false} on the dialog only when you compose every part yourself. Then the dialog renders only your children. Keep a Title, the Error part, Apply, and Cancel in that arrangement. For the field names of each dialog, see Dialog fields.

The packaged link popup opens under the selected text on the slide. With no text selected, it opens under the control that opened it. It has Apply, Cancel, and Remove link when the text has a link.

Use useHyperlinkPopup() to build your own link UI. It gives the link popup's state with no UI. Inside a root, it is the state that the packaged popup shows, so your panel and the link controls agree.

Turn off the packaged popup with popups.hyperlink: false, or hyperlinkPopup={false} on <PptxEditor>. Then mount your panel anywhere inside the root. While it is mounted, the toolbar's link control, Insert > Link…, and Ctrl+K (Cmd+K on macOS) open it.

import { translateKeyOrText } from '@pptx-editor.dev/core/editor';
import { PptxEditor, useHyperlinkPopup, useTranslation } from '@pptx-editor.dev/react';

function LinkPanel() {
  const { state, setUrl, commitEdit, unlink, close } = useHyperlinkPopup();
  const { t } = useTranslation();
  if (state.mode !== 'editing') return null;
  // Open under the anchor. With no anchor, place the panel yourself.
  const box = state.anchor?.getBoundingClientRect();
  return (
    <form
      className="acme-link-panel"
      style={box ? { position: 'fixed', left: box.left, top: box.bottom + 4 } : undefined}
      onKeyDown={(event) => {
        if (event.key === 'Escape') close();
      }}
      onSubmit={(event) => {
        event.preventDefault();
        commitEdit();
      }}
    >
      <input
        autoFocus
        aria-label="Link address"
        value={state.draft}
        onChange={(event) => setUrl(event.target.value)}
      />
      {state.error ? <p role="alert">{translateKeyOrText(t, state.error)}</p> : null}
      <button type="submit">Apply</button>
      {state.url ? (
        <button type="button" onClick={unlink}>
          Remove
        </button>
      ) : null}
      <button type="button" onClick={close}>
        Cancel
      </button>
    </form>
  );
}

export function DeckEditor({ bytes }: { bytes: Uint8Array }) {
  return (
    <PptxEditor.Root document={bytes} popups={{ hyperlink: false }}>
      <PptxEditor.Viewport>
        <PptxEditor.Toolbar />
        <PptxEditor.Content />
        <LinkPanel />
      </PptxEditor.Viewport>
    </PptxEditor.Root>
  );
}

This example renders editing mode only. Use the packaged HyperLink parts for a panel with reading mode and keyboard access. The hook supplies state and actions. Your panel supplies positioning, focus handling, and Escape handling:

  • Position. state.anchor is the element the request came from. Else it is the visible toolbar link button, the open menu bar trigger, or the slide. Calls without an anchor use the caret or slide when available. A standalone instance can have no anchor. The example reads the anchor's box at each render. Read it again on scroll and resize, or use your own positioning library.
  • Focus. The field takes focus when the panel opens. When the panel closes, PptxEditor.Root returns focus to the slide, unless focus moved elsewhere.
  • Escape. The form closes the panel on Escape.
  • Refusal. commitEdit() returns false when the engine refuses the target, for example an unsafe address. The panel stays open, and state.error contains a catalog key, such as errors.unsafeLink, or the engine's own text. Translate it with translateKeyOrText(t, state.error).

The link controls open the panel in editing mode, with the selected link in state.url and its draft in state.draft. A click into linked text opens reading mode. Keep that mode separate from your edit form. The packaged panel leaves focus at the caret and supports F6 to focus its actions. See The link popup renderer.

MemberDescription
statemode (closed, reading, or editing), url, safe href (or null), anchor, the edited field draft, copied, refusal text error (or null), and canEdit (whether the selection can take a link now). In Vue, a computed ref.
open(link?, anchor?), openAtCaret(anchor?), close()Show or hide the panel. open() reads the link at the caret, or edits when there is none. open(null) edits. A supplied link requests reading mode for the current selection. openAtCaret edits the selection, as Ctrl+K does.
setUrl(url), beginEdit(), commitEdit()Change and apply the draft. A change updates draft and clears copied and error.
unlink(), copy(), openTarget()Remove the link (the text stays), copy the safe target, or open it in a new tab. A target that sanitizeHref refuses never opens.

useHyperlinkPopupInstance(active?) makes a separate popup state, including inside a root. Set active to false to stop it from listening for editor link requests. PPTX Editor writes a link on the selected text with setLink, so the state has no display text field. To restyle the packaged popup instead, see The link popup renderer.

Open a dialog from your control

import { useEditorDialog } from '@pptx-editor.dev/react';

export function PageSetupButton() {
  const dialog = useEditorDialog('pageSetup');
  return (
    <button
      type="button"
      aria-disabled={!dialog.isAvailable || undefined}
      onClick={() => {
        if (dialog.isAvailable) dialog.open();
      }}
    >
      Page setup
    </button>
  );
}

Focus returns to the button when the dialog closes. The button stays focusable when the pageSetup entry is false. For the other keys, see Open a dialog by key.

Limits

  • A false entry turns off the surface, not the operation. The control that opens it stays visible and disabled with a reason. See What a false entry does.
  • A dialog closes when the editor opens another document. The Alt Text and Format Picture dialogs also close when the selection moves to another object. See Popup lifetime.
  • Dialogs render outside PptxEditor.Viewport. Scope your theme on an element around PptxEditor.Root. See Dialog styling.
  • A full custom arrangement must keep a title, an error region, OK (Apply), and Cancel.
  • An application modal must trap and restore focus like the packaged surface.

See also