Shapes, behaviors & value bindings
Beyond the kind-specific fields, every component — piece, card, die, token — shares a set of optional rendering refinements. Each one defaults to today’s plain behaviour when absent, so you only add them when an object needs a non-rectangular outline, snap rotation, an attached marker, or a live number.
These are still table-as-scene fields: they change how an object is drawn
and handled, not what is legal. In particular a value’s min / max /
step are display hints — taybl never clamps the number.
How to edit in the Studio
Open your game → Layout tab and select the component. The Behaviors
section exposes the Flippable, Rotatable, and Stackable toggles.
The Shape control sits in the same Properties section — Rectangle,
Circle or Hexagon (a generated custom polygon can be reset to a rectangle
there too). The remaining refinements — footprint, rotationStep /
orientations, attachable, and valueBinding — are produced by the
generation pipeline into the GDL and don’t have a dedicated inspector control
yet; adjust them by editing the generated definition.
Size — where a component’s proportions come from
A component’s Fixed size (Width / Height / Thickness in the inspector) is optional, and the badge on that section tells you which rule is in play:
auto— the component has no size of its own, so it takes its proportions from its artwork and its scale from the zone it sits in. Square art is drawn square, a tall card is drawn tall. This does not depend on whether you called the component a card, a piece or a token, so changing its Type never changes its shape.63×88(any explicit size) — those proportions win, and the artwork is fitted inside them. If the art’s proportions differ you will see the table through the gap, because art is never cropped. Clear all three fields to go back toauto.
taybl adds no corners to your artwork either. Whatever outline you drew is what gets dealt — a card with square corners stays square, and one you drew with rounded corners keeps exactly the curve you gave it. The rounding you see on a placeholder taybl renders for you — a colour-and-letter chip, a dice face with pips, an empty pad — is taybl’s own, and it goes away the moment you bind artwork.
Either way the number is a ratio, not a measurement: most zones scale their contents to the zone box, so size sets proportions and relative scale rather than the absolute size on screen.
One place a fixed size always shows through is dragging. A component with
one is carried at that size — the size it occupies out on the open table —
rather than at whatever size the zone you picked it up from happened to draw
it, so a piece pulled out of a tightly packed supply, or a card out of your
hand, previews the drop instead of the pickup. An auto component is carried
at the size it was drawn at, since it has no size of its own to fall back on.
Shape — the outline within the size box
A component’s shape is the outline drawn inside its size bounding box —
it sets the hit-area, the colour fallback, and how tiles tessellate. It is a
discriminated union on type. Absent → rect.
type | Extra fields | What it draws |
|---|---|---|
rect | — | A rectangle (the default) |
circle | — | A circle with diameter min(w, h). Applies to the colour fallback — uploaded art is never clipped to it, so a round piece wants round art |
hex | orientation: pointy or flat | A hexagon, point-up or flat-top |
polygon | points: array of { x, y } (≥ 3) | A free outline; each point is 0..1 of the size box (0,0 = top-left, 1,1 = bottom-right) — used for meeple silhouettes |
Background colour — a surface behind transparent art
Card and component art is often a transparent PNG. By default nothing is
painted behind it, so the transparent areas show the table felt. Set
backgroundColor to a hex colour (e.g. #ffffff) and taybl paints that solid
fill behind the art — it shows only through the transparent pixels, giving a
clear card asset a proper white surface. An opaque asset simply covers it, so
the fill never appears; absent → no fill (transparent art shows the table).
Edit it in the Layout tab: select a component and use the Appearance → Background colour picker (with a clear button to remove it). Select many components to set them all at once — the multi-select panel has the same control — or ask Pip to "give all my cards a white background".
A die is the exception, and a useful one. A die has no art for the fill to
sit behind, so backgroundColor is the die’s face colour — the single-
component control is labelled Die colour there, and it is how you colour
dice without uploading any art. See dice. (The multi-select panel
keeps the Background label whatever kinds are selected; it writes the same
field.)
| Field | Values | Default | What it does |
|---|---|---|---|
backgroundColor | hex colour string (#rrggbb) | .optional() | Solid fill painted behind the art, visible only through transparent areas — on a die, the face colour itself |
Padding — breathing room inside the cell
By default a component fills its cell edge-to-edge, exactly like an uploaded
image. Set padding to inset the art — it is a fraction (0..0.45) trimmed
from each side, so 0.1 leaves a 10% margin all around and pulls the piece
in. Use it to give a die, meeple, or token a little air in a large cell, or
leave it at 0 for tiles that should tessellate. Board tiles laid on a
tessellating lattice ignore padding and always touch.
Edit it in the Layout tab: select a component and drag the Padding slider — or ask Pip to "add some padding to the dice".
| Field | Values | Default | What it does |
|---|---|---|---|
padding | number 0..0.45 (fraction inset per side) | .optional() (fills the cell) | Breathing room around the art inside its cell |
Preview size — how big it peeks
Resting the pointer on a component, or holding V over it, opens a peek:
the same component, drawn large enough to read. The peek always keeps the
real artwork — its true proportions and its shape outline, so a hex tile
peeks as a hexagon and a round token peeks round. This setting says how BIG
it is drawn, and nothing else.
Set previewSize when the default is the wrong size for what the art has to
say. A dense player aid or a card of rules text wants large; a wooden cube
that is legible at a glance wants small, so its peek does not blot out half
the table. The value is the peek’s longest edge: the named sizes are
small 320px, medium 480px and large 720px, and a plain number
(120–1200) sets it exactly. Whatever you pick is clamped to fit the
player’s screen, so a big value can never open a peek off the edge.
Edit it in the Layout tab: select a component and open Preview size, whose Peek size menu offers Small, Medium, Large and Custom… — the last revealing a Longest edge box for an exact number of pixels. Or ask Pip to "make the player aid preview large". The peek itself belongs to the play surface, so check your choice in Preview rather than on the editor canvas.
How long the pointer has to rest before a peek opens is the player’s choice, not the designer’s — it lives in their account preferences, applies to every game they play, and can be switched off altogether. So size the peek for the reader who opens one, but never write a rule that assumes one opens by itself. See moving around the table.
| Field | Values | Default | What it does |
|---|---|---|---|
previewSize | small | medium | large | number 120..1200 (px) | .optional() (medium, 480px) | Longest edge of the component’s peek |
Layer — what sits on top
Where two objects overlap, one of them has to be drawn over the other, and
normally that is simply whichever was touched last. For cards that is exactly
right. For markers it is the wrong way round: put a cube on a card to mark it,
move the card, and the cube disappears underneath the thing it was marking.
layer settles the question once, in the design, instead of leaving it to
whatever a player happened to pick up most recently.
Higher paints on top; absent → 0. It is a ceiling, not a nudge — a
layer-0 card can never rise above a layer-1 cube, however recently it was
moved. That is the whole point of the field: give your markers a layer above
your cards and they stay visible on the things they mark, permanently, with
nobody having to remember to restack them.
Within one layer nothing changes. The last thing touched still sits on top, so a deck of ordinary cards that all share a layer goes on behaving exactly as it always has — you only notice the field where you have deliberately put things on different layers.
It applies only where pieces sit loose: the open play felt, and any free or
scatter zone. A deck, a fan, a grid and a board cell each order their own
contents — their arrangement already says what sits where — so they ignore it.
This is also what bounds the table’s Bring to front / Send to back
verbs (and the ] / [ keys): they reorder within a layer, so neither will
lift a card above a higher-layer object, and the menu offers them only when
they can actually move something. See
handling components.
Edit it in the Layout tab: select a component and open Layer, whose Layer box takes the number directly — with Bring to front and Send to back buttons beside it that do the arithmetic for you, setting this component just past the highest or lowest layer anything else in the game uses. Or ask Pip to "put the cubes above the cards".
| Field | Values | Default | What it does |
|---|---|---|---|
layer | integer | .optional() (0) | Draw order among overlapping objects in open play and free / scatter zones — higher paints on top, and a lower layer can never rise above a higher one |
Footprint — multi-cell pieces
A footprint lets one component cover several grid cells (a domino, a
polyomino, a 2×2 building). It is { cells: { col, row }[] } — integer
offsets from the anchor cell, which is the cell a placement or move
names. The anchor { col: 0, row: 0 } must be present; the covered cells
are derived at render time, so no runtime state changes. Absent → the
component occupies a single cell.
Behaviors — how a component can be handled
Every card, piece, token and sheet is flippable; every card,
piece and token is rotatable. The physical
table lets you turn any bit over or spin it, so these are not opt-in: a
component flipped to a back with no art shows a generic back (a neutral
felt square with a ring, matching a card’s back), and a rotate with no snap
config cycles freely. The behaviors object only narrows or annotates
these affordances. flippable/rotatable are kept for legacy definitions but
no longer gate anything, so the inspector no longer shows them as checkboxes;
stackable is still an opt-in checkbox.
The die is the one exclusion, and deliberately: a die has N numbered faces, not two sides, so it offers Roll instead of Flip and is never rotated (its result, not its orientation, is what it carries). A sheet is the mirror case — it flips, so a player can turn a filled-in pad over to hide it, but there is nothing to gain from turning a sheet of paper sideways, so it offers no Rotate.
| Field | Values | Default | What it does |
|---|---|---|---|
flippable | true | .optional() | Legacy flag. Every card/piece/token/sheet is flippable regardless — a die never is — so this no longer gates flipping |
rotatable | true | .optional() | Legacy flag. Every card/piece/token is rotatable regardless — this no longer gates rotation |
stackable | true | .optional() | Copies can be stacked into a pile |
rotationStep | number > 0 (degrees) | .optional() | The rotation snap increment (e.g. 90 → four orientations). Absent → free rotation. Ignored whenever orientations is present |
orientations | array of angles in degrees | .optional() | Explicit allowed angles (e.g. [0, 90, 180, 270]). When present this is the full set of legal angles and overrides rotationStep. Each entry is wrapped into [0, 360) and de-duplicated |
States — multi-state components
A states array gives one component 2+ switchable states, each with its
own art and properties — a tile that upgrades village → city → metropolis, a
creature that transforms, a board with several printed sides. The runtime
object remembers which state is active and a player flips between them with
the switch state action; the renderer draws the active state’s face.
The first state is the default. This is not the same as a card’s
variants, which expand into separate cards — states keep one object whose
face changes. Absent → a single-state component, exactly as before.
| Field | Values | Default | What it does |
|---|---|---|---|
states[].id | text (snake_case) | — | Unique state identifier within this component |
states[].name | text | .optional() | Display name for the state |
states[].faces | { front, back } bindings | .optional() | Art for this state; falls back to the component’s own faces |
states[].properties | key/value record | .optional() | Property overrides applied while this state is active |
Attachable — pinned markers
attachable: true lets a component be pinned onto another object instance
— a +1/+1 counter on a creature, a damage marker on a card. A pinned marker
follows its host when it moves and is removed or captured with it; the engine
handles that ripple, and the renderer draws the marker as an overlay on the
host. Absent → not attachable.
| Field | Values | Default | What it does |
|---|---|---|---|
attachable | true | .optional() | This component may be pinned onto another object instance |
Attach slots — named positions on a host
When a host component declares attachSlots, markers that attach to it can
target a specific slot instead of the generic anchor (corner / center / edge).
Each slot has fractional x / y coordinates within the host's bounding box
(0,0 = top-left, 1,1 = bottom-right), an optional label, an accepts filter,
and a capacity (default 1). The renderer draws the marker centred at the
slot's position. When no slotId is provided on attach, the engine auto-assigns
the first slot with available capacity.
| Field | Values | Default | What it does |
|---|---|---|---|
attachSlots[].id | text (snake_case) | — | Unique slot identifier within this component |
attachSlots[].label | text | .optional() | Display name shown on hover |
attachSlots[].x | 0–1 | — | Horizontal position as a fraction of the host width |
attachSlots[].y | 0–1 | — | Vertical position as a fraction of the host height |
attachSlots[].accepts | array of kinds | .optional() | Which component kinds may attach here (omit = any) |
attachSlots[].capacity | integer > 0 | 1 | Maximum markers this slot holds |
How attached markers lay out — attachDisplay
Set on the host (the piece being attached to), this decides what several markers on one host look like:
stack(default) — identical markers collapse into a single chit with a×Ncount badge. Right for counters where only the TOTAL matters: three +1/+1 counters on a creature read as one chit showing ×3. Dragging that chit takes one marker off, not the whole pile, so a player can spend a counter at a time.free— every marker stays exactly where the player dropped it, stored as a fractionalattachOffseton the object. Right when a marker's POSITION on the piece carries meaning — beetles placed on particular corners of a field card, damage pips on a specific ship section.underlay— each attachment is drawn beneath the host at full size, keeping its own rotation and face, so only the part that overhangs is visible. This is a physical tuck: a card slid under a tableau card so a scoring strip pokes out, a wonder stage under a wonder board, a card installed under an ice. Likefreeit positions each attachment individually — an underlay cannot collapse into a×Nchit, which is the whole point of it.
Tucking at the table
Players tuck by holding Alt (Option) while dragging one object onto another, or from the object menu — Tuck under…, with Un-tuck on the tucked object afterwards. Tab cycles the drop mode mid-drag — and, over a space with landing rules, the rules themselves — the drag ghost shows which mode is armed, and a live preview of the tuck appears under the host before you let go.
The modifier is required when the host is a card, and deliberately so: without it, dropping a card onto an occupied tableau cell could never be told apart from tucking it underneath. A drop with no modifier always means what it always has.
A tuck is an ordinary attachment underneath, so it follows its host when the host moves, and is captured or removed along with it.
A face-up underlay is public. Only a strip of it shows on screen, but the card is face-up in a public zone, so its identity is visible to every player exactly as any other face-up card would be. Tuck face-down when the tucked card is meant to be secret — that redacts properly, and opponents see only that something is tucked there, at its real size, showing its back.
A named attachSlots entry always wins over any of the three modes: a marker
that lands in a slot sits at the slot’s coordinates regardless of
attachDisplay.
Free placement has no capacity limit — a host holds as many markers as players drop on it. (Slots do cap, but overflow still attaches; it falls back to the anchor rather than being refused.)
| Field | Values | Default | What it does |
|---|---|---|---|
attachDisplay | stack | free | underlay | stack | How things attached to this component are positioned — pinned on top as a counted chit, kept where they were dropped, or tucked underneath at full size |
Value binding — a live number on a component
A valueBinding renders a runtime number on the component — a life total, a
money count, a dial. The renderer reads key from either global or the
owning player’s state. As with every taybl field, the bounds are advisory:
min / max / step shape the spinner widget but the engine does not
enforce them. Absent → no number is drawn.
| Field | Values | Default | What it does |
|---|---|---|---|
scope | global player | — | Whether the number lives on the table or on the owning player |
key | text | — | Which state value to read and display |
display | number dial | .optional() | A numeric badge (default) or a dial widget |
min | number | .optional() | Display-only lower bound for the spinner — not enforced |
max | number | .optional() | Display-only upper bound for the spinner — not enforced |
step | number > 0 | .optional() | Spinner increment size — display hint only |
Stat block — per-instance properties a player adjusts
Where a valueBinding shows one table- or player-scoped number, a
statBlock gives a component an ordered set of per-instance properties — a
creature card with HP, MP, Attack, and Defense, a relic with Charges and an
Element. Each declared property’s initial is seeded onto every copy when it
is placed and is also the default the play menu resets to; from then on
each copy tracks its own value: damage one Hero and the others are
untouched. Every property has a type — a number stepper, a bar (with a
max ceiling and a fill), a short text value, or an on/off toggle — and
an optional icon. Pinned properties show inline on a frosted strip welded
to the piece’s art (capped at the table’s statStripMax); the rest collapse
into a +N pill that opens the stat menu. Players can also add an ad-hoc
property (say crit) to a single card at the table. Values live in the
object’s property bag, so a face-down or hidden card shows nothing. Edit
properties in the Layout tab’s Properties inspector section. As
everywhere, min / max / step are display hints — the engine never clamps
a value. Absent → no properties.
A statBlock does double duty on a sheet (kind: sheet): a
sheet holds no values of its own, so every blank on the printed pad is one of
these declared properties, and the sheet field only says how they are laid out
in rows and columns. That is why the Studio’s sheet builder writes both lists at
once — a number field becomes a number cell, a toggle a checkbox.
| Field | Values | Default | What it does |
|---|---|---|---|
statBlock[].key | text | — | The property key this value lives under |
statBlock[].label | text | .optional() | Badge label; absent → the key |
statBlock[].type | number bar text toggle | number | Value kind + control shape |
statBlock[].icon | text | .optional() | Curated glyph key; unknown → 2-letter badge |
statBlock[].pinned | bool | .optional() | Show inline on the piece (capped at statStripMax) |
statBlock[].initial | number/text/bool | .optional() | Value seeded onto each copy; the reset default |
statBlock[].min | number | .optional() | Display-only lower bound — not enforced |
statBlock[].max | number | .optional() | Display-only upper bound / bar ceiling — not enforced |
statBlock[].step | number > 0 | 1 | −/+ increment size — display hint only |
statBlock[].display | number dial | .optional() | A numeric badge (default) or a dial |
Related
- Components overview — the shared fields and kinds
- Sheets — a printed pad whose blanks are
statBlockproperties - Tokens — the most common home for a
valueBindingcounter - Pieces — meeple silhouettes via
shape: polygon - Grids — the cells a
footprintcovers