Layout
Place the PPTX editor toolbar, menu bar, slide canvas, slide rail, speaker notes, zoom control, and side panel.
Compose the editor from parts inside PptxEditor.Root. Root owns the editor and renders no element. For every part and prop, see the layout reference.
Workspace layout
The packaged PptxEditor uses this arrangement, and a composed editor works best with the same one. The toolbar is on top. Under it, a row holds the slide rail, a main column, and an optional side panel. The main column holds the canvas, the floating parts, and the notes pane. In the packaged editor, the view switch, the zoom pill, and the font notice float above the notes pane at every notes height. The canvas leaves room for them under the fitted slide.
import { useState } from 'react';
import { PptxEditor } from '@pptx-editor.dev/react';
import '@pptx-editor.dev/core/styles/editor.css';
export function Workspace({ bytes }: { bytes: Uint8Array }) {
const [panel, setPanel] = useState(false);
return (
<div className="acme-theme">
<PptxEditor.Root document={bytes}>
<PptxEditor.Viewport className="deck-workspace">
<div className="deck-workspace__dock">
<PptxEditor.Toolbar />
<button type="button" aria-pressed={panel} onClick={() => setPanel(!panel)}>
Format
</button>
</div>
<div className="deck-workspace__body">
<PptxEditor.SlideRail paneWidth={220} />
<div className="deck-workspace__main">
<PptxEditor.Content />
<PptxEditor.FindBar />
<PptxEditor.EditNotice />
<PptxEditor.FontNotice />
<PptxEditor.Loading overlay />
<PptxEditor.NotesPane defaultOpen={false} style={{ maxHeight: 240 }} />
<PptxEditor.ZoomPill />
</div>
{panel && <PptxEditor.SidePanel paneWidth={320} onClose={() => setPanel(false)} />}
</div>
<PptxEditor.Slideshow />
</PptxEditor.Viewport>
</PptxEditor.Root>
</div>
);
}<script setup lang="ts">
import {
PptxEditorContent,
PptxEditorLoading,
PptxEditorRoot,
PptxEditorSlideRail,
PptxEditorToolbar,
PptxEditorViewport,
} from '@pptx-editor.dev/vue';
import '@pptx-editor.dev/core/styles/editor.css';
defineProps<{ bytes: Uint8Array }>();
</script>
<template>
<div class="acme-theme">
<PptxEditorRoot :document="bytes">
<PptxEditorViewport class="deck-workspace">
<div class="deck-workspace__dock">
<PptxEditorToolbar />
</div>
<div class="deck-workspace__body">
<PptxEditorSlideRail :pane-width="220" />
<div class="deck-workspace__main">
<PptxEditorContent />
<PptxEditorLoading overlay />
</div>
</div>
</PptxEditorViewport>
</PptxEditorRoot>
</div>
</template>The deck-workspace classes are your own CSS:
.acme-theme {
height: 100vh;
}
.deck-workspace {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
background: var(--doc-bg);
}
.deck-workspace__dock {
display: flex;
justify-content: center;
padding: 8px 14px 10px;
}
.deck-workspace__body {
display: flex;
flex: 1 1 auto;
min-height: 0;
padding-left: 12px;
}
.deck-workspace__main {
position: relative;
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-width: 0;
min-height: 0;
}| Part | Requirement |
|---|---|
PptxEditor.Viewport | The styling scope: a plain block element with no layout of its own. Give it a height, or a flexible parent with min-height: 0. It is a landmark region named Presentation editor, so a screen reader user can go to the editor. |
| Each flex level | Keep min-height: 0, so the canvas and the rail scroll inside the window instead of growing the page. |
| The main column | position: relative. The zoom pill, the find bar, and the notices use absolute positions. |
PptxEditor.Toolbar, PptxEditor.Menu | Inside PptxEditor.Viewport. Their menus render inside the viewport and take its tokens. |
PptxEditor.Content | Attaches the slide canvas. |
Root hosts the link popup. Ctrl+K (Cmd+K on macOS), the toolbar's link control, and Insert > Link… work with no toolbar on the page. Keep every part, menu, and application control that uses the editor under the same root.
Name the editor landmark
The viewport is a region landmark with the name Presentation editor (viewport.ariaLabel). The slide rail (navigation, Slides) and the speaker notes (region, Speaker notes) are landmarks inside it. When a page shows more than one editor, give each one its own name with label. It takes text or a translation key.
When your own landmarks (header, main, aside) are inside the viewport, set label={false}. The viewport then renders no region, and your landmarks stay at the top level. Put the toolbar and the canvas in your main.
<PptxEditor.Viewport label={false}>
<header>{/* your app bar */}</header>
<main>
<PptxEditor.Toolbar />
<PptxEditor.Content />
</main>
</PptxEditor.Viewport>In Vue, write <PptxEditorViewport :label="false">.
Change one part of the packaged editor
Use part props to configure the packaged <PptxEditor>. Each prop takes true, false, or the props object of its part. An object adds the part with those props, so you can change one part and keep the packaged editor.
<PptxEditor
document={bytes}
menu={{ children: <PptxEditor.Menu.Arrange hidden /> }}
contextMenu={{ children: <PptxEditor.ContextMenu.Cut hidden /> }}
slideRail={{ orientation: 'horizontal' }}
notesPane={false}
/>| Prop | Part | Default |
|---|---|---|
toolbar | PptxEditor.Toolbar (ToolbarProps). | true |
menu | PptxEditor.Menu (MenuProps), above the toolbar. | false |
contextMenu | PptxEditor.ContextMenu (PptxEditorContextMenuProps). It sets popups.contextMenu, and a popups.contextMenu entry wins over it. | true |
hyperlinkPopup | The packaged link popup. false acts as popups.hyperlink: false. | true |
slideRail | PptxEditor.SlideRail (SlideRailProps). | true |
notesPane | PptxEditor.NotesPane (NotesPaneProps). | true |
openFailure | PptxEditor.OpenFailure (PptxEditorOpenFailureProps). See Open failure. | true |
In Vue, pass the same props to PptxEditor, for example :menu="true" and :notes-pane="false". To show PptxEditor.StatusBar, compose the editor.
Switch between Normal and Slide Sorter
The packaged PptxEditor swaps its workspace when the view changes. In a composed editor, read the view with useEditorState and make the same switch. The view.presentationView slot, in the View menu or a toolbar, changes the view.
The slide master view needs no switch. In that view, PptxEditor.SlideRail lists masters and layouts, PptxEditor.Content shows the bar with Close master, and PptxEditor.NotesPane hides. See Edit layouts and masters.
import { PptxEditor, useEditorState } from '@pptx-editor.dev/react';
export function Workspace() {
const sorter = useEditorState((s) => s.presentationView === 'slideSorter');
return sorter ? (
<PptxEditor.SlideSorter />
) : (
<>
<PptxEditor.SlideRail />
<PptxEditor.Content />
<PptxEditor.NotesPane />
</>
);
}Slide rail parts
The action row under the thumbnails has named parts, as PptxEditor.Toolbar and PptxEditor.StatusBar have. A named child replaces its part in place. Other children go after Delete. preset={false} renders only your children, in your order.
<PptxEditor.SlideRail orientation="horizontal">
<PptxEditor.SlideRail.MoveEarlier hidden />
<PptxEditor.SlideRail.MoveLater hidden />
<PptxEditor.SlideRail.AddSlide icon={<PlusIcon />} className="acme-add" />
<PptxEditor.SlideRail.Action label="Ask for feedback" icon={<ChatIcon />} onSelect={ask} />
</PptxEditor.SlideRail>A horizontal rail narrower than 420 pixels, as on a phone, keeps New slide, Hide thumbnails, and your actions in the row. It folds the layout gallery, Duplicate slide, the two moves, and Delete slide into one More slide actions menu. A hidden part stays out of that menu. A vertical rail never folds. In a right-to-left horizontal rail, the earlier move goes to the right: the buttons, the slide menu rows, their shortcuts, and the icons follow the arrow keys.
| Part | Button |
|---|---|
SlideRail.AddSlide | New slide |
SlideRail.ChooseLayout | The layout gallery |
SlideRail.Duplicate | Duplicate slide |
SlideRail.MoveEarlier, SlideRail.MoveLater | The two move buttons |
SlideRail.Delete | Delete slide |
SlideRail.Collapse | Hide thumbnails. toggle={false} on the rail removes it. |
SlideRail.Expand | Show thumbnails, the one button of the folded strip. toggle={false} on the rail removes it. |
SlideRail.Action | Your own button: the same component as Toolbar.Action. |
Each slide in the rail and in the slide sorter has the name Slide 3, or Slide 3 (hidden) for a hidden slide. The name comes from hidden text in the item, not from aria-label. The thumbnail art is hidden from assistive technology, so the slide's text is not read as the name of the item. A layout tile has the name of its layout.
Each named part takes hidden, icon, className, and asChild. A hidden button keeps its command on the keyboard and in the slide menu. A hidden SlideRail.Expand leaves the folded strip with no button. The open rail keeps the current slide in view, also when its panel changes size. For the Vue names, see React and Vue reference.
Horizontal slide rail
In a tall, narrow host, such as a panel beside a chat, put the rail under the slide as a row. In Vue, pass the same prop to PptxEditorSlideRail.
<PptxEditor.Content />
<PptxEditor.SlideRail orientation="horizontal" />
<PptxEditor.NotesPane />The row works as the column does. Left and Right step through the slides, and Ctrl+Left and Ctrl+Right (Cmd on macOS) move them. A drag reorders along the row. On a section header, Down expands and Up collapses. Hide thumbnails folds the row down. To change the thumbnail width, see Horizontal rail hooks.
Open the slide rail from your chrome
Set toggle={false} to remove the rail's own buttons, and control open from your application. The example uses the CSS from Workspace layout.
export function RailFromHostChrome({ bytes }: { bytes: Uint8Array }) {
const [rail, setRail] = useState(true);
return (
<PptxEditor.Root document={bytes}>
<PptxEditor.Viewport className="deck-workspace">
<div className="deck-workspace__dock">
<button type="button" aria-pressed={rail} onClick={() => setRail(!rail)}>
Slides
</button>
<PptxEditor.Toolbar />
</div>
<div className="deck-workspace__body">
<PptxEditor.SlideRail open={rail} onOpenChange={setRail} toggle={false} />
<div className="deck-workspace__main">
<PptxEditor.Content />
</div>
</div>
</PptxEditor.Viewport>
</PptxEditor.Root>
);
}Build a notes toggle
useNotesPane() returns open, and setOpen and toggle functions that keep their identity across renders. Without open, the notes pane reads the same state.
function NotesToggle() {
const notes = useNotesPane();
return (
<button type="button" aria-pressed={notes.open} onClick={notes.toggle}>
Notes
</button>
);
}Render one notes pane in each root. For the pane props and the notes height, see Pane configuration.
Open the selection pane
The selection pane lists the objects of the current slide, top first, with groups as a tree. A click selects an object, even a hidden one. The eye button shows or hides it, and a double-click or F2 renames it. Show all, Hide all, and the order buttons act on the slide.
The side panel shows the pane on its Selection tab. To open that tab from your own control, set tab and onTabChange (v-model:tab in Vue):
import { useState } from 'react';
import { PptxEditor, type SidePanelTab } from '@pptx-editor.dev/react';
export function Panel() {
const [tab, setTab] = useState<SidePanelTab>('format');
return (
<>
<button type="button" onClick={() => setTab('selection')}>
Selection pane
</button>
<PptxEditor.SidePanel tab={tab} onTabChange={setTab} />
</>
);
}<script setup lang="ts">
import { shallowRef } from 'vue';
import { PptxEditorSidePanel, type SidePanelTab } from '@pptx-editor.dev/vue';
const tab = shallowRef<SidePanelTab>('format');
</script>
<template>
<button type="button" @click="tab = 'selection'">Selection pane</button>
<PptxEditorSidePanel v-model:tab="tab" />
</template>Render these components inside the editor root.
In the pane, a row keeps the keyboard focus. Up and Down move, Right and Left open and close a group, H shows or hides the object, and F2 renames it. Show all also shows the hidden objects inside groups. Hide all hides the top-level objects.
Open the side panel from the toolbar
The toolbar's Selection pane, Format shape, and Edit chart… controls call requestPane(editor, pane). A mounted side panel turns to that tab and moves the focus to it. While no side panel is mounted and your host does not listen, the controls are disabled with the disabled.noSidePanel reason.
When your layout can hide the side panel, listen for the requests and show the panel. listenPaneRequests returns a function that stops the listener:
import { useEffect, useState } from 'react';
import {
PptxEditor,
listenPaneRequests,
usePptxEditor,
type SidePanelTab,
} from '@pptx-editor.dev/react';
// Render inside the editor root.
export function Panel() {
const editor = usePptxEditor();
const [open, setOpen] = useState(false);
const [tab, setTab] = useState<SidePanelTab>('format');
useEffect(() => {
if (!editor) return;
return listenPaneRequests(editor, (pane) => {
setTab(pane);
setOpen(true);
});
}, [editor]);
return open ? (
<PptxEditor.SidePanel tab={tab} onTabChange={setTab} onClose={() => setOpen(false)} />
) : null;
}<script setup lang="ts">
import { shallowRef, watch } from 'vue';
import {
PptxEditorSidePanel,
listenPaneRequests,
usePptxEditor,
type SidePanelTab,
} from '@pptx-editor.dev/vue';
// Render inside the editor root.
const editor = usePptxEditor();
const open = shallowRef(false);
const tab = shallowRef<SidePanelTab>('format');
watch(
editor,
(e, _old, onCleanup) => {
if (!e) return;
onCleanup(
listenPaneRequests(e, (pane) => {
tab.value = pane;
open.value = true;
})
);
},
{ immediate: true }
);
</script>
<template>
<PptxEditorSidePanel v-if="open" v-model:tab="tab" :on-close="() => (open = false)" />
</template>requestPane returns false when nothing listens. Every listener hears each request, and your host decides what an answer does.
The listener also gets a PaneRequest with a focus flag. Insert > Chart opens the Chart tab with focus set to false, so the keyboard stays on the slide and Undo and Delete act on the new chart. Pass requestPane(editor, pane, { focus: false }) to open a tab the same way.
Show the side panel on a narrow screen
On a phone or a narrow window, put the side panel under the slide in your layout, not over it. The slide area gets shorter, the editor fits the slide to the new size, and the chart or shape that you edit stays in view. In a short window, such as a phone held sideways, keep the panel at the right with a smaller width.
The following CSS assumes that your layout puts the slide area (.my-slide) and the side panel in one flex row (.my-workspace):
@media (max-width: 900px) and (min-height: 501px) {
.my-workspace {
flex-direction: column;
}
.my-workspace > .my-slide {
flex: 1 1 0%;
min-height: 0;
}
.my-workspace > .pptx-panel {
flex: none;
width: auto;
height: 40%;
}
}
@media (max-height: 500px) {
.my-workspace > .pptx-panel {
width: 280px;
}
}Start the panel closed on a narrow screen, and open it from listenPaneRequests: Insert > Chart then shows the Chart tab under the new chart.
Show the pane elsewhere
To show the pane outside the side panel, render PptxEditor.SelectionPane (PptxEditorSelectionPane in Vue) anywhere inside the root. The pane writes through the setShapesVisible, renameShape, and order commands, so each change is one undo step. In view mode, the pane selects but does not change objects.
Open failure
The editor refuses a file that it cannot open safely, such as a damaged archive or a file that holds no presentation. An open that throws is refused too, so the loading screen never stays up after a failure. When a deck was open before, it stays open. The open-failure notice shows a generic message, says whether the open deck stays, and shows a stable error code. It names no contact of its own.
Copy details copies a plain-text report that a person can paste into a support request: the code, the reason, the editor version, the browser, and the time. The report holds no file content and no file name. When the browser blocks the clipboard, the notice shows the report so the person can select and copy it. Build the same text yourself with openFailureReport(failure) from @pptx-editor.dev/core/editor. Hide the button with showCopy={false} (Vue: :show-copy="false").
<PptxEditor> renders the notice. A composed editor does not: add PptxEditor.OpenFailure (Vue: PptxEditorOpenFailure) inside PptxEditor.Root, in a positioned box. For its options and the error codes, see Open-failure options.
Add your own actions
Pass renderOpenFailureActions in React, or fill the #openFailureActions slot in Vue, to add buttons before Close. Each receives the context: failure, message, and dismiss().
<PptxEditor
document={bytes}
renderOpenFailureActions={({ failure }) => (
<a className="pptx-open-failure__action" href={`/support?code=${failure.code}`}>
Get help
</a>
)}
/><PptxEditor :document="bytes">
<template #openFailureActions="{ failure }">
<a class="pptx-open-failure__action" :href="`/support?code=${failure.code}`">Get help</a>
</template>
</PptxEditor>Send the code to your support page, not the file. renderOpenFailureActions replaces openFailure.actions, so pass only one. With openFailure={false} (Vue: :open-failure="false"), it renders nothing. Both cases print a development warning.
Show a banner or a toast
Turn off the notice with openFailure={false} (Vue: :open-failure="false"), then read the failure with useOpenFailure inside the root:
import { PptxEditor, useOpenFailure } from '@pptx-editor.dev/react';
function OpenFailureBanner() {
const { failure, message, dismiss } = useOpenFailure();
if (!failure) return null;
return (
<div role="alert" className="banner">
{message} ({failure.code})
<button type="button" onClick={() => dismiss(failure)}>
Dismiss
</button>
</div>
);
}
<PptxEditor document={bytes} openFailure={false}>
<OpenFailureBanner />
</PptxEditor>;<!-- OpenFailureBanner.vue -->
<script setup lang="ts">
import { useOpenFailure } from '@pptx-editor.dev/vue';
const { failure, message, dismiss } = useOpenFailure();
</script>
<template>
<div v-if="failure" role="alert" class="banner">
{{ message }} ({{ failure.code }})
<button type="button" @click="dismiss(failure)">Dismiss</button>
</div>
</template>Mount the banner in the default slot of <PptxEditor :document="bytes" :open-failure="false">.
dismiss hides only the current refusal. Pass the failure that your UI showed, so a late close cannot hide a newer refusal. kept says whether the deck that was open stays open.
Limits
- Keep the classes that
Viewportsupplies. The tokens and the menus find their scope through.pptx-editor. - Render one notes pane in each root. A second one keeps its own state and prints a development warning.
- In the Slide Sorter view, mount
PptxEditor.SlideSorter. Without it, a development warning says so. - Mount
PptxEditor.Slideshowonce inside the viewport, andPptxEditor.FindBarfor Find and Replace. - In a long deck, the slide rail and the Slide Sorter do not paint every thumbnail when they open. The current slide, the first thumbnails of the list, and the thumbnails in view paint at once. The others paint when they scroll into view or when the browser is idle, and stay empty until then. A custom list gets the same behavior with
editor.renderThumbnail(index, element, { defer: true }). Call the function that it returns when the list removes the element. - While a long deck opens, the slide rail mounts its first screen with the first slide and the other items over the next frames, and a thumbnail is an empty placeholder until its slide is ready. See Long decks.