Frosted card
No shape glyph · no P chip · hover for actions
Extra lines to demonstrate maxHeight + scroll on the card.
Line three · line four · line five.
LIBRARY DOCS · v0.2.17
Sectioned guides for Block, ShapeFrame, TetrisProvider, viewports, sizeAt / shapeAt / layoutAt media mutations, size="auto", accentPlacement, hover overlays, entrance animations, every tetromino — plus a live playground and API tables.
Getting started
TetrisProvider across desktop, tablet, and mobile tracks — mutate footprints with sizeAt and silhouettes with shapeAt, reflow ShapeFrame cells with layoutAt, fill leftover columns with size="auto", add entrance motion with animation, or compose literal tetrominos with ShapeFrame.Getting started
Package
npm install @stack_layout/tetris-uiQuick start
import { Block, ShapeFrame, TetrisProvider } from "@stack_layout/tetris-ui";import "@stack_layout/tetris-ui/styles.css";
<TetrisProvider viewport="auto" gap={20} stretch> <Block title="Revenue" priority={95} size="enlarge" sizeAt={{ tablet: "md", mobile: "sm" }} accent="#0F766E" accentPlacement="left" hover={{ background: "rgba(15, 118, 110, 0.2)", content: <span>Open report</span>, }} minHeight={160} maxHeight={280} scroll > $128k </Block>
<Block title="Feed" priority={70} size="auto" accent="none" /></TetrisProvider>
// ShapeFrame — silhouette on desktop · stack cards on mobile<ShapeFrame shape="T" layout="silhouette" layoutAt={{ mobile: "stack", tablet: "carousel" }} voids="invisible" glass backdrop={false} accent="none"> <span>A</span> <span>B</span> <span>C</span> <span>D</span></ShapeFrame>Output
No shape glyph · no P chip · hover for actions
Extra lines to demonstrate maxHeight + scroll on the card.
Line three · line four · line five.
Component
shape / shapeAt. Prefer Tile for freeform boards that do not need shape glyphs.Freeform Tile · accent + hover (no shape)
<Tile title="Revenue" size="enlarge" sizeAt={{ tablet: "md", mobile: "sm" }} priority={95} accent="#0F766E" accentPlacement="left" hover={{ background: "rgba(15, 118, 110, 0.2)", content: <span>Open</span>, }}> $128k</Tile>Output
$128k
Hover · left hairline · title optional
Component
layout / layoutAt (stack / carousel). Empty bounding cells stay void in silhouette mode — control them with voids.L silhouette · stack on mobile
<ShapeFrame shape="L" layout="silhouette" layoutAt={{ mobile: "stack", tablet: "carousel" }} voids="invisible" glass border="none" gap={8} size="sm" accent="none"> <span>A</span> <span>B</span> <span>C</span> <span>Foot</span></ShapeFrame>Output
Component
Tile children (no shape API). Reads size / priority and assigns CSS grid placement. Use Block when you need shape glyphs.Provider · Tile + auto fill + stretch
<TetrisProvider viewport="desktop" gap={16} stretch> <Tile title="Hero" size="enlarge" priority={95} accent="#0F766E" accentPlacement="left" /> <Tile size="auto" priority={70} accent="none">Feed</Tile></TetrisProvider>Output
Packs first
auto fills leftover · no title
Spatial engine
priority · 0–100
Think of priority as boarding order for a plane. P95 boards before P25. The engine sorts blocks descending and places them one by one into the first free footprint that fits.
90–100
Mission-critical
Revenue, checkout, alerts
70–89
Primary UI
Feeds, banners, main nav
40–69
Supporting
Tasks, team, messages
0–39
Ambient
Weather, tips, décor
Higher priority packs first
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function PriorityBoard() { return ( <TetrisProvider viewport="desktop" stretch gap={16}> <Block title="Revenue" priority={95} size="enlarge" showPriority accent="#0F766E" accentPlacement="left" /> <Block title="Banner" priority={88} size="full" showPriority accent="#0369A1" /> <Block title="Team feed" priority={70} size="md" showPriority accent="#334155" /> <Block title="Weather" priority={25} size="sm" showPriority accent="none" /> </TetrisProvider> );}Output
Must stay visible. Packs first — claims the best top-left real estate.
size enlarge
Wide strip. High priority so it still lands early on a full row.
size full
Important, but yields to Revenue. Fills the next open gap.
size md
Nice-to-have. On mobile it often drops lower or squeezes last.
size sm
stretch so cards that share a row match the tallest content height, and gap for spacing between them. Without stretch, each card height fits its own content.cols · size · height
The packer sees a CSS grid of columns (12 desktop / 8 tablet / 4 mobile). Each block asks for a column span — size tokens are shorthand for that width. Card height grows with its content unless you set minHeight / maxHeight.
| size | column span | min cols | Feels like |
|---|---|---|---|
| sm | 2 | 2 | Chip / ambient widget |
| md | 3 | 2 | Default module |
| lg | 4 | 3 | Rich content panel |
| enlarge | 6 | 4 | Hero metric or chart |
| full | 12 | 4 | Full-width banner strip |
| auto | 0 | 1 | Fills leftover columns · even split with peer autos |
Size maps to column span; height is content
// size → preferred column span (height fits content)size = "sm"; // 2 cols — chip, weather, badge clustersize = "md"; // 3 cols — default cardsize = "lg"; // 4 cols — rich panelsize = "enlarge"; // 6 cols — hero metric / chartsize = "full"; // 12 cols — banner across the tracksize = "auto"; // fills leftover columns (even split when several are auto)
// Height bounds are optional<Block minHeight={160} maxHeight={320} scroll>…</Block>
// Fill the row: fixed + flexible<Block size="sm" /><Block size="md" /><Block size="auto" /> // takes remaining cols on the track
// Conceptual packer inputconst preferred = { cols: 4, rows: 1 };const min = { cols: 2, rows: 1 };const max = { cols: 6, rows: 1 };viewport="desktop"
12 columns
viewport="tablet"
8 columns
viewport="mobile"
4 columns
Full interactive demos for each track → Viewports.
min · max · resolve
Preferred is the dream column span. Min is the smallest usable width. Max caps greed. When the track is narrow, the engine clamps columns: cols = clamp(preferred, min…max) ∩ viewport. Height still fits content unless you set minHeight / maxHeight on the Block.
preferred
Ideal column span when space is plentiful. Mapped from size tokens in the library.
min
Never narrower than this. Protects readability — a chart that can’t go below 2 cols.
max
Never wider than this. Stops a low-priority widget from swallowing the board.
Clamp preferred into the viewport track
function resolveSize(preferred, min, max, columns) { const cols = Math.min( columns, Math.max(min.cols, Math.min(preferred.cols, max.cols)), ); const rows = Math.max(min.rows, Math.min(preferred.rows, max.rows)); return { cols, rows };}
// Example: enlarge on mobile (4 cols)// preferred 6 cols → clamped to 4 cols (fits the track, respects min)Live clamp across viewports
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function FootprintDemo() { return ( <TetrisProvider viewport="desktop"> <Block title="Hero metric" priority={94} size="enlarge" accent="#0F766E" accentPlacement="left" /> <Block title="Side note" priority={40} size="sm" accent="#64748B" /> <Block title="Full banner" priority={80} size="full" accent="#1D4E4A" accentPlacement="bottom" /> </TetrisProvider> );}Output
$128k
Wants 6 cols — shrinks on tablet/mobile without going below min.
Already near min — little room to shrink, so it reflows downward first.
Prefers the full track width; on mobile it still spans all 4 columns.
shape · I O T L S J Z
Shape tells humans (and your product language) what kind of module this is. Inside TetrisProvider it also tags the block for docs, glyphs, and ShapeFrame silhouettes. Pick the silhouette that matches the relationship between parts.
I
Banners, steppers, timelines
One long run — sequential story.
O
KPIs, avatars, stats
Balanced square — equal weight.
T
Nav + feature stage
Crown of three, stem of focus.
L
Feed + context foot
Stack with a detail footing.
S
Offset chart / media
Staggered pairs — never boring.
J
Rail + nested panel
Sidebar with a leftward base.
Z
Alerts, status strips
Mirror stagger — urgency grammar.
Shape as meaning
import { Block, ShapeFrame } from "@stack_layout/tetris-ui";
// Shape as meaning on packed modules<Block shape="O" size="enlarge" priority={95} title="Revenue" /><Block shape="I" size="full" priority={88} title="Onboarding" /><Block shape="L" size="md" priority={70} title="Activity" />
// Literal tetromino — stack cards on mobile, carousel on tablet<ShapeFrame shape="T" layout="silhouette" layoutAt={{ mobile: "stack", tablet: "carousel" }} size="md" voids="invisible"> <NavItem /> <NavItem /> <NavItem /> <FeaturePanel /></ShapeFrame>Output
freeform · auto fill
Declare rules — never gridColumn / gridRow. Prefer Tile inside TetrisProvider. The freeform lab is a full sample dashboard (KPIs, chart, avatars, activity, image cards, tasks). glass="regular" is opaque; frosted/transparent are glass. Scroll moves body content only. accentPlacement, hover, titles, and glass are toggleable.
01 · Wrap in TetrisProvider
Pass gap (and optional stretch). The packer writes placement for you.
02 · Mix fixed + auto sizes
sm / md / enlarge claim fixed spans. auto fills what is left — or even-splits when every card in the band is auto.
03 · Priority orders the pack
Give metric autos a higher priority than the hero so the top row packs first, then enlarge + auto on the next band.
04 · Stretch vs custom height
stretch = shared row height from content. Or set minHeight / maxHeight per Tile for explicit bounds.
Live on /system under Freeform stretch and Freeform auto + height — same board as below.
Viewport
accentPlacement
border
radius
glass
hover type
Hover background
Sample ops dashboard — metrics, chart, avatars, activity, image cards, and tasks on Tile. border="none" + accent = accent-only edges; glass="frosted" uses real backdrop blur.
vs last week
2,739
+4.2%
vs last week
87%
+1.1%
vs last week
$1.28M
+8.4%
7-day rolling · live
Network feed
Census sync completed for Ward B
syncPriya shared Q3 occupancy forecast
shareNew transfer request · ICU → Step-down
alertStaffing model recalibrated (+2 nights)
opsEast wing imaging suite went live
launchClinical leadership
Amara Okafor
Chief of Staff
Jonas Berg
Ops Lead
Priya Shah
Analytics
Leo Mensah
Floor Charge
Imaging · 12 bays
Imaging · 12 bays
Today
Outpatient · live
Outpatient · live
Needs attention
2 pharmacy SKUs below par
OR-3 delayed 25m
Broadcast
Pin a channel to the bed board — leftover width from size="auto".
size=auto fills leftover · stretch equalizes row height
import { Tile, TetrisProvider } from "@stack_layout/tetris-ui";import "@stack_layout/tetris-ui/styles.css";
<TetrisProvider viewport="desktop" gap={16} stretch> <Tile title="Patients" size="auto" priority={96} glass="frosted" accent={"#0F766E"} accentPlacement="top"> {/* KPI + sparkline */} </Tile> <Tile title="Occupancy" size="auto" priority={95} glass="frosted" accent={"#0369A1"} accentPlacement="left" /> <Tile title="Revenue" size="auto" priority={94} glass="frosted" accent={"#7C3AED"} accentPlacement="all" /> <Tile title="Admissions pulse" size="enlarge" priority={88} glass="frosted" accent={"#0F766E"}> {/* chart */} </Tile> <Tile title="Activity" size="auto" priority={87} accent={"#B45309"} accentPlacement="left"> {/* feed list */} </Tile> <Tile title="On duty" size="md" priority={80}>{/* avatars */}</Tile> <Tile title="East Wing" size="auto" priority={78}>{/* image card */}</Tile> <Tile title="Open tasks" size="auto" priority={76}>{/* checklist */}</Tile></TetrisProvider>size="auto" expands into free columns. stretch equalizes heights in a row; omit it and set minHeight / maxHeight when you want explicit card heights.TetrisProvider · viewport
viewport sets the track width the packer optimizes for. Same blocks, different column counts — low-priority modules yield first as the board narrows. Use sizeAt on each Block to shrink (or grow) footprints per track, shapeAt to mutate silhouettes (media mutation), and viewport="auto" if you want the library to follow the window width.Active track: 12 cols · desktop
Same blocks, three tracks
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function ViewportBoard() { return ( <TetrisProvider viewport="desktop" gap={16} stretch> <Block title="Revenue" priority={95} size="enlarge" sizeAt={{ tablet: "md", mobile: "sm" }} shape="O" /> <Block title="Feed" priority={70} size="md" shape="L" /> <Block title="Alerts" priority={80} size="sm" shape="Z" /> <Block title="Weather" priority={25} size="sm" shape="S" /> </TetrisProvider> );}Output
$128k
enlarge → enlarge
Yields to Revenue on narrow tracks
High urgency
Ambient · drops first on mobile
| viewport | columns | Typical use |
|---|---|---|
| desktop | 12 | Full dashboards, side-by-side modules |
| tablet | 8 | Condensed boards, fewer columns per row |
| mobile | 4 | Stack-first; low priority reclaim space |
| auto | follow window | matchMedia: ≤767 mobile · ≤1023 tablet · else desktop |
Keep a base size and override per track. Omitted keys fall back to size. The packer uses the resolved size (not CSS alone), so grid span actually changes. Affects Block inside TetrisProvider.
enlarge on desktop · md on tablet · sm on mobile
<TetrisProvider viewport="desktop" gap={16} stretch> <Block title="Hero metric" size="enlarge" sizeAt={{ tablet: "md", mobile: "sm" }} priority={95} /> <Block title="Sidebar" size="md" priority={60} /></TetrisProvider>
// Or let the window pick the track:<TetrisProvider viewport="auto" breakpoints={{ mobileMax: 767, tabletMax: 1023 }}> …</TetrisProvider>Output
Active size: enlarge
No sizeAt · always md
Block · ShapeFrame · media
sizeAt swaps footprints. Keep a base shape; override with shapeAt. Omitted keys fall back to the base. Use it when desktop wants an S stagger and mobile should collapse to a solid O.Active track: desktop · resolved shape: S
S on desktop · T on tablet · O on mobile
import { Block, ShapeFrame, TetrisProvider } from "@stack_layout/tetris-ui";
// Block — glyph + data-shape follow the packer track<TetrisProvider viewport="desktop" gap={16}> <Block title="Hero" shape="S" shapeAt={{ tablet: "T", mobile: "O" }} size="enlarge" sizeAt={{ mobile: "md" }} priority={90} /></TetrisProvider>
// ShapeFrame — silhouette mutates; combine with layoutAt for stack / carousel<ShapeFrame shape="S" shapeAt={{ tablet: "T", mobile: "O" }} layout="silhouette" layoutAt={{ mobile: "stack" }} viewport="desktop" voids="invisible" size="sm"> <span>A</span> <span>B</span> <span>C</span> <span>D</span></ShapeFrame>
// Window-driven (no manual viewport chips):<TetrisProvider viewport="auto"> <Block shape="S" shapeAt={{ mobile: "O" }} … /></TetrisProvider>Output
Base S · active S
No shapeAt · always O
ShapeFrame · cells A–D
Block
Resolves inside TetrisProvider — updates the glyph, footer meta, and data-shape. Packing footprint still comes from size / sizeAt.
ShapeFrame
Rebuilds the tetromino grid. Nested under a provider, it inherits the active track; standalone, pass viewport (defaults to "auto" when shapeAt is set).
ShapeFrame · responsive flow
layout sets the base mode; layoutAt overrides per track (same pattern as sizeAt / shapeAt). Cells always follow child index order 0→3 — voids are skipped in stack and carousel.Resolved layout: silhouette · track desktop
silhouette
Literal tetromino grid. Voids respected. Best for desktop dashboards that teach the shape.
stack
One full-width column of cards. Ideal for phones — each cell reads as a stacked row card.
carousel
Horizontal scroll-snap. Swipe between cells. Use carouselSnap="page" (full slide) or "cell" (peek).
T silhouette · stack on mobile · carousel on tablet
import { ShapeFrame } from "@stack_layout/tetris-ui";
<ShapeFrame shape="T" layout="silhouette" layoutAt={{ mobile: "stack", tablet: "carousel" }} viewport="desktop" voids="invisible" size="sm" gap={12} carouselSnap="page"> <span>Nav 1</span> {/* index 0 */} <span>Nav 2</span> {/* index 1 */} <span>Nav 3</span> {/* index 2 */} <span>Feature</span>{/* index 3 — T stem */}</ShapeFrame>
// Window-driven (no chips):<ShapeFrame shape="T" layout="silhouette" layoutAt={{ mobile: "stack", tablet: "carousel" }} voids="invisible"> …</ShapeFrame>Output
ShapeFrame · T · silhouette
Index order
Stack and carousel ignore grid coordinates — they flatten cells in child order 0→3. For a T, that is crown-left, crown-center, crown-right, then stem. Reorder children if you want a different reading order on mobile.
With shapeAt
You can combine both: keep layoutAt for flow and shapeAt if you also need a different silhouette before stacking. Most UIs only need layoutAt.
Block · ShapeFrame
accent + accentPlacement, and optional hover overlays. Shape glyphs and priority chips stay hidden unless you set showShape / showPriority.color tints the solid or glass fill. Default is rgba(255, 255, 255, 0.72). Pair with glass (default true) for the frosted look.
Default glass white · custom tint
<Block title="Default" /> {/* glass + glass white */}<Block title="Sky" color="rgba(14, 165, 233, 0.22)" /><Block title="Solid" glass={false} color="#ffffff" />
<ShapeFrame shape="O" color="rgba(197, 230, 58, 0.28)"> <span>A</span><span>B</span><span>C</span><span>D</span></ShapeFrame>Output
glass white
tinted glass
opaque white
Pass animation on Block or ShapeFrame. puzzle-entrance and lego-drop-in stagger per ShapeFrame cell. Respects prefers-reduced-motion.
glass makes the fill translucent. backdrop (default true) controls blur. Affects Block and ShapeFrame.
Glass with optional blur
<Block glass backdrop={true} border="all" title="Frost"> Translucent + blur</Block>
<ShapeFrame shape="O" glass backdrop={true} size="sm"> <span>A</span><span>B</span><span>C</span><span>D</span></ShapeFrame>Output
Glass + backdrop blur
accent sets the color (or "none"). accentPlacement chooses which sides get the classic hairline — drawn as a radius-aware ring so rounded corners stay continuous: top, right, bottom, left, all (full round), or none. Title / subtitle stay optional.
Accent color + side
<Block title="With accent" accent="#0F766E" accentPlacement="top"/><Block title="Clean card" accent="none" />Output
Edge · top
Always accent="none"
Opt-in with hover. Pass true for a subtle wash, or an object with background and custom content.
Custom hover content
<Block title="Patients" size="auto" accent="#0F766E" accentPlacement="left" hover={{ background: "rgba(15, 118, 110, 0.22)", content: <button type="button">Open chart</button>, }}> 2,739</Block>Output
2,739
Hover this card
Optional height bounds (number → px, or any CSS length) and overflow scrolling. Omit both to let the card height fit its content. Affects Block.
Bounded height + scroll
<Block title="Notes" minHeight={160} maxHeight={200} scroll accent="none"> {/* long body… */}</Block>Output
Space between silhouette cells (px). Affects ShapeFrame only — board spacing uses TetrisProvider gap.
Cell gap inside the silhouette
<ShapeFrame shape="T" layout="silhouette" layoutAt={{ mobile: "stack" }} gap={6} voids="invisible" size="sm"> <span>A</span><span>B</span><span>C</span><span>D</span></ShapeFrame>Output
Merge your own classes and inline styles on Block, ShapeFrame, and TetrisProvider.
Bring your own styles
<Block className="ring-1 ring-black/5" style={{ boxShadow: "0 8px 24px rgba(0,0,0,0.06)" }} title="Custom"/>
<TetrisProvider className="my-board" gap={20}> …</TetrisProvider>Output
className + style on the card shell
Try it · Block · ShapeFrame
puzzle-entrance and lego-drop-in stagger per shape cell.Animation type
fade-up — Rise into place
Block · card
animation="fade-up"
Cards animate as one shell. Use this for packed modules and stand-alone content blocks.
ShapeFrame · silhouette
Puzzle and lego stagger cells A→D. Other types animate the whole frame.
Copy the prop
import { Block, ShapeFrame } from "@stack_layout/tetris-ui";
<Block title="Studio card" animation="fade-up" accent="#0F766E"> Content</Block>
<ShapeFrame shape="T" layout="silhouette" layoutAt={{ mobile: "stack" }} animation="fade-up" voids="invisible"> <span>A</span> <span>B</span> <span>C</span> <span>D</span></ShapeFrame>Interactive
3 cols · accent left
Freeform chrome: no glyph / P chip unless toggled. Hover is opt-in.
Fill
accentPlacement
Color
Animation
Border
Sides
Radius
Size
Shape
ShapeFrame layout
Empty cells (voids)
Priority · 72
minHeight · off
maxHeight · off
Scroll
L
L
L
L
Shape primitives
Block shape to tag meaning on a packed module, or ShapeFrame when four children must occupy the literal tetromino silhouette — voids and all. Each example shows IDE-style source plus a live output.One straight run — sequence over hierarchy
The I is the simplest sentence in the grammar: four units in a line. Use it when the user should move left→right through equal steps, metrics, or filters. Nothing branches. Nothing nests. Continuity is the product.
Anatomy
Bounding box 4×1. All four cells are solid — no voids. In a ShapeFrame, children map 1→2→3→4 along the bar. As a packed Block with size="full", it stretches across the track like a banner.
Reading order
Left → right (or top → bottom if you rotate the mental model vertically).
Packing tip
Pair with size="full" and priority 80–95 for headers and onboarding rails. On mobile the footprint still wants the full 4-column track.
Voids
No empty cells — every slot in the box is solid.
Cell roles (ShapeFrame children 1–4)
Best for
Avoid
Code → output
Packed banner Block
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function OnboardingBanner() { return ( <TetrisProvider viewport="desktop"> <Block shape="I" size="full" priority={90} title="Onboarding" accent="#0F766E" > Discover → Compose → Assemble → Ship </Block> </TetrisProvider> );}Output
Discover → Compose → Assemble → Ship
Literal four-step ShapeFrame
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function ProcessSteps() { return ( <ShapeFrame shape="I" size="lg" surface="bordered" accent="#0F766E"> <Step n={1} label="Discover" /> <Step n={2} label="Compose" /> <Step n={3} label="Assemble" /> <Step n={4} label="Ship" /> </ShapeFrame> );}Output
Discover
Compose
Assemble
Ship
Glass toolbar strip
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function TableToolbar() { return ( <ShapeFrame shape="I" size="md" glass border="bottom" voids="invisible" > <Filter label="Status" /> <Filter label="Owner" /> <Filter label="Range" /> <Filter label="Export" /> </ShapeFrame> );}Output
Perfect balance — four equal weights
The O is a square of trust. No cell outranks another. Use it for metric clusters, avatar grids, and scoreboards where hierarchy would lie. If one number must dominate, bump its Block priority — don’t break the O.
Anatomy
Bounding box 2×2. Four solid cells, no voids. ShapeFrame children fill TL → TR → BL → BR. As a single Block (size enlarge/md), the O reads as one solid module — still “square energy.”
Reading order
Clockwise from top-left, or scan as a 2×2 dashboard tile.
Packing tip
Default hero for Revenue-style modules: shape="O" size="enlarge" priority={95}. On narrow viewports it clamps toward min 2×2 and stays readable.
Voids
No empty cells — every slot in the box is solid.
Cell roles (ShapeFrame children 1–4)
Best for
Avoid
Code → output
Hero metric Block
import { Block } from "@stack_layout/tetris-ui";
export function RevenueHero() { return ( <Block shape="O" size="enlarge" priority={95} surface="glass" border="all" title="Revenue" accent="#B45309" > <p className="text-4xl">$128.4k</p> <p>+12.4% vs last month</p> </Block> );}Output
$128.4k
+12.4% vs last month
2×2 scoreboard Frame
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function Scoreboard() { return ( <ShapeFrame shape="O" size="md" surface="bordered" accent="#B45309"> <Stat label="MRR" value="$42k" /> <Stat label="NRR" value="118%" /> <Stat label="MAU" value="12.4k" /> <Stat label="NPS" value="72" /> </ShapeFrame> );}Output
$42k
118%
12.4k
72
Avatar cluster
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function TeamAvatars() { return ( <ShapeFrame shape="O" size="sm" glass border="none"> <Avatar name="Maya" /> <Avatar name="Jon" /> <Avatar name="Ava" /> <Avatar name="Rae" /> </ShapeFrame> );}Output
Crown of three, stem of focus
The T is how you say “these options crown one stage.” Three peers across the top; the stem below is always the thing that matters — feature panel, canvas, or primary content. If every child is equal, you wanted an O.
Anatomy
Bounding box 3×2 with one void under the left and right crown cells. Occupied: (0,0) (1,0) (2,0) (1,1). Pass voids="invisible" so empty cells disappear, or "ghost" to hint the silhouette.
Reading order
Scan the crown left→right, then drop into the stem — the primary panel.
Packing tip
Great as ShapeFrame for composed UI. As a packed Block, shape="T" tags the grammar while size="md"/"lg" sets the rectangular footprint the packer places.
Voids
T / L / S / J / Z have empty cells in the bounding box. Control them with voids="visible" | "ghost" | "invisible".
Cell roles (ShapeFrame children 1–4)
Best for
Avoid
Code → output
Nav crown + feature stem
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function WorkspaceStage() { return ( <ShapeFrame shape="T" layout="silhouette" layoutAt={{ mobile: "stack", tablet: "carousel" }} size="lg" voids="invisible" glass border="all" accent="#0E7490" > <NavItem label="Overview" /> <NavItem label="Analytics" active /> <NavItem label="Team" /> <FeatureStage title="Live conversion" /> </ShapeFrame> );}Output
Stem
Live conversion
Packed module tagged as T
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function WorkspaceBlock() { return ( <TetrisProvider> <Block shape="T" size="md" priority={75} title="Workspace" subtitle="Nav + stage grammar" accent="#0E7490" > Primary panel sits under the crown. </Block> </TetrisProvider> );}Output
Nav + stage grammar
Primary panel sits under the crown.
Ghost voids — teach the silhouette
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function TSilhouette() { return ( <ShapeFrame shape="T" voids="ghost" size="md" surface="flat"> <span>A</span> <span>B</span> <span>C</span> <span>Stage</span> </ShapeFrame> );}Output
Stack down, then open sideways
The L is a vertical story with a door at the bottom. Feed, history, or tools stack in the column; the foot opens into related detail without leaving the stack. Perfect when context should feel attached, not modal.
Anatomy
Bounding box 2×3. Cells: (0,0) (0,1) (0,2) (1,2). Void cells sit to the right of the upper column. Child order follows the column top→bottom, then the foot.
Reading order
Top → down the spine, then right into the foot.
Packing tip
Use ShapeFrame when the foot must read as literal L. Pair with priority 65–80 for activity modules that matter but yield to Revenue.
Voids
Hide empty cells with voids="invisible" for production UI; use "visible" while designing.
Cell roles (ShapeFrame children 1–4)
Best for
Avoid
Code → output
Feed + related foot
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function ActivityL() { return ( <ShapeFrame shape="L" layout="silhouette" layoutAt={{ mobile: "stack" }} size="lg" voids="invisible" surface="bordered" accent="#A16207" > <FeedItem who="Maya" action="merged" /> <FeedItem who="Jon" action="shipped" /> <FeedItem who="Ava" action="cleared" /> <RelatedPanel title="Invoice #4821" /> </ShapeFrame> );}Output
Maya merged
Jon shipped
Ava cleared
Invoice #4821
Related context foot
Completable task Block (L grammar)
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function VerifyTask() { return ( <TetrisProvider> <Block shape="L" size="md" priority={92} title="Verify email" subtitle="Complete to clear this block" accent="#B45309" > Completing clears the block — neighbors RE-FLOW. </Block> </TetrisProvider> );}Output
Complete to clear this block
Completing clears the block — neighbors RE-FLOW.
Borderless glass stack
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function ToolStack() { return ( <ShapeFrame shape="L" glass border="none" voids="invisible" size="md" > <Tool label="Select" /> <Tool label="Draw" /> <Tool label="Erase" /> <Detail label="Inspector" /> </ShapeFrame> );}Output
Inspector
Properties for selection
Intentional offset — still on-grid
The S refuses the boring rectangle without leaving the grid. Two staggered pairs — chart beside media, before beside after. Use it when comparison or energy matters more than symmetry.
Anatomy
Bounding box 3×2. Cells: (1,0) (2,0) (0,1) (1,1). Voids at (0,0) and (2,1). Children fill top-right pair, then bottom-left pair.
Reading order
Top staggered pair, then the offset pair below — a gentle zigzag.
Packing tip
Ideal ShapeFrame for galleries and analytics pairings. As a Block, size="sm"/"md" keeps ambient S modules from dominating priority.
Voids
Empty corners are part of the silhouette — set voids="invisible" so layout doesn’t show blank cards.
Cell roles (ShapeFrame children 1–4)
Best for
Avoid
Code → output
Chart + media stagger
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function AnalyticsS() { return ( <ShapeFrame shape="S" size="lg" voids="invisible" radius="soft" accent="#3F6212" > <Chart title="Funnel" value="68%" /> <Chart title="Cohort" value="41%" /> <Media title="Cover" /> <Media title="Clip" /> </ShapeFrame> );}Output
68%
41%
Cover
Clip
Packed ambient widget
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function WeatherWidget() { return ( <TetrisProvider> <Block shape="S" size="sm" priority={30} title="Weather" accent="#64748B" > 72° · Partly cloudy </Block> </TetrisProvider> );}Output
72°
Partly cloudy
Before / after
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function CompareS() { return ( <ShapeFrame shape="S" voids="ghost" surface="flat" size="md"> <Before label="Draft" /> <BeforeMeta label="v1" /> <After label="Shipped" /> <AfterMeta label="v2" /> </ShapeFrame> );}Output
Draft
v1
Shipped
v2
Rail first — panel opens left
The J mirrors the L: a vertical rail with a base that opens leftward. Inbox, boards, archives — anything where the spine lives on the right and nested detail unfolds from the foot. If your primary reading flow needs a left spine, pick L instead.
Anatomy
Bounding box 2×3. Cells: (1,0) (1,1) (1,2) (0,2). Void on the upper left. Children run down the right rail, then into the leftward base panel.
Reading order
Down the right rail, then left into the nested panel.
Packing tip
ShapeFrame owns the silhouette. Priority 60–75 suits supporting rails that should not steal space from P90+ heroes.
Voids
Same void controls as L/T — prefer invisible voids in product UI.
Cell roles (ShapeFrame children 1–4)
Best for
Avoid
Code → output
Inbox rail + thread
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function InboxJ() { return ( <ShapeFrame shape="J" size="enlarge" voids="invisible" surface="bordered" accent="#475569" > <RailItem label="Inbox" count={12} /> <RailItem label="Drafts" count={3} /> <RailItem label="Archive" count={90} /> <ThreadPanel subject="Layout rules for Q3" /> </ShapeFrame> );}Output
Layout rules for Q3
Thread opens left from the rail
Packed notifications module
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function Notifications() { return ( <TetrisProvider> <Block shape="J" size="md" priority={65} title="Notifications" subtitle="3 unread" accent="#475569" > Priority reflow ready · New shape: J-block </Block> </TetrisProvider> );}Output
3 unread
Glass drawer
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function ToolDrawer() { return ( <ShapeFrame shape="J" glass border="left" voids="invisible" size="md" > <Tool label="Layers" /> <Tool label="Assets" /> <Tool label="Export" /> <DrawerBody title="Inspector" /> </ShapeFrame> );}Output
Inspector
Drawer body from the J base
Diagonal tension — signal, not square
The Z is the alarm grammar. Staggered like S but mirrored — use it when the UI should feel like a signal: critical → warning, degraded → recovering. Calm dashboards should stay on O; urgency belongs on Z.
Anatomy
Bounding box 3×2. Cells: (0,0) (1,0) (1,1) (2,1). Voids at (2,0) and (0,1). Children fill the top-left pair, then the bottom-right pair — a sharp zigzag.
Reading order
Top-left pair → jump diagonally to bottom-right pair.
Packing tip
Give alerts high priority (80+) even with size="sm" so they pack early. Shape tags the urgency; priority keeps them on-screen when the viewport shrinks.
Voids
Invisible voids keep the zigzag tight; ghost voids help during design critiques.
Cell roles (ShapeFrame children 1–4)
Best for
Avoid
Code → output
Status zigzag
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function IncidentZ() { return ( <ShapeFrame shape="Z" size="md" voids="invisible" surface="flat" accent="#9F1239" > <Alert level="critical" count={2} /> <Alert level="warning" count={5} /> <Status label="Degraded" /> <Status label="Recovering" /> </ShapeFrame> );}Output
2
5
Degraded
Recovering
High-priority packed signal
import { Block, TetrisProvider } from "@stack_layout/tetris-ui";
export function IncidentsBlock() { return ( <TetrisProvider> <Block shape="Z" size="sm" priority={88} title="Incidents" accent="#9F1239" > 2 critical · page on-call </Block> </TetrisProvider> );}Output
2 critical · page on-call
Glass urgency strip
import { ShapeFrame } from "@stack_layout/tetris-ui";
export function SignalStrip() { return ( <ShapeFrame shape="Z" glass border={["top", "bottom"]} voids="invisible" > <Signal label="CPU" /> <Signal label="Mem" /> <Signal label="Err" /> <Signal label="Lat" /> </ShapeFrame> );}Output
84%
61%
0.2%
42ms
Reference
Single content module — alone or inside TetrisProvider.
| Prop | Values | Notes |
|---|---|---|
| glass | frosted | transparent | regular | boolean | frosted/transparent = glass; regular = opaque solid (true→frosted, false→regular) |
| backdrop | boolean | Legacy blur toggle when glass is frosted-capable — prefer glass="transparent" |
| color | CSS color | Fill tint — default glass white rgba(255,255,255,0.72) |
| animation | fade-in | fade-up | fade-bottom | slide-in | slide-up | slide | puzzle-entrance | lego-drop-in | false | Entrance motion on the card shell |
| border | none | false | all | true | side | side[] | Outline. none/false = no outline (accent-only when accent is set). Omitted + accent on → none |
| borderless / bordered | boolean (legacy) | Maps to border="none" / border="all" |
| surface | preset | features object | glass | bordered | borderless | flat |
| radius | flat | sharp | rounded | soft | Corner language — flat/sharp = square; rounded/soft = curved |
| size | sm | md | lg | enlarge | full | auto | Column span — auto fills leftover cols (even split with peer autos) |
| sizeAt | { desktop?, tablet?, mobile? } | Per-viewport size overrides for the packer |
| priority | 0–100 | Packer order — hidden unless showPriority |
| shape | I O T L S J Z | Optional glyph via showShape (off by default) |
| shapeAt | { desktop?, tablet?, mobile? } | Per-viewport shape overrides (glyph + data-shape) |
| accent | CSS color | "none" | false | Accent color; none disables |
| accentPlacement | top | right | bottom | left | all | full | none | Hairline ring follows border-radius (default top); all/full = all sides |
| hover | true | { type?: regular|frosted, background?, content?, className? } | Overlay — does not block scroll; does not center content |
| showShape / showPriority / showMeta | boolean | Chrome toggles — all default false for freeform |
| minHeight / maxHeight | number | CSS length | Optional bounds — omit to fit content |
| scroll | true | "y" | "x" | "both" | Scrolls body content only — header/footer stay fixed |
| state | fall | stack | lock | clear | Motion model hook |
| className / style | string / CSSProperties | Merge onto the card shell |
| title / subtitle | string? | Optional header copy |
Freeform TetrisProvider card — Block without shape / shapeAt / showShape. Prefer Tile on packed dashboards.
| Prop | Values | Notes |
|---|---|---|
| (inherits Block) | omit shape* | Same chrome, size, accentPlacement, hover, title?, etc. |
| title / subtitle | string? | Optional — body-only tiles are valid |
Four children in a tetromino silhouette — or stack / carousel via layoutAt. Fluid width.
| Prop | Values | Notes |
|---|---|---|
| shape | I O T L S J Z | Required base silhouette |
| shapeAt | { desktop?, tablet?, mobile? } | Per-viewport silhouette overrides |
| layout | silhouette | stack | carousel | Base cell flow (default silhouette) |
| layoutAt | { desktop?, tablet?, mobile? } | Per-viewport layout overrides (stack / carousel) |
| carouselSnap | "page" | "cell" | Carousel slide width — full page or peek cell |
| viewport / breakpoints | auto | mode · { mobileMax?, tabletMax? } | Resolve shapeAt / layoutAt outside TetrisProvider (inherits when nested) |
| glass / flat / backdrop / border | same as Block | Per-cell chrome (glass on by default) |
| color | CSS color | Cell fill tint — default glass white |
| animation | same tokens as Block | Frame or staggered cell entrance (puzzle / lego) |
| accent | CSS color | "none" | false | Hairline + wash; none disables |
| voids | visible | ghost | invisible | Empty cells on T L S J Z |
| radius / size | same tokens as Block | Cell scale language |
| gap | number (px) | Space between silhouette cells (default 10) |
| className | string | Merge onto the frame root |
Priority packer — assigns CSS grid placement from child Block rules. Card heights fit content by default.
| Prop | Values | Notes |
|---|---|---|
| viewport | desktop | tablet | mobile | "auto" | Track width 12/8/4, or matchMedia from the window |
| breakpoints | { mobileMax?, tabletMax? } | Cutoffs for viewport="auto" (default 767 / 1023) |
| stretch | boolean | Equalize heights of cards that share a row |
| gap | number | string | [row, col] | { row, column } | Freeform spacing between packed cards (default 12) |
| className / style | string / CSSProperties | Merge onto the board grid |
VIEWPORT_COLUMNS
Constant map: desktop → 12, tablet → 8, mobile → 4. Used by the packer and docs demos.
resolveSizeForViewport
Pure helper: size + sizeAt + active viewport → effective SizeVariant.
resolveShapeForViewport
Pure helper: shape + shapeAt + active viewport → effective BlockShape.
resolveLayoutForViewport
Pure helper: layout + layoutAt + active viewport → effective ShapeLayoutMode (silhouette / stack / carousel).
SHAPE_GUIDES
Data for docs and tooling — principle, best-for, avoid, and mental model per shape.
packLayout
Headless packer (no React). Pass block defs + viewport; get placed rects.
resolveGap
Normalizes gap into CSS + px helpers for custom boards.