Layout and appearance
API
| Hook | Contract |
|---|---|
useGameTheme() |
GameTheme: the game's /theme.json. |
useColorMode() |
"light" | "dark", following the player's setting. |
useViewport() |
Viewport: { width: number; height: number }, the size of the client's area in CSS pixels. It changes when that area is resized. See Layout. |
Layout
useViewport() gives the size of the area your client draws in. It changes when the player resizes the window, rotates a phone or enters full screen. Size the board from it, or choose a layout from it. The tic-tac-toe client sizes its cells from the width, less its padding and gaps:
import { useViewport } from "boardweaver/react";
const PADDING = 16;
const GAP = 6;
function useCellSize() {
const { width } = useViewport();
return Math.max(32, Math.min(88, Math.floor((width - 2 * PADDING - 2 * GAP) / 3)));
}
- Keep a fixed place for controls, status, hands and private goals in every turn phase. Change what they hold without resizing the board.
- Keep a seat's private information on screen during other players' turns while the view still holds it.
- Scale the board to both the width and the height. Give the log its own bounded scrolling area.
- Use wider side panels when the space allows. Test short landscape windows as well as narrow portrait ones.
- Read names and avatars from
usePlayers(). Draw a fallback for a player with noavatarUrl. - Use the dialog parts of
boardweaver/ui(Client libraries) for focus and dismissal.
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.