Scaling & responsiveness
GUI scale tokens, container-adaptive scaling, and the adaptation strategy across viewport sizes.
Three adaptation tiers
Different component tiers adapt to different sizes differently:
| Tier | Strategy |
|---|---|
| L3 fixed geometry (container screens / HUD / menus) | Uniform scaling only — never re-layouted, never stretched (spec §6.4 vanilla-coordinate invariant) |
| L1 / L2 primitives & containers (Button, Input, Card, Panel…) | Fluid: nine-slice / tiled textures stretch naturally; width follows the container |
| Presentation (docs site / playground) | ScaledBox picks the scale automatically from the container width |
Global scale
Override --mc-scale on :root (see Theming):
:root {
--mc-scale: 3;
}
--mc-gui, --spacing, gui() and nine-slice borders all follow. Layout coordinates remain vanilla GUI px values; no component code changes. Vanilla GUI scale is integer — fractional factors blur the pixel font and textures. Stick to 1 / 2 / 3 / 4.
Container-adaptive: ScaledBox
ScaledBox measures its own container and picks the scaling mode:
import { ScaledBox } from "@tennantite/ui";
// integer mode (default): writes the largest integer step into --mc-scale,
// the whole subtree re-layouts — in-game "GUI Scale: Auto", crispest pixels
<ScaledBox width={176}>
<ChestScreen rows={3} />
</ScaledBox>
// fit mode: transform-scales to fill a fixed-height frame (marketing
// previews); content renders at --mc-scale: 1
<ScaledBox className="h-[340px]" height={166} mode="fit" width={176}>
<InventoryScreen />
</ScaledBox>
width/height: intrinsic content size (GUI px). Omit them to measure the rendered content (needs browser layout; SSR renders at step 1 and corrects after hydration).maxScale: scale cap; floored for integer mode (fractional allowed for fit), default 2.align: horizontal alignment of the content in integer mode, centered by default.- ScaledBox never goes below step 1. Containers narrower than that decide themselves whether to clip or scroll — better than deformation.
Manual --mc-scale overrides need mc-scale-scope
CSS custom properties resolve their var() references at the declaring element
and inherit the resolved value: overriding --mc-scale on a subtree does not
re-resolve --mc-gui / --spacing. When scaling locally, pair the override
with the mc-scale-scope utility from the tokens package:
<div className="mc-scale-scope" style={{ "--mc-scale": 1 }}>
<ChestScreen rows={3} />
</div>ScaledBox handles this for you; app-wide scaling via :root needs no extra care.
Boundaries
- Portal popups: Dialog / Select / Tooltip popups mount on
bodyand do not follow the subtree--mc-scale. Scaled contexts should keep popups closed (e.g.inertpreviews). - SSR:
ScaledBoxnever touches browser APIs during render; the server emits step 1 and the measurement corrects after hydration (spec §1.1). - Textures & text: integer steps stay pixel-crisp; fit mode’s continuous scaling softens pixels slightly — reserve it for marketing slots.