How to create multi-page designs.

The <Designer /> component is designed to be unopinionated about state management, making it easy to build multi-page editors.

The following example demonstrates how to implement a page-based workflow with persistent storage. Each page maintains its own set of layers, and you can seamlessly switch between pages using custom actions.

Pages

We define a Page type to represent each page in the editor. A page has an id, a name, and a layers array.

components/custom-designer.tsx
type Page = {
  id: string
  name: string
  layers: Layer[]
}

We use Jotai to manage the state of the editor. We define a pagesAtom to store the list of pages, and a selectedPageIdAtom to store the ID of the currently selected page.

components/custom-designer.tsx
const INITIAL_PAGES = [
  {
    id: "1",
    name: "Page 1",
    layers: [],
  },
]
 
const pagesAtom = atomWithStorage<Page[]>("pages", INITIAL_PAGES)
const selectedPageIdAtom = atomWithStorage<string>("selectedPageId", INITIAL_PAGES[0].id)

Designer

The <Designer /> component is used to create the editor. We use the layers prop to pass the layers for the currently selected page and the onLayersChange prop to update the layers for the currently selected page.

components/custom-designer.tsx
export function CustomDesigner() {
  return (
    <Designer layers={selectedPage?.layers} onLayersChange={handleLayersChange}>
      <DesignerContent>
        <DesignerCanvas>
          <DesignerFrame />
        </DesignerCanvas>
      </DesignerContent>
      <DesignerToolbar>
    </Designer>
  )
}

ActionPageSelector

components/custom-designer.tsx
function ActionPageSelector() {
  const [pages] = useAtom(pageAtom)
  const [selectedPageId, setSelectedPageId] = useAtom(selectedPageIdAtom)
 
  return (
    <Action>
      <ActionLabel htmlFor="page-selector" className="sr-only">
        Page
      </ActionLabel>
      <ActionControls>
        <Select value={selectedPageId} onValueChange={setSelectedPageId}>
          <SelectTrigger>
            <SelectValue placeholder="Select a page" />
            <SelectContent>
              {pages.map((page) => (
                <SelectItem key={page.id} value={page.id}>
                  {page.name}
                </SelectItem>
              ))}
            </SelectContent>
          </SelectTrigger>
        </Select>
      </ActionControls>
    </Action>
  )
}

ActionAddPage

components/custom-designer.tsx
function ActionAddPage() {
  const [pages, setPages] = useAtom(pageAtom)
  const [, setSelectedPageId] = useAtom(selectedPageIdAtom)
 
  return (
    <Button
      variant="ghost"
      onClick={() => {
        const newPage = {
          id: `page-${pages.length + 1}`,
          name: `Page ${pages.length + 1}`,
          layers: [],
        }
        setPages([...pages, newPage])
        setSelectedPageId(newPage.id)
      }}
    >
      <IconPlus />
      Add Page
    </Button>
  )
}

ActionReset

components/custom-designer.tsx
function ActionReset() {
  const [, setPages] = useAtom(pageAtom)
  const [, setSelectedPageId] = useAtom(selectedPageIdAtom)
 
  return (
    <Button
      variant="outline"
      onClick={() => {
        setPages(INITIAL_PAGES)
        setSelectedPageId("1")
      }}
    >
      Reset
    </Button>
  )
}

ActionExportPages

components/custom-designer.tsx
function ActionExportPages() {
  const [pages] = useAtom(pageAtom)
 
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="primary">Export</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>View JSON</DialogTitle>
          <DialogDescription>
            Here's the JSON representation of the current pages.
          </DialogDescription>
        </DialogHeader>
        <div className="max-h-[50vh] overflow-y-auto rounded-md border bg-black p-2 font-mono text-white text-xs">
          <pre>{JSON.stringify(pages, null, 2)}</pre>
        </div>
        <DialogFooter>
          <DialogClose asChild>
            <Button>Close</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}