Have more questions? Join our

Accessible UI primitives (boardweaver/ui)

The platform bundles Radix UI — the radix-ui package, version 1.6.7 — and exposes it as boardweaver/ui. Import it like any module: it resolves to a single shared copy the frame provides, so nothing is added to your bundle, and your game is version-locked to that copy. The platform upgrades it deliberately, never underneath a committed game.

import { Dialog, ContextMenu, Tooltip } from "boardweaver/ui";

Reach for these instead of hand-rolling. They are unstyled — you own every pixel — and they already handle focus traps, escape/outside-click dismissal, roving tabindex, hover-and-focus parity, and the aria-* wiring that hand-written versions almost always omit.

The ones you will use most

Import Use it for
Dialog Modal surfaces: rules, settings, setup
AlertDialog Confirming something irreversible ("resign?"). Unlike Dialog it does not dismiss on outside click and focuses cancel first
Tooltip Short text on hover and keyboard focus
HoverCard Rich hover content — hover a card, show it enlarged
Popover Anchored non-modal panels: piece detail, bid entry
ContextMenu Right-click menus
DropdownMenu The click-triggered menu. Use this for piece actions — there is no right-click on touch, so ContextMenu alone is desktop-only
VisuallyHidden Screen-reader-only text. Use it to announce state changes a sighted player reads from the board

Everything else available

Accordion, AccessibleIcon, AspectRatio, Avatar, Checkbox, Collapsible, Direction, Form, Label, Menubar, NavigationMenu, Portal, Progress, RadioGroup, ScrollArea, Select, Separator, Slider, Slot, Switch, Tabs, Toast, Toggle, ToggleGroup, Toolbar.

Each import is a namespace of subcomponents (Dialog.Root, Dialog.Trigger, Dialog.Content, …). The prop types are Radix's own, so the upstream documentation applies exactly — look up any component at https://www.radix-ui.com/primitives/docs/components/<name>.

Three notes on that upstream documentation:

  • It documents Radix's current release, which may be newer than the 1.6.7 bundled here. If a prop the site describes is a type error in your game, that is the likely reason — it landed after the bundled version. Trust the type error.

  • Where it says import * as Dialog from "@radix-ui/react-dialog", use import { Dialog } from "boardweaver/ui" instead. Importing @radix-ui/* or radix-ui directly is a compile error — those specifiers do not exist inside a game.

  • Anything under Radix's "internal" or "unstable" entry points is not available. If you cannot import it from boardweaver/ui, it is not part of the platform surface.

A worked example

import { useState } from "react";
import { Dialog, DropdownMenu, VisuallyHidden } from "boardweaver/ui";
import { useMatch, useSelf } from "boardweaver/react";

export default function App() {
  const match = useMatch();
  const { isActive } = useSelf();
  const [rulesOpen, setRulesOpen] = useState(false);

  return (
    <div>
      {/* Announce turn changes to screen readers without drawing anything. */}
      <VisuallyHidden.Root>
        {isActive ? "Your turn" : "Waiting for your opponent"}
      </VisuallyHidden.Root>

      <DropdownMenu.Root>
        <DropdownMenu.Trigger aria-label="Piece actions">…</DropdownMenu.Trigger>
        <DropdownMenu.Portal>
          <DropdownMenu.Content side="bottom" style={{ background: "#fff", padding: 4 }}>
            <DropdownMenu.Item
              disabled={!isActive}
              onSelect={() => match.click({ type: "Click", pieceId: "tile-1" })}
            >
              Play tile
            </DropdownMenu.Item>
          </DropdownMenu.Content>
        </DropdownMenu.Portal>
      </DropdownMenu.Root>

      <Dialog.Root open={rulesOpen} onOpenChange={setRulesOpen}>
        <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" }}>
            <Dialog.Title>How to play</Dialog.Title>
            <Dialog.Description>Place a tile, then end your turn.</Dialog.Description>
            <Dialog.Close>Close</Dialog.Close>
          </Dialog.Content>
        </Dialog.Portal>
      </Dialog.Root>
    </div>
  );
}

Styling

Every primitive renders a plain element with no styles attached, so style it however you style the rest of your game — inline style, a <style> tag, className plus your own CSS. Radix also sets data-state attributes (data-state="open", data-state="checked") you can target for open/closed and enter/exit styling.

Portals and the frame

Dialog.Portal and friends render into document.body — which, inside the game frame, is your document. Nothing can clip or escape your UI, and you never need to think about the host page.