Scaffold a complete, responsive page layout with @allxsmith/bestax-bulma — app shells/dashboards, marketing/landing pages, centered auth/settings pages, and card-grid catalogs. Use when building a full page or overall app layout (not a single component).
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Bestax Layout Scaffold?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/allxsmith-bestax-layout-scaffold-bestax-skills)
---
name: bestax-layout-scaffold
description: Scaffold a complete, responsive page layout with @allxsmith/bestax-bulma — app shells/dashboards, marketing/landing pages, centered auth/settings pages, and card-grid catalogs. Use when building a full page or overall app layout (not a single component).
license: MIT
---
# Scaffolding a page layout with @allxsmith/bestax-bulma
Turn a high-level request ("admin dashboard", "landing page", "login screen", "product catalog")
into a complete responsive page built from bestax-bulma layout components.
## Behavioral rule
Select an archetype from the request and build it in one shot. Do **not** ask layout questions
("how many columns?", "where should the nav go?", "what width?") — infer the structure from the
request and proceed. The archetype determines the structure; fill it with the requested content.
Ask **at most one** clarifying question, and only for a high-level fork the request genuinely does
not imply: whether the page is **public-facing** (marketing) or an **internal tool** (authenticated
app). When the request already signals this ("dashboard", "admin", "landing", "login", "pricing"),
skip the question and default.
## Select an archetype
| Request signals | Archetype |
| ---------------------------------------------------------------------- | ------------- |
| dashboard, admin, console, internal tool, authenticated app, "sidebar" | **App shell** |
| landing, marketing, homepage, product/pricing page, public site | **Landing** |
| login, sign up, auth, settings, checkout, a single focused form | **Centered** |
| catalog, gallery, products, listing, "grid of cards", search results | **Card grid** |
Default when ambiguous: internal tool → App shell; public-facing → Landing; one focused task →
Centered; a collection of items → Card grid. For mixed requests, pick the dominant intent (e.g.
"admin dashboard with a product list" → App shell whose main column holds a Card grid).
## Approach
- Compose pages from the shipped layout components — `Container`, `Section`, `Hero`, `Footer`,
`Level`, `Columns`/`Column`, `Grid`/`Cell`, `Navbar`, `Menu`, `Card`. There is **no `Tile`
component**. For **uniform grids** (card grids, galleries — same-shaped items) prefer
`Grid`/`Cell`: CSS Grid gives equal-height cells for free (per row — each row's cells match
its tallest, same row-level behavior as the flex recipe). Use `Columns`/`Column` for
proportional or per-breakpoint column layouts — and when cards there must be equal height,
apply the flex recipe (`Column display="flex" flexDirection="column"` + `Card flexGrow="1"`;
`height: 100%` on the card doesn't help — the column's height is auto).
- Rely on Bulma's responsive defaults: `Columns` sit side by side on tablet and up and stack on
mobile. Add responsive `size*` props only to tune the breakpoints.
- Interactive extras don't share a state API — never transfer one by analogy:
`Collapse trigger={node} open/defaultOpen onOpenChange` (`onOpenChange(open)` reports every
trigger; `onOpen/onClose` fire only when the Collapse holds its own state), an accordion is
`Collapses value/defaultValue/onChange` around plain `Collapse` children indexed from 0 by
position (`number | null`, or `number[]` with `multiple`; hide an item with
`visibility="hidden"`, never `{cond && …}`, or the open state shifts to its neighbour) rather
than hand-rolled open-index state,
`Tabs value={i}/onChange`
(each `Tabs.Tab`/`Tabs.Content.Item` requires `index={i}`, and `Tabs.Content` must be a
**child of `<Tabs>`** — the active-tab context lives on it; a sibling panel never
switches. The arrow keys, Home/End, Enter/Space, the single tab stop and the
`aria-controls`/`aria-labelledby` links come built in, so add no `onKeyDown` or ARIA
of your own; `onChange` fires when a tab is activated, never on
arrow-key focus), `Dropdown active/onActiveChange`,
`ToggleButton pressed/defaultPressed onPressedChange` for a button that stays on or off (not
`Button isActive`, which sets no `aria-pressed`; the label stays the same in both states; a
set goes in `Buttons hasAddons role="group"` with an `aria-label`, where it's also
`Buttons.ToggleButton`, and a row where only one can be on is `SegmentedControl`),
`SegmentedControl aria-label="…" value/defaultValue onChange` for a row of buttons where
only one can be on, such as a list or grid view (one `SegmentedControl.Segment value="…"` per
option; a radio group with the single tab stop and the arrow keys built in, so add no
`onKeyDown` of your own; an icon-only segment needs its own `aria-label`; the arrows check as
they move, so `onChange(value)` fires on each, and it can't be cleared; it carries no form
value, so a choice a form submits is `Radios`),
`Popover trigger={<Button>…</Button>} open/defaultOpen onOpenChange` (`trigger` is one
element, not a node: Popover clones it to add the click and ARIA, so never wrap it in a
clickable of your own; build the panel from `Popover.Header`/`Body`/`Footer`/`Close`, and a
panel without a `Popover.Header` needs `ariaLabel`),
`HoverCard trigger={<Link href="…">…</Link>}` for a preview on hover or keyboard focus
(`trigger` is one link or button, cloned the same way; the card's content goes straight in as
children with no sub-components; a trigger inside a `Paragraph` needs `appendToBody`; hover
and focus only unless `openOnTap` is set, which suits only a trigger with no tap action of its
own, such as a `Button` with no `onClick`; on touch a link trigger just follows the link, so
what the card shows must be on that page too, and a form is a `Popover`),
`ContextMenu aria-label="…" trigger={<Table.Tr>…</Table.Tr>}` for actions on a right-click
or a long press (`trigger` is one element, a row or a card, cloned rather than wrapped; the
items are `ContextMenu.Item`/`ContextMenu.Divider`, which are `Dropdown`'s; no `open` prop,
`onOpenChange` reports it; always put the same items in a visible `Dropdown` on the row too,
since a context menu is only a shortcut),
`Steps value={i}/onStepClick items={[{label, icon?}]}` for a journey the user moves through,
such as a wizard or a checkout (child form is `Steps.Step`, not `Steps.Item`; a record of
what happened is `Timeline`). `Reveal cascade` staggers only its **direct children** — to
stagger a grid, put `<Reveal delay={i * 80}>` inside each `Cell`, not around the container.
- **An empty list, table or search with no results is `EmptyState`**, not a hand-built `Box`
of `Icon`, `Title` and `Buttons isCentered`:
`<EmptyState icon="folder-open" title="No projects yet" titleAs="h3" description="…">` with
its one `<Button>` as a child (two or more go in `EmptyState.Actions`). Its `title` is always a
heading, so set `titleAs` to the level under the list's own heading. It has no live region:
after a search or a filter, keep a `Paragraph` with `role="status"`, `aria-atomic="true"` and
`visibility="sr-only"` beside the search field the whole time, saying the count, and have
"Clear search" put focus back in the field. While data loads show a `Skeleton` or a `Loader`,
and a failed load is a `Message` or a `Notification`, never an EmptyState. The archetypes
reference has the full pattern under Card grid.
- **A table people sort, search, page through or pick rows from is `DataTable`**, not a `Table`
with sort state, `aria-sort`, checkboxes and a `Pagination` wired by hand:
`<DataTable caption="Users" columns={[{ id: 'name', header: 'Name', sortable: true, isRowHeader: true }, …]} data={rows} selectionMode="multiple" pageSize={10} hasSearch />`.
A column is `{ id, header }`, reading `row[id]`, plus `accessor` (the raw value, which sorts),
`format` (its text, which the cell shows and the search matches) and `cell` (rich content).
It needs a `caption`, `aria-label` or `aria-labelledby`, and it shows its own `EmptyState` and
says the match count itself. `toolbar={table => …}` holds buttons that act on
`table.state.selected` or call `table.setFilter`; `useDataTable({ columns, data, … })` returns
the same table for `<DataTable table={table} />` or for markup of your own through its
`get…Props` getters; `manual` with `rowCount` hands sorting, filtering and paging to a server.
`table.toCSV()` writes what's shown as CSV, escaping formulas, for a download button; there's
no Excel export. A table with nothing to sort, search or pick stays `Table`.
- **An order's history, a ticket's activity, a changelog or an activity feed is `Timeline`**,
not a hand-built list with a border down the side: `<Timeline titleAs="h3">` with a
`<Timeline.Item title="Shipped" time="14 October 2026" dateTime="2026-10-14">` per event, any
detail as its children, and the whole date in `time`. `titleAs` is the level under the
section's heading, or `"p"` in a busy feed. For something that moves along on its own, like a
parcel or a deployment, `active={i}` marks progress and `pending="Out for delivery"` ends it on
what's under way, oldest first. A journey the user moves through, like a wizard or a checkout,
stays `Steps`.
- **Facts about one thing, each a label and its value (an order summary, a profile, a file's
details), are `DescriptionList`**, not `Columns` of label and value or a two-column `Table`:
`<DescriptionList aria-labelledby="order-heading">` under a `Title id="order-heading"`, with a
`<DescriptionList.Item label="Order" value="#1234" />` per fact. It stacks on a phone and puts
the labels beside their values from tablet up, so add no breakpoint code; `orientation`
pins it. Several values, or a label with an info tip, go in as `DescriptionList.Label` and
`DescriptionList.Value` children. Several records with the same fields stay a `Table`, and so
do facts a screen reader must announce with their labels, as a `Table` with `scope="row"`
headers.
- **A number shown to the reader (a price, a total, a count, a percentage, a dashboard figure) is
`NumberFormatter`**, not `toLocaleString()` or an `Intl.NumberFormat` built by hand:
`<NumberFormatter value={8456.4} format={{ style: 'currency', currency: 'USD' }} />`, with any
`Intl.NumberFormat` options in `format` (`style: 'percent'` takes a fraction, `0.125` for
12.5%). It writes in its own `locale`, else the `LocaleProvider`'s, else the runtime's, so a
server-rendered app sets a `locale` on a `LocaleProvider` at the root. Its text is the number
alone, so words go around it: `Total: <NumberFormatter … />`. Text that can't hold an element,
an `aria-label` or a `placeholder`, gets the same from `const format = useNumberFormat();` and
`format(1500, { style: 'currency', currency: 'EUR' })`.
- **A dashboard's row of key figures (revenue, new customers, churn) is `Stats` of `Stat`s**, not
`Level` items, `Columns` of `Title`s or a `Card` per figure. Put
`<Stats aria-labelledby="kpis" minCol={8} gap={4}>` under a `Title id="kpis"`, with a `Stat` per
figure: `label`, a number `value` with `format` (`Intl.NumberFormat` options, as
`NumberFormatter` takes), `change` and `description`, as in
`<Stat label="Revenue" value={8456.4} change={0.12} description="since last month" />`.
`change` is a fraction written with its sign ("+12%"), green up and red down, and
`changeColor="danger"` marks a rise that's bad news, such as churn. The row wraps on its own, so
add no breakpoint code, and it holds only `Stat`s (a `LocaleProvider` goes around it, and on a
page rendered on the server it's what keeps the server and the browser writing the numbers
alike, unless each `Stat` gets a `locale`). `loading`
shows a `Skeleton` in place of a figure that isn't there yet. One figure on its own is a lone
`Stat`. For a card look, a `Box` goes around a lone `Stat` or the whole `Stats` row, never around
one `Stat` inside a row, which would break the row's list.
- **A file browser, a checkbox tree (permissions, filters) or an outline you browse is
`TreeView`**, with `<TreeView.Item value="…" label="…" icon="folder">` items nested inside each
other: `<TreeView aria-label="Files" selectionMode="single">` with `selected`/`onSelectedChange`
picks one, and `selectionMode="checkbox"` with `checked`/`onCheckedChange` draws boxes with
mixed parents, where a parent's value checks its whole branch. For categories, where checking a
parent means only the parent, `selectionMode="checkbox-independent"` draws a box per item that
checks that item alone, with no mixed parents. Map the data with a recursive
function, and render an item's children whether it's open or not. A folder that loads when it
opens takes `hasChildren`, `isLoading` while its children are on their way, and
`loadError="Couldn't load src"` with `onRetry` when the load fails, setting `isLoading` again
while the retry runs: the tree draws the Loading… and Retry rows and announces them, so don't
hand-write a placeholder item. Its items aren't
links, so the app's sidebar and any list of pages stay on `Menu` with `Menu.Item href`.
- **A sidebar whose groups of links open and close is `Menu.Section`**, one per group, directly
in `Menu` (never inside a `Menu.Item` or `Menu.List`):
`<Menu.Section label="Components" defaultOpen>` around that group's `Menu.List`, with
`defaultOpen` on the group holding the current page (or `open`/`onOpenChange` to hold the state).
Its heading is a real button with `aria-expanded`, and its links stay links, each a tab stop.
Not `TreeView`, and not a `Collapse` inside a hand-written `<li>`.
- **A long page's "On this page" list of its sections is `TableOfContents`**, not a `Menu` with
a scroll listener of your own: give each section's heading an `id` and pass the sections in page
order, `<TableOfContents isSticky items={[{ id: 'install', label: 'Install' }, { id: 'usage', label: 'Usage', children: […] }]} />`,
in a side `Column`. Its links are plain `#id` links that the browser follows itself, and it marks the
section being read with `aria-current="location"`. Offset a sticky header with
`scroll-padding-top` on `html` in the page's CSS (which the highlight reads too), not with
`scroll-margin-top` on the headings, and put any smooth scrolling in that CSS under
`prefers-reduced-motion: no-preference`. To mark the section in view in the app's own nav
`Menu`, `useScrollSpy({ ids })` returns its id.
- **An app's frame, a header across the top with a navigation column down the side, is
`AppFrame`**, not a `fixed` `Navbar` over `Columns`: `<AppFrame>` with `AppFrame.Header` (a
`Navbar` with `<AppFrame.Burger />` in its `Navbar.Brand`), `AppFrame.Nav` (the `Menu`) and
`AppFrame.Main` (the page), plus `AppFrame.Aside` and `AppFrame.Footer` when there are any. The
header and the column stick on their own, so no `fixed` and no `has-navbar-fixed-top`; below
desktop the column is a drawer the burger opens, and from desktop up the burger collapses it.
It renders the page's `<main>`, so it goes at the top of the app, never inside a `<main>`.
- **Panes the user resizes are `Splitter`** (a file tree beside an editor, a list beside its
detail, a query above its results), not hand-written drag code or fixed `Columns`:
`<Splitter height={480}>` with a `<Splitter.Pane label="Files" defaultSize={25} minSize="12rem">`
per pane. The handles go in on their own, each named by its pane's `label`, so give every pane
one. Sizes are percentages, limits also take `px` or `rem`, `orientation="vertical"` stacks
them (and needs a `height`), and `collapsible` with `collapsed`/`onCollapsedChange` lets a
button hide a pane. Put scrolling content in a `ScrollArea` inside the pane.
- **Content taller or wider than its space is `ScrollArea`** (release notes in a Card, a sidebar
longer than the screen, a feed that grows then scrolls, a wide table on a phone), not a `Box`
with `overflowY="auto"` and a height in a style: `<ScrollArea height={240} aria-label="…">`,
or `maxHeight="20rem"` to grow with its content first. It needs `aria-label` or
`aria-labelledby`, since while it overflows it's a region a keyboard user tabs to. The
helper props and `className` go on its frame and the `ref` on the element that scrolls.
`hasFade` fades the edges that have more, `orientation="horizontal"` suits a wide table, and
`onEdgeReach={edge => …}` loads more at an edge.
- **Something that stays in a corner of the screen (a floating button, a chat or help launcher,
a feedback tab, a consent notice) is `Affix`**, not `pos="fixed"` with offsets in a style:
`<Affix position="bottom-left"><Button color="primary">Feedback</Button></Affix>`.
`position` is one of the four corners, `'bottom-right'` by default, and stays physical in
right-to-left text. The spacing helpers nudge one further in (`mb="6"` lifts it over a bottom
bar), and `portal` moves it out of an ancestor with a `transform`. Two in one corner overlap,
so put what shares a corner in one Affix. Tab reaches it where it is in the markup, whatever
corner it shows in, so write it after the main content. For an element pinned once the page
scrolls past it, use `pos="sticky"` with a `top` instead.
- **A command palette (⌘K or Ctrl+K) is `Command.Dialog`**, not a `Modal` with an `Input` and a
hand-written `keydown` listener: `<Command.Dialog shortcut="mod+k" placeholder="…" actions={[…]} />`,
each action `{ id, label, onSelect }` with `group`, `keywords`, `description`, `icon` and `hint`
(a `Kbd.Group`) as it needs them. Typing filters the actions by label, keywords and description,
Enter runs the top match, and running one closes the dialog. `shortcut` is off unless given, and
it does nothing while the user types in a field. A button that opens it too uses `open` and
`onOpenChange`, with `aria-keyshortcuts` on the button. The same list inline, in a page or a
Popover, is `Command`. For server results, build it from parts: `Command.Input`, then
`Command.List` of `Command.Item`s, or `Command.Empty`, filtered from `onQueryChange`.
- Link lists (footer nav, sidebars): a bare `UnorderedList` of `ListItem`s is already
marker-less and flush — Bulma's reset unstyles `ul` — so no prop or CSS is needed;
bullets appear only inside `Content`.
- `Navbar.Burger`/`Navbar.Menu` are **controlled** — wire the same `active` state to both:
`active` on `Navbar.Menu` shows/hides the mobile menu, while `active` + `onClick` on
`Navbar.Burger` make the burger toggle it and animate. Left unwired, clicking the burger
does nothing (no error, silent failure). For a `fixed="top"` `Navbar`, add the
`has-navbar-fixed-top` class to `<html>` so content is not hidden behind it — the library
does not do this automatically, and an inline padding offset is not a substitute.
- **Style with helper props — no inline `style`, no raw Bulma `className`s.** Before writing
`style={{ … }}` anywhere, translate each declaration with the mapping table below — the helper
props cover the common cases. Bare markup has wrapper elements that take all
helper props: `<Span textSize="7" textColor="grey">`, `Paragraph`, `Strong` — never a raw
`<span className="is-size-7 has-text-grey">`. Table cells: `Th`/`Td` take `textAlign="right"`,
`textWeight`, `textSize` directly (their `color` prop colors the cell; for muted cell text
wrap content in `Span textColor="grey"`). Set the app-wide icon library once with
`<ConfigProvider iconLibrary="…">` at the root rather than `library` on every `<Icon>`. An app
in another language, or a right-to-left one, also gets `lang` and `dir` on `<html>` and a
`<LocaleProvider locale="…">` at the root, so overlays and date pickers follow it.
- **Alternating section bands are a prop, not CSS: `bgColor="scheme-main-bis"` on every
other `Section` (next band `scheme-main-ter`).** The scheme values render as a
dark-mode-safe inline `background-color: var(--bulma-scheme-*)` — zero custom CSS — and
still never `bgColor="light"`/`"white"`: those are fixed colors that stay light when dark
mode flips the text.
- **Decorative CSS is budgeted: two compact rules, ≤10 lines per app — comments count:
at most one short inline note, never a file-header comment block — every value derived
from `--bulma-*`.** A marketing page gets at most one hero wash and one featured-card
ring, applied via `className` — no resets (Bulma ships one;
body/list margins are already zero) and no grid textures, masks, or multi-layer backdrops;
the components carry the design:
```css
.hero-wash {
background-image: radial-gradient(
60rem 30rem at 20% -10%,
hsl(var(--bulma-primary-h) var(--bulma-primary-s) 50% / 0.2),
transparent 60%
);
}
.featured-ring {
--bulma-shadow: 0 0 0 2px var(--bulma-primary);
}
```
The ring works by overriding the **upstream token**, not the component's own var: `.card`
and `.box` re-declare `--bulma-card-shadow`/`--bulma-box-shadow` from `--bulma-shadow` on
their own selector, so setting _those_ from an ancestor never wins (same for
`--bulma-box-radius`; `--bulma-card-radius` is a literal with no ancestor route at all).
A class-scoped CSS rule keeps the ring on just the one featured card without wrapping it in
its own `Theme`; `<Theme bulmaVars={{ '--bulma-shadow': … }}>` also compiles and is the
better fit when the override already applies to a whole scoped subtree. Either way the
subtree stays theme- and dark-mode-aware.
- **CTAs on a colored hero must stay legible in both schemes.** On a fixed-color surface
(`Hero color="primary"`, a dark banner), use **filled** buttons — `color="light"` or
`color="primary" isInverted` — never a thin `isOutlined` secondary: a light outline + light
label on a dark surface is low-contrast and gets worse under OS dark mode. And when the page's
design is single-mode (a fixed light or dark look), pin it on the `<html>` in `index.html`,
`<html lang="en" data-theme="light">`, so a visitor's OS dark mode can't flip Bulma's text
colors out from under the fixed palette, from the first paint on (details: the
`bestax-theming` skill's contrast rules).
## Three components core Bulma will talk you out of
Most of this library's additions get found on their own, because nothing in Bulma does the
job. These three do not: each has a Bulma near-miss close enough to stop the search. Across
44 cold-start builds, `Dialog` was used **zero** times and `LinkButton` in two thirds — and
every miss shipped the "not this" column instead.
| You need | Use | Not this |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| A brief confirmation after an action, self-dismissing | `Toast` — mount `<ToastContainer position="top-right" />` once at the app root, then `toast.success('Saved')` (also `.danger`/`.warning`/`.info`) from anywhere | `Notification`/`Message` |
| A confirm or alert the user must answer before anything | `Dialog` — mount `<DialogContainer />` at the root, then `if (await dialog.confirm({ title, message })) …` | `Modal` — an empty shell; the title, message, button row and confirm/cancel wiring are all yours to rebuild |
| A control that reads as text or a link but _does_ something | `LinkButton` (`variant="text" \| "ghost" \| "underline"`, optional `color`) | `<a href="#">`/`<div onClick>` (no keyboard or screen-reader support) or `Button color="text"` (still button-shaped) |
Mounting a container without ever calling `toast.*`/`dialog.*` is not usage — the container is
the mount point, the imperative call is the thing that shows something. Both also work as
ordinary controlled components when you would rather hold the state yourself —
`<Toast message … duration onClose>`, `<Dialog isOpen title message type onConfirm onCancel>` —
but in an app with more than one call site the root container plus the imperative helper is
less wiring, not more.
## Inline style → helper prop mapping
Look up the declaration you were about to inline. The spacing, typography, and flex helpers
below are on every component; `textColor`/`bgColor` are on the content components you'll
target (`Box`, `Block`, `Title`, `Content`, `Hero`, `Card`, …) — the ones with a semantic
`color` variant (`Tag`, `Tabs`, `Panel`) take `color` instead; wrap content in a `Block` if
you need a text color there. `Notification` is the mixed case: it takes `textColor`, but its
background comes from the semantic `color` prop, not `bgColor`.
| Inline style you're about to write | Helper props instead |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `marginTop: '1rem'` (any margin/padding) | `mt="4"` — `m`/`mt`/`mx`/`p`/`py`/… scale: `1`=0.25rem, `2`=0.5rem, `3`=0.75rem, `4`=1rem, `5`=1.5rem, `6`=3rem (nearest step) |
| `textAlign: 'center'` | `textAlign="centered"` (also `left`, `right`, `justified`) |
| `color: '#…'` | `textColor` with the nearest Bulma color: `primary`, `link`, `info`, `success`, `warning`, `danger`, `white`, `black`, `grey` (+ `grey-light`, `grey-dark`, …) |
| `backgroundColor: '#…'` | `bgColor` (same palette) |
| `fontSize: …` | `textSize="1"`…`"7"` (`1` largest). On `Title`/`SubTitle`, `as` sets the level and `size` picks it too, so an `<h3>` at size 4 is `as="h3" textSize="4"` |
| `fontWeight: …` | `textWeight`: `light`, `normal`, `medium`, `semibold`, `bold` |
| `textTransform`, italics | `textTransform`: `uppercase`, `lowercase`, `capitalized`, `italic` |
| `display: 'flex'` + flex properties | same-named props: `display="flex"`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap` |
| `height: '100%'` on a flex child | `flexGrow="1"` |
| `display: 'none'` | `visibility="hidden"`, or responsive `display*` props (`displayMobile`, `displayTablet`, …) |
| `gap: '1rem'` (flex or grid) | `gap="2"`, or one axis: `columnGap`, `rowGap`; gap scale: `1`=0.5rem, `2`=1rem, `4`=2rem, half steps (`"1.5"`) |
No helper matches (e.g. `maxWidth`, a one-off gradient)? Add a named class to the project's
global stylesheet and pass it via `className` — still never inline `style`. In a create-bestax
app that is `src/index.css`, which its entry file imports after bestax's stylesheet, so the
class wins over a Bulma rule of the same weight. The starter page's `src/App.css` loads after
bestax's too, but only while the starter's `App` imports it.
## References
- `references/layout-components.md` — the layout component inventory: real prop names, types, and
accepted values, plus subcomponent nesting.
- `references/archetypes.md` — the four archetypes: selection criteria, JSX skeleton, and responsive
behavior.
## Examples
- `examples/app-shell.tsx`: an `AppFrame` with a `Navbar` header, a `Menu` column and the page
(dashboard).
- `examples/landing.tsx` — fixed `Navbar` (controlled burger) + `Hero` + `Section`s + `Footer`.
- `examples/centered.tsx` — centered single column (auth/settings).
- `examples/card-grid.tsx` — multiline `Columns` of `Card`s (catalog).
- `examples/content-page.tsx` — hero + feature cards + CTA styled with helper props (no inline
`style`), wrapped in `ConfigProvider`.
## Checklist
- [ ] Map the request to one archetype; do not ask layout questions.
- [ ] Wrap page content in `Container` (+ `Section` for vertical rhythm).
- [ ] Use `Grid`/`Cell` for uniform grids (equal heights per row, free); `Columns`/`Column`
for proportional or per-breakpoint side-by-side layout — with the flex recipe when its
cards must match height.
- [ ] An app shell is `AppFrame`, with `AppFrame.Burger` in `Navbar.Brand`: no `fixed`, no
`has-navbar-fixed-top`, and never inside a `<main>`.
- [ ] Elsewhere, wire `active` state to **both** `Navbar.Burger` and `Navbar.Menu` (they are
controlled), and for a fixed navbar add `has-navbar-fixed-top` to `<html>`.
- [ ] Do not use `Tile` — it is not shipped.
- [ ] Action feedback goes through `Toast`, a confirmation through `Dialog`, a text-styled
action through `LinkButton` — not `Notification`, `Modal` or a bare `<a>`.
- [ ] An empty list or a search with no results renders `EmptyState`, and a search or a filter
keeps a `role="status"` region beside its field that says the count.
- [ ] Style with helper props, not inline `style` — translate via the mapping table; values
with no helper get a named class in the stylesheet, never `style={{}}`. No raw Bulma
`className`s either (`Span`/`Paragraph` wrap bare text; `Th`/`Td` take `textAlign`/`textWeight`).
- [ ] Alternating bands are `bgColor="scheme-main-bis"` (next band `scheme-main-ter`) on
every other `Section` — never band CSS, never `bgColor="light"`/`"white"`.
- [ ] Decorative CSS ≤10 lines total incl. comments — no file-header comment (hero wash +
featured-card ring), `--bulma-*`-derived; no resets — Bulma ships one.
The ring sets `--bulma-shadow` (via a scoped CSS rule or `Theme bulmaVars`).
- [ ] Set the icon library once via `<ConfigProvider iconLibrary="…">` at the root.
- [ ] Site built? ~800 KB raw / ~82 KB gzip CSS is the expected default-flavor size — to shrink
it, run the `bestax-optimize` skill (measure first).