Have more questions? Join our

This documents framework v1, which is deprecated. Games already on it keep running and their authors can keep editing them, but new games are created on the current framework — start with React frontends.

BWSS styling

BWSS (BoardWeaver Style System) is a strict, structured subset of CSS. PieceDef.render() and SpaceDef.render() return BWSS nodes; the framework validates and serializes them to safe DOM. Raw CSS strings are never accepted.

const node = {
  type: "Box",
  style: {
    width: 120,
    height: 80,
    padding: 8,
    borderRadius: 6,
    backgroundColor: "$colors.surface.canvas",          // theme-token ref
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    color: "#222",
  },
  children: [
    "Score: 5",                                         // text is a string child
    { type: "Image", src: "TrophyIcon", style: { width: 16, height: 16 } },
  ],
};

Node types

type Allowed inside Notes
"Box" piece renders, space renders Generic container. Children: BWSS nodes or strings (text).
"Image" piece renders, space renders { src: GameImageKey, alt?, style? }. See images.
"Svg" piece renders, space renders { viewBox: {width,height}, paths: [{d, fill?, stroke?, ...}], style? }. Only <svg> + <path> are emitted.
"Pieces" space renders only Insertion-point marker for laid-out piece children. See spaces.

There is no Text node — text is a plain string in a Box's children array.

Box

type PieceBoxNode = { type: "Box"; style?: BwssStyle; children?: (PieceBwssNode | string)[] };
type SpaceBoxNode = { type: "Box"; style?: BwssStyle; children?: (SpaceBwssNode | string)[] };

Same JSON shape; the recursive child union differs: piece-side excludes Pieces.

Image

type ImageNode = { type: "Image"; src: GameImageKey; alt?: string; style?: BwssStyle };

src is resolved through the game's images map at render time. Unknown keys render nothing.

Svg

type SvgNode = {
  type: "Svg";
  viewBox: { width: number; height: number };
  paths: SvgPathChild[];
  style?: BwssStyle;
};
type SvgPathChild = {
  d: string;                                            // SVG path-data string
  fill?: string;
  stroke?: string;
  strokeWidth?: number | string;
  fillRule?: "nonzero" | "evenodd";
  strokeLinecap?: "butt" | "round" | "square";
  strokeLinejoin?: "miter" | "round" | "bevel";
  opacity?: number | string;
};
  • Only <svg> and <path> are emitted. No <g>, <defs>, <linearGradient>, <use>, <foreignObject>, <script>, <text>, <image>.
  • d is a character-class-validated string: digits, command letters (MLHVCSQTAZmlhvcsqtaz), whitespace, +-.eE,. Anything else (incl. <>"'();) is rejected.
  • Caps: max 16 paths per Svg; max 4000 chars per d; max 16,000 chars total across paths; viewBox sides ≤ 10,000.

Pieces

type PiecesNode = { type: "Pieces"; style?: BwssStyle };

Only valid inside a space's render tree. The framework substitutes it with the space's laid-out piece children. style layers over the type-defaulted flex props (Stack / Horizontal / Vertical) — author overrides win.

BwssStyle — the allow-list

Strict (unknown properties throw). Categories below; values are typed.

Layout

display, flexDirection, flexWrap, justifyContent, alignItems, gap,
flex, flexBasis, flexGrow, flexShrink,
width, height, minWidth, maxWidth, minHeight, maxHeight,
padding, paddingX, paddingY, paddingTop, paddingRight, paddingBottom, paddingLeft,
margin,  marginX,  marginY,  marginTop,  marginRight,  marginBottom,  marginLeft,
position, top, right, bottom, left,
overflow

display: "flex" | "block" | "inline-flex" | "none". position: "relative" | "absolute". overflow: "hidden" | "auto" | "visible".

Typography

fontSize, lineHeight, letterSpacing,
fontWeight, fontStyle, fontFamily,
textAlign, textTransform, whiteSpace, textOverflow,
color, textGradient

fontWeight: "normal" | "bold" | "100"–"900". fontFamily: "sans" | "serif" | "mono" or a GameFontKey string. textAlign: "left" | "right" | "center" | "justify". textTransform: "none" | "uppercase" | "lowercase" | "capitalize". whiteSpace: "normal" | "nowrap" | "pre" | "pre-line" | "pre-wrap". textOverflow: "clip" | "ellipsis".

Background

backgroundColor, backgroundImage, backgroundGradient,
backgroundSize, backgroundPosition, backgroundRepeat

backgroundGradient is a structured linear | radial array (max 4 layers). No raw CSS linear-gradient(...) strings.

Border

borderRadius,
borderWidth, borderStyle, borderColor,
borderTopWidth, borderTopStyle, borderTopColor,
borderRightWidth, borderRightStyle, borderRightColor,
borderBottomWidth, borderBottomStyle, borderBottomColor,
borderLeftWidth, borderLeftStyle, borderLeftColor

borderStyle (and per-side): "none" | "solid" | "dashed" | "dotted". There is no border / borderTop shorthand — split into width / style / color.

Effects

boxShadow, opacity, filter, clipPath, transform
  • boxShadow: "none" OR an array of structured layers { offsetX, offsetY, blur?, spread?, color, inset? }.
  • opacity: number in [0, 1] or "0%""100%".
  • filter: array of single-key function objects: { blur }, { brightness }, { contrast }, { grayscale }, { hueRotate }, { invert }, { saturate }, { sepia }, { dropShadow: {...} }. Max 8 entries.
  • clipPath: a single-key object: { polygon }, { circle }, { ellipse }, or { inset }.
  • transform: array of single-key function objects: { rotate }, { translateX }, { translateY }, { scale }. Max 4 entries.

Value types

Color (color, backgroundColor, borderColor, boxShadow[*].color, dropShadow.color, Svg fill / stroke)

One of:

  • "#abc" / "#abcdef" / "#abcdef12" (hex, 3/6/8 digits)
  • "rgb(r, g, b)" / "rgba(r, g, b, a)" with numeric args
  • a named CSS color
  • a theme-token ref: "$colors.surface.canvas", "$semanticTokens.fg.muted", etc. (
    lt;segment>(.<segment>)+
    , alphanumerics + _)

No url(), var(), calc().

Length (width, padding, top, borderRadius, ...)

One of:

  • a number (treated as px)
  • a unit string: <number>(px|%|em|rem)
  • a $theme-token ref

No calc(), no vh / vw / vmin / vmax / ch / ex.

Theme tokens

A theme token ref starts with $, has at least two dotted segments, and only contains alphanumerics, _, and .:

$colors.surface.canvas        // OK
$semanticTokens.fg.muted      // OK
$spacing.lg                   // OK
$linear-gradient(...)         // REJECTED — parens disallowed
$colors                       // REJECTED — needs ≥ 2 segments

The resolver returns values that have already been validated as colors / lengths. An unknown token resolves to nothing.

Gotchas

  • Style validation is .strict(): an unknown style property (e.g. transform typoed as tranform, or background shorthand) is REJECTED, not silently dropped.
  • No CSS shorthand parsing anywhere — border, background, font, transition, animation etc. are absent by design.
  • A piece's render() MUST NOT return a Pieces node (or contain one in its children) — piece-side BWSS rejects it.
  • A space's render() MUST return exactly one Pieces node for piece children to appear (zero → no pieces + warning; multiple → fallback to default container + warning).
  • Image.src is a GameImageKey string — unknown keys render nothing. See images.
  • Svg.paths[*].d is character-class restricted: digits, MLHVCSQTAZmlhvcsqtaz, whitespace, +-.eE, only. URLs, parens, and quote marks throw.
  • Text inside a Box is a plain string child — there's no Text node. To style text, set typography style fields on the enclosing Box.