Getting started

Build your first Waraq editor in minutes.

This guide walks you through assembling a minimal but fully functional design editor.

Minimal editor

"use client"
import {
  Waraq,
  WaraqBackground,
  WaraqStage,
  WaraqFrame,
  WaraqPanel,
  WaraqPane,
  WaraqPaneTitle,
  WaraqPaneContent,
  WaraqToolbar,
  WaraqToolbarGroup,
  PaneAddLayer,
  PaneLayerTree,
  ActionToolbarTool,
  ActionToolbarHistory,
  ActionToolbarZoomGroup,
  WaraqKeyboardShortcuts,
  ActionPosition,
  ActionSize,
  ActionFill,
  ActionBorder,
  ActionCorner,
} from "@codecanon/waraq"

export default function Editor() {
  return (
    <Waraq>
      {/* Canvas background pattern */}
      <WaraqBackground variant="dots" />

      {/* Top toolbar */}
      <WaraqToolbar position="top">
        <WaraqToolbarGroup>
          <ActionToolbarTool />
        </WaraqToolbarGroup>
        <WaraqToolbarGroup>
          <ActionToolbarHistory />
        </WaraqToolbarGroup>
        <ActionToolbarZoomGroup />
        <WaraqToolbarGroup>
          <WaraqKeyboardShortcuts />
        </WaraqToolbarGroup>
      </WaraqToolbar>

      {/* Left panel — layers */}
      <WaraqPanel position="left-start">
        <WaraqPane>
          <WaraqPaneTitle>Add layer</WaraqPaneTitle>
          <WaraqPaneContent>
            <PaneAddLayer />
          </WaraqPaneContent>
        </WaraqPane>
        <WaraqPane>
          <WaraqPaneTitle>Layers</WaraqPaneTitle>
          <WaraqPaneContent>
            <PaneLayerTree />
          </WaraqPaneContent>
        </WaraqPane>
      </WaraqPanel>

      {/* Canvas */}
      <WaraqStage>
        <WaraqFrame />
      </WaraqStage>

      {/* Right panel — properties */}
      <WaraqPanel position="right-start">
        <WaraqPane showFor="layer">
          <WaraqPaneTitle>Position & size</WaraqPaneTitle>
          <WaraqPaneContent>
            <ActionPosition />
            <ActionSize />
          </WaraqPaneContent>
        </WaraqPane>
        <WaraqPane showFor="layer">
          <WaraqPaneTitle>Appearance</WaraqPaneTitle>
          <WaraqPaneContent>
            <ActionFill />
            <ActionCorner />
            <ActionBorder />
          </WaraqPaneContent>
        </WaraqPane>
      </WaraqPanel>
    </Waraq>
  )
}

Persisting state

Pass data and onDataChange to sync the canvas with your own state or storage:

import { createWaraqData, type WaraqData } from "@codecanon/waraq/lib"
import { useState } from "react"

export default function Editor() {
  const [data, setData] = useState<WaraqData>(() => createWaraqData())

  return (
    <Waraq data={data} onDataChange={setData}>
      {/* … */}
    </Waraq>
  )
}

WaraqData is plain JSON, so you can serialize it to localStorage, a database, or a URL param.

Loading data from an API

Pass null while your data is in-flight. The editor defers history initialisation until the real value arrives, so undo/redo starts from the correct baseline instead of an empty state:

import { type WaraqData } from "@codecanon/waraq/lib"
import { useEffect, useState } from "react"

export default function Editor() {
  const [data, setData] = useState<WaraqData | null>(null)

  useEffect(() => {
    fetchDesign().then(setData)
  }, [])

  return (
    <Waraq data={data} onDataChange={setData}>
      {/* … */}
    </Waraq>
  )
}

null vs undefinednull means "controlled but loading". undefined (i.e. omitting the prop) means uncontrolled, and the editor treats initialLayers as the permanent starting state. Always use null while waiting for data, never undefined.

Controlling the editor programmatically

Use the focused hooks inside any component that is a child of <Waraq>. Each hook subscribes only to the state it needs, so components re-render only when relevant state changes:

"use client"
import { useWaraqDocument, useWaraqZoom } from "@codecanon/waraq"
import { Button } from "@codecanon/waraq/ui"

function MyControls() {
  const { addLayer, deleteSelected, layers } = useWaraqDocument()
  const { zoomFit } = useWaraqZoom()

  return (
    <div className="flex gap-2">
      <Button onClick={() => addLayer("text")}>Add text</Button>
      <Button variant="destructive" onClick={deleteSelected}>Delete</Button>
      <Button variant="outline" onClick={zoomFit}>Fit canvas</Button>
      <span>{layers.length} layers</span>
    </div>
  )
}

Adding custom layer types

See Custom layers for a full guide.

import { Waraq } from "@codecanon/waraq"
import { Type } from "lucide-react"

const myLayerTypes = [
  {
    id: "badge",
    name: "Badge",
    icon: <Type size={16} />,
    defaultValues: {
      value: "New badge",
      cssVars: { "--width": "120px", "--height": "40px" },
    },
    Component: ({ layer }) => (
      <div className="flex h-full items-center justify-center rounded-full bg-blue-500 px-4 text-white text-sm font-semibold">
        {layer.value}
      </div>
    ),
  },
]

export default function Editor() {
  return (
    <Waraq layerTypes={myLayerTypes}>
      {/* … */}
    </Waraq>
  )
}

On this page