CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

bestax-layout-scaffold

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).

Quality

83%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

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 signalsArchetype
dashboard, admin, console, internal tool, authenticated app, "sidebar"App shell
landing, marketing, homepage, product/pricing page, public siteLanding
login, sign up, auth, settings, checkout, a single focused formCentered
catalog, gallery, products, listing, "grid of cards", search resultsCard 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?}]} (child form is Steps.Step, not Steps.Item). 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.

  • 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 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. 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>.

  • 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.

  • 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.Items, or Command.Empty, filtered from onQueryChange.

  • Link lists (footer nav, sidebars): a bare UnorderedList of ListItems 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 classNames. 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:

    .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 needUseNot this
A brief confirmation after an action, self-dismissingToast — mount <ToastContainer position="top-right" /> once at the app root, then toast.success('Saved') (also .danger/.warning/.info) from anywhereNotification/Message
A confirm or alert the user must answer before anythingDialog — 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 somethingLinkButton (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 writeHelper 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, italicstextTransform: uppercase, lowercase, capitalized, italic
display: 'flex' + flex propertiessame-named props: display="flex", flexDirection, justifyContent, alignItems, flexWrap
height: '100%' on a flex childflexGrow="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 — fixed Navbar + sidebar Menu + content (dashboard).
  • examples/landing.tsx — fixed Navbar (controlled burger) + Hero + Sections + Footer.
  • examples/centered.tsx — centered single column (auth/settings).
  • examples/card-grid.tsx — multiline Columns of Cards (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.
  • Wire active state to both Navbar.Burger and Navbar.Menu (they are controlled).
  • 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 classNames 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).
Repository

allxsmith/bestax

Path

skills/bestax-layout-scaffold/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.