Have more questions? Join our Discord

Client libraries

Besides react and boardweaver/react, a client may import four libraries. The frame provides one shared copy of each, so they add nothing to the game's bundle, and every game uses the versions below.

Import Library Use it for
boardweaver/motion motion/react from motion 12.43.0 Animating pieces, panels and effects
boardweaver/ui radix-ui 1.6.7 Dialogs, menus, tooltips and other accessible controls
boardweaver/dnd @dnd-kit/core 6.3.1 and @dnd-kit/sortable 10.0.0 Dragging a piece onto a space
zod zod 3.25.76 Parsing JSON, as boardweaver/game does

Import only the names listed here as values. Any other value fails the commit with No matching export. Import an upstream type, such as DragEndEvent, with type: the build erases it, so it needs no listing. Importing the upstream package by its own name (motion/react, radix-ui, @dnd-kit/core) fails it with Cannot resolve import. zod is imported by its own name.

boardweaver/motion

motion, LayoutGroup, AnimatePresence, usePresence, useIsPresent, useMotionValue, useTransform, useSpring, useMotionTemplate, useMotionValueEvent, useAnimate, useInView, useAnimationFrame, MotionConfig and useReducedMotion. The props are Motion's own, so its documentation at https://motion.dev/docs/react applies.

  • Animate a move from useLastChange(). Pieces have no ids, so a layoutId cannot follow a piece from one space to another by itself. Each Moved movement gives the piece's from and to positions: draw the piece at to and animate it in from where from is on screen. When the game writes its own id into a piece's publicState, use that id as the layoutId instead.
  • Skip the animation when coalesced is true. The board jumped, as What changed explains.
  • Respect reduced motion. When useReducedMotion() is true, show the end state at once.

boardweaver/ui

AccessibleIcon, Accordion, AlertDialog, AspectRatio, Avatar, Checkbox, Collapsible, ContextMenu, Dialog, Direction, DropdownMenu, Form, HoverCard, Label, Menubar, NavigationMenu, Popover, Portal, Progress, RadioGroup, ScrollArea, Select, Separator, Slider, Slot, Switch, Tabs, Toast, Toggle, ToggleGroup, Toolbar, Tooltip and VisuallyHidden.

Each one is a namespace of parts (Dialog.Root, Dialog.Trigger, Dialog.Content). The parts are unstyled: style them with the style prop or a <style> element, and target Radix's data-state attributes for open and closed styles. Radix's documentation at https://www.radix-ui.com/primitives/docs/components/<name> applies; where it imports @radix-ui/react-dialog, import { Dialog } from boardweaver/ui.

import { Dialog } from "boardweaver/ui";

export function RulesButton() {
  return (
    <Dialog.Root>
      <Dialog.Trigger>Rules</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay style={{ position: "fixed", inset: 0, background: "#0008" }} />
        <Dialog.Content style={{ position: "fixed", inset: "20%", background: "#fff", padding: 16 }}>
          <Dialog.Title>How to play</Dialog.Title>
          <Dialog.Description>Place a mark on an empty square.</Dialog.Description>
          <Dialog.Close>Close</Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

A portal renders into the frame's own document, so a dialog can never cover Boardweaver's bar or escape the frame.

boardweaver/dnd

DndContext, DragOverlay, useDraggable, useDroppable, useSensor, useSensors, PointerSensor, KeyboardSensor, TouchSensor, closestCenter, closestCorners, SortableContext, useSortable, arrayMove, verticalListSortingStrategy, horizontalListSortingStrategy, rectSortingStrategy and sortableKeyboardCoordinates. dnd-kit's documentation at https://docs.dndkit.com applies.

A drop is a move like any other: send it with act from onDragEnd. When the game takes a move as two clicks, a piece and then a space, pass both steps as one list. Make a piece draggable only while its PieceClick is offered, and keep the piece's position in the draggable's data, which onDragEnd reads back:

import type { ReactNode } from "react";
import { DndContext, useDraggable, useDroppable, type DragEndEvent } from "boardweaver/dnd";
import { useAct, useOffers, useUndo } from "boardweaver/react";

type PieceAt = { spaceId: string; index: number };

export function DraggablePiece({ spaceId, index, children }: PieceAt & { children: ReactNode }) {
  const offered = useOffers().some(
    ({ action }) => action.type === "PieceClick" && action.spaceId === spaceId && action.index === index,
  );
  const data: PieceAt = { spaceId, index };
  const { setNodeRef, listeners, attributes, transform } = useDraggable({
    id: `${spaceId}:${index}`,
    data,
    disabled: !offered,
  });
  const style = transform ? { transform: `translate(${transform.x}px, ${transform.y}px)` } : undefined;
  return (
    <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
      {children}
    </div>
  );
}

export function DroppableSpace({ spaceId, children }: { spaceId: string; children: ReactNode }) {
  const { setNodeRef } = useDroppable({ id: spaceId });
  return <div ref={setNodeRef}>{children}</div>;
}

export function DragBoard({ children }: { children: ReactNode }) {
  const act = useAct();
  const { undo } = useUndo();
  const onDragEnd = ({ active, over }: DragEndEvent) => {
    const piece = active.data.current as PieceAt | undefined;
    if (!over || !piece || over.id === piece.spaceId) return;
    const outcome = act([
      { type: "PieceClick", spaceId: piece.spaceId, index: piece.index },
      { type: "SpaceClick", spaceId: String(over.id) },
    ]);
    if (outcome.type === "failed" && outcome.applied) undo();
  };
  return <DndContext onDragEnd={onDragEnd}>{children}</DndContext>;
}

The space a piece may move to is offered only after its PieceClick, so every space is droppable. Ignore a drop back on the piece's own space. A drop on a space the game does not offer fails after the PieceClick joined the draft, and applied is 1: call undo() to take the PieceClick back, or the piece stays picked up. Call act once, in onDragEnd itself, as Taking a step explains.

zod

z and ZodError. This is zod 3, and its documentation at https://v3.zod.dev applies.

  • Parse a map with its client type. boardweaver/game already holds a schema for each client type. Write a schema with z only for JSON no client type describes.