busy-office-ui
Menu

Troubleshooting

This framework's failure modes are unusually silent — modern CSS degrades by doing nothing, not by erroring. Symptom → cause:

"My markup looks right but nothing happens"

Usually a class or an attribute value that does not exist. Both fail silently: an unknown class leaves the element unstyled and looks like a CSS bug, and an unknown attribute value is worse — the attribute is present, spelled correctly, and does nothing at all.

npx bo-check-markup dist          # or any directory of built HTML

It checks your HTML against api.json, which is extracted from the shipped CSS and so cannot disagree with it: every bo-* class must exist, and every framework data-* attribute must carry a value the CSS actually switches on. It reports the file and suggests the nearest real name.

Your own data-* hooks are never flagged — data-row-id and the like belong to your app, and nothing distinguishes them from a misspelled framework attribute, so the tool stays quiet rather than noisy. This repo runs it over its own docs on every build; it found an invented .bo-composer__input and a .bo-u-text-sm that never existed.

Better than reaching for it once something is already wrong: wire it into your build. It needs built HTML, so it runs after your build — and it checks that names exist, not that your markup means anything.

SymptomCause & fix
Dialogs / tabs / dropdowns do nothingThe behavior init was never called. initDialogs(), initTabs(), initDropdowns() — once each, anywhere, any time. See JS behaviors.
Components render unstyled or half-styled à la carteImport order: tokens, then reset, then components. Components carry only their own layer.
Your overrides don't applyThey're inside an @layer. Write app CSS unlayered — unlayered always wins. Never !important.
Form error messages never appearBelow the browser floor (:has()/:user-invalid), interactive reveals are fail-closed. Server-set aria-invalid + message-as-sibling still works. Floor: Chrome/Edge 119 · Firefox 129 · Safari 17.5.
Bulk-actions bar stale after an HTMX row swapOnly htmx:after:swap re-derives automatically. Other swap mechanisms: call refreshDataTable(container).
Everything is 40% too smallThe host page sets html { font-size: 62.5% }. The framework assumes an unmodified root — embed via iframe.
Density attribute seems ignored on one tableIt's on a wrapper while the table sits in a narrow container — explicit density wins only when set on the table or its container element itself.
Sort arrows render but clicking does nothingBy contract: your code re-orders rows and sets aria-sort; the CSS only draws the indicator.
Toast appears behind an open dialogThe dialog owns the browser top layer; z-index can't beat it. Render confirmations inside the dialog.
Checked rows POST nothingRow checkboxes need name/value.
Buttons/tables go unstyled the moment Tailwind (or an old reset) is addedThe reset is unlayered, and unlayered CSS beats every @layer — by design. Wrap it: see below.

Using with Tailwind or an existing reset

All framework CSS lives in @layer, and your unlayered CSS always wins — that's the override story. The flip side: an unlayered reset you didn't write (Tailwind v3 preflight, normalize.css, Bootstrap reboot, a CSS-in-JS global) also wins, and silently strips components — preflight's button { background: transparent } is the classic. Wrap the reset in a layer that ranks below the framework's:

/* app.css — the ONE statement that matters comes first: it fixes the
   layer order with your reset's layer ranked BELOW the framework's. */
@layer app-reset, bo-reset, bo-tokens, bo-primitives, bo-components, bo-utilities;
@import url("tailwind-or-normalize-or-reboot.css") layer(app-reset);
@import url("busy-office-ui.min.css");   /* or the npm import */

/* Your own overrides: write them UNLAYERED, as usual — unlayered still
   beats everything, which is the framework's normal override story. */

The order statement must be the first rule of the entry stylesheet — a layer's rank is fixed by where it first appears, so declaring app-reset before the framework's layers is what pins it underneath. Tailwind v4 declares its own layers and coexists without wrapping; on v3 you can also just disable preflight (corePlugins: { preflight: false }).

Proof, live

Reset unlayered: the button is stripped bare.
Same reset wrapped in app-reset: framework wins.

Each frame is an isolated document running the exact same hostile reset — the only difference is the one @layer statement. (Frames pin a white background: they don't inherit the docs theme.)

↑↓ move Enter open Esc close