Detailed references

v3 · Preview

Framework v3 is an opt-in preview available to everyone. New games use the stable framework by default.

Client libraries

Besides react and boardweaver/react, a client may import five libraries. The frame provides one copy of each, so they add nothing to the game's bundle. 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
react-intl react-intl 12.1.4 Showing the game's text in the player's language, from its catalogs

Import each library by the name in the first column. The build refuses motion/react, radix-ui and @dnd-kit/core with Cannot resolve import. Import as values only the names listed under each library below. The build refuses any other value with No matching export. Import an upstream type, such as DragEndEvent, with type: the build erases it.

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.

How to time a Motion animation of a board change is in Directional animation and Optional animation helpers.

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. A dialog stays inside the frame and never covers Boardweaver's bar.

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 step like any other: send it with act from onDragEnd. When the game takes a move as a PieceClick and then a SpaceClick, pass both steps as one list. Make a piece draggable only while its PieceClick is offered. 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 game offers a piece's target spaces only after its PieceClick, so make every space 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, with applied 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.

boardweaver/game already holds a schema for each client type. Write a schema with z only for JSON that no client type describes.

react-intl

FormattedMessage, FormattedNumber, FormattedDate, FormattedTime, FormattedDateTimeRange, FormattedList, FormattedPlural, FormattedRelativeTime, FormattedDisplayName, useIntl, defineMessage and defineMessages. The components and useIntl() are react-intl's own, so its documentation at https://formatjs.github.io/docs/react-intl applies. react-intl exports no IntlProvider: the frame renders the provider, with the game's catalogs. How to write and show the client's messages, and why it passes no defaultMessage, is in Messages in the client.

Need a hand with this?

Join our Discord