This page is generated by scripts/build-do-nots.mjs. It does not carry its own opinions; every line is pulled from a kit contract doc's Banned combinations section, a design-canon sentence, or an app-patterns recipe's Do-nots list, cited back to where it lives. Regenerate with node scripts/build-do-nots.mjs after editing any of those. Do not hand-edit this file; edit the source and regenerate.
The rule this page exists to fix: a rule filed under one component's contract is invisible to a builder who never opens that doc. This page is the one place that reads all three sources at once, so a builder can check here before shipping instead of guessing which doc might mention their surface.
By component#
One subsection per kit contract doc that carries a Banned combinations section. A doc with no such section (no contract has been written for it yet, or it has no misuse on record) has no entry here; check the doc itself.
Alert#
- Do not reach for
Alertfor a transient event: a save that succeeded, a job that finished, a copy that landed. (packages/ui/docs/alert.md:44) - Do not paint two different outcomes the same tone, and do not make one banner switch tone as conditions change. (
packages/ui/docs/alert.md:46) - Do not render an outcome banner below the table or form it explains. (
packages/ui/docs/alert.md:48) - Do not pass a JSX array as
childrenexpecting one row per child. (packages/ui/docs/alert.md:50)
AppShell#
- Do not infer
activefromactiveHrefplus a prefix check done outside the component, for example matchingpathname.startsWith(item.href)and never settingitem.active. (packages/ui/docs/app-shell.md:62) - Do not render a page's own breadcrumb, sidebar, or topbar markup inside
childrenexpectingAppShellto absorb it. (packages/ui/docs/app-shell.md:64) - Do not mount a second toast stack, modal host, or file-preview host per shell instance. (
packages/ui/docs/app-shell.md:66) - A nav group kicker lighter than
/65, or a hand-rolled indent on the rows: a lighter ink fails AA at 11px; hierarchy in a long column comes fromnavGrouping="railed"(quiet kicker, rows one step in behind a rail, canon doc 08 §A10), never from restyling the kicker or rows from an app. (packages/ui/docs/app-shell.md:67)
Breadcrumbs#
- Do not pass every crumb an
href. (packages/ui/docs/breadcrumbs.md:54) - Do not render the trail preset when the app's own topbar already carries the crumb; one location line per page. (
packages/ui/docs/breadcrumbs.md:54)
Button#
dropdown(split-button): implemented and typed, but has zero shipped call sites in either app. The one place that visually resembles a split-button,RowMenu, deliberately bypassesButtonand builds its own trigger andDropdownwiring. Before reaching forButton'sdropdownprop, confirmRowMenugenuinely does not fit; it is the proven pattern for a trailing action menu. (packages/ui/docs/button.md:65)promisifyandbrickedandIconOnClick: all implemented, all currently unused in shipped code. Cautioned, not banned: treat as unverified in production layouts. If you use one of these, screenshot-check it, since no existing screen has exercised it. (packages/ui/docs/button.md:66)- Raw daisyUI
btnclasses instead ofButton: cautioned. The canon (doc 2 §C) flags two known, bounded deviations: dense editable grids (contentgen setup, loadsheet alias rows) and the kit's ownModalFooter, both marked as a documented swap-later TODO, not a pattern to copy into new code. (packages/ui/docs/button.md:67) - A disabled button with no explanation: canon doc 2 §A4 is explicit that "a button that cannot act is hidden or explains itself, never silently disabled." Reach for
brickedplustooltip, or a plain-text hint beside the button, never baredisabled. (packages/ui/docs/button.md:68) - Bare
variant="outline" color="neutral"as a toolbar's right-hand button: banned across the app (owner, doc 59 G1; canon doc 2 §A10). That combination with no icon, noshadeand no tone is the kit's zero and reads as a placeholder next to a card title or a page title. Minimum isshade; an icon is strongly encouraged; a navigation action isvariant="text"withIcon="Right" iconRight; an action with a meaning takes its tone. And it lives in the toolbar slot, never in a row of its own under the title. (packages/ui/docs/button.md:69) - Underline on a Button: never, in any variant, at rest or on hover (doc 60 §5). Hover feedback is the shade wash. Underline is for prose links inside
Markdown. (packages/ui/docs/button.md:70)
Card#
- A nested
Cardinside aModalor peek panel. Canon doc 5 §A3 names this a defect outright: use aborder-tdivider plus a heading instead. This has already been caught and corrected once (the doc cites it as a measured violation, doc 10 §A10). (packages/ui/docs/card.md:56) - Doubling the horizontal inset. A full-bleed child that re-adds its own edge padding instead of canceling the card's (
-mx-6plus first/last-cell padding) overflows by exactly the doubled amount; canon doc 5 §A4 records this happening once ("overflowed 2px") and states the fix as structural: name the single element that owns the inset. (packages/ui/docs/card.md:57) compacton customer-facing content cards. Reserved for denser admin and config screens (canon doc 5 §A7); a customer content card that reaches forcompactfor visual tightness rather than density need is working against the convention. (packages/ui/docs/card.md:58)- A sibling
<div>holding a card-scoped mutating action instead oftoolbar. Canon doc 5 §A2 is explicit that this always goes through thetoolbarslot. (packages/ui/docs/card.md:59) - A hand-typed opacity on the header icon disc (
iconWrapClassName="bg-primary/8",text-base-content/55): the disc's washes are ladder rungs picked byiconVariantandiconTone; a shade that looks wrong is feedback against the ladder (canon doc 01 §A8), not a one-off number. A deliberate one-off override is allowed, named as such in the commit. (packages/ui/docs/card.md:60)
Chip#
onChange(orselected) together withonRemove. See the contract note above:onRemovewins unconditionally andonChange/selectedare dead code on that render. If a chip needs to be both selectable and removable, that is two chips or a different component, not oneChipwith both props. (packages/ui/docs/chip.md:53)Chipstanding in forStatusPill/StatusDotorFieldChip. Canon doc 3 §A1 states this as a defect, not a preference: selection, state, and naming are three different signals and the reader needs to tell them apart at a glance. (packages/ui/docs/chip.md:54)Chipwithselectedas a two-option segmented toggle. The shipped precedent for that exact shape is a hand-rolled button pair (ToggleChip,ItemsView.tsx:62-90), not this component. If you're tempted to reach forselectedto build a Yes/No or grouped/flat switch, check whether a join-pair button (canon doc 7 §B, "Admin quick toggle") fits better first. (packages/ui/docs/chip.md:55)- Passing
iconProps. It is silently dropped; if per-render icon customization beyondiconSize/iconClassNameis needed, it is not currently plumbed through. (packages/ui/docs/chip.md:56)
CodeBlock#
- Do not fall back to a raw
<pre>for a stored JSON or text document. (packages/ui/docs/code-block.md:45) CodeBlockrenders exactly what it is given and copy writes that string verbatim, never the colorized output (code-block.tsx:165); pass the raw source and pick thelanguagethat colours it. (packages/ui/docs/code-block.md:47)- Do not lift the
maxHeightcap on a page-level mount. (packages/ui/docs/code-block.md:49) - Do not use
CodeBlockas an editor. (packages/ui/docs/code-block.md:51)
CopyButton#
- No banned combinations documented: None found; there is nothing shipped that misuses this component. One naming collision is worth knowing about:
walmart-mvp/frontend/src/features/catalog/WalmartSubmit.tsx:511declares its own localCopyButtonfunction, unrelated to the kit's, for a listing title bar's copy control (WalmartSubmit.tsx:479). Check the import,@versable-git/uiversus a local declaration, before reading or extending aCopyButtoncall site in that file. (packages/ui/docs/copy-button.md:42)
Dropdown#
- Do not compose a new
Dropdownplus a hand-rolled<ul>of clickable rows whenRowMenuorSelectalready cover the shape. (packages/ui/docs/dropdown.md:53) - Do not rely on
middlewarePropsto overridefliporshiftdefaults casually. (packages/ui/docs/dropdown.md:55) - Do not nest a portaled
Dropdowninside aModalwithoutnoPortal. (packages/ui/docs/dropdown.md:57)
Dropzone#
- Do not pass both
name(form mode) and rely ononFilesto also drive an upload. (packages/ui/docs/dropzone.md:49) - Do not omit
hintwithout setting it tonullexplicitly whenheadlinealready carries the instruction. (packages/ui/docs/dropzone.md:51)
EmptyState#
- Do not omit
Iconon a state that is empty only because something is still loading or running. (packages/ui/docs/empty-state.md:52) - Do not render an "all clear, this is good news" state in the default neutral. (
packages/ui/docs/empty-state.md:54) - Do not ship a single generic
EmptyStatecovering true-empty, filtered-empty, and in-progress-empty as one undifferentiated case. (packages/ui/docs/empty-state.md:56) - Do not use
walmart-mvp/frontend/src/components/EmptyState.tsxand the kit'sEmptyStateinterchangeably. (packages/ui/docs/empty-state.md:58)
FieldChip#
- Do not use
FieldChipto show a record's state (that isStatusPill/StatusDot) or a selectable, removable value (that isChip). (packages/ui/docs/field-chip.md:49) - Do not add
size,variant,Icon, or a removable mode toFieldChipto get a richer chip. (packages/ui/docs/field-chip.md:51) - Do not put a raw palette colour on a chip tone. (
packages/ui/docs/field-chip.md:53) FieldChipisinline-flex, so a bare string child cannot shrink below its longest word and paints over the next column; wrap it (WrappingFieldChip,wrapping-field-chip.tsx:11-17). (packages/ui/docs/field-chip.md:55)
Icon#
- Do not import a
react-iconspackage directly at a component call site. (packages/ui/docs/icon.md:47) - Do not treat the registry's key count as fixed. (
packages/ui/docs/icon.md:49) - Do not assume passing
iconPropstoStatusPilldoes anything. (packages/ui/docs/icon.md:51)
Identity (IdentityRow / WorkspaceSwitcher)#
- Do not colour the exit. (
packages/ui/docs/identity.md:63) - Do not tint the avatar disc or the title by status. (
packages/ui/docs/identity.md:65) - Do not hang
onClickon a bareIdentityRowand call it a control. (packages/ui/docs/identity.md:67) WorkspaceSwitcherhas one live mount point,AppShell'ssidebarTop(App.tsx:501-514); a second picker in the topbar or a page is the duplicate-chrome defectapp-shell.md's checklist opens with. (packages/ui/docs/identity.md:69)
InlineEdit#
- No banned combinations documented: None found. No shipped call site sets
disabled, and no shipped call site exists in walmart; both are implemented and correct, not proven in production use outside speedway's rename flows. (packages/ui/docs/inline-edit.md:44)
Input#
flatornoBorderas prop names. These do not exist on the current component; the live equivalent issurface="flat"orsurface="outline". A caller trying to passflat/noBorderis either targeting an old version of this component or misremembering the old app'sInputV1. Neither compiles against the current types. (packages/ui/docs/input.md:73)errorandbottomtogether, expecting both to show. The component suppressesbottomwhenevererroris set (input.tsx:198-205); passing both is not a type error, but onlyerrorwill ever render. Treat them as one slot with two possible contents, not two independent ones. (packages/ui/docs/input.md:74)isTogglewithoutelement="checkbox".isToggleonly changes rendering whenelementresolves to"checkbox"(controlKind,input.tsx:10-13); on any other mode it is a silent no-op. (packages/ui/docs/input.md:75)- A
placeholderas a field's only label. A placeholder is not an accessible name: it vanishes on the first keystroke and screen readers treat it inconsistently, so the field reaches a screen reader or a password manager unnamed. A stringtopnames the field on its own; a field without one needs an explicitaria-label. The other likely wrong answer is reaching for aLabelcomponent to pair withhtmlFor, the shadcn idiom; this kit exports noLabel, andtopis that surface. (packages/ui/docs/input.md:76) - A ReactNode
topon an accessibility-critical field, with noaria-label. Only a stringtopbecomes the accessible name, because JSX has no text for the component to lift. Passaria-labelalongside it. (packages/ui/docs/input.md:77)
Kicker#
- Do not give a
Kickera colour class; a label is never a status. (packages/ui/docs/kicker.md:36) - Do not use it in place of
PageTitleorCard title. (packages/ui/docs/kicker.md:36) - Do not stack two kickers with nothing between them; a kicker opens content, it is not the content. (
packages/ui/docs/kicker.md:36) - Do not pass a paragraph as children; the idiom is a two-to-four word label. (
packages/ui/docs/kicker.md:36)
ListItem#
- No banned combinations documented: None found; there is no shipped misuse because there is no shipped use. (
packages/ui/docs/list-item.md:35)
Markdown#
- Do not render a doc's H1 twice: strip it and give it to
PageTitle, or leave the doc bare, never both. (packages/ui/docs/markdown.md:44) - Do not reach for
Markdownto render one formatted sentence inside another component; that isrenderMarkdownLite. (packages/ui/docs/markdown.md:44)
Modal#
- Body content that re-adds horizontal padding is banned. (
packages/ui/docs/modal.md:59) - A generic grey empty or error state inside a modal is banned. (
packages/ui/docs/modal.md:61) - A one off raw confirm instead of
ConfirmModalis banned. (packages/ui/docs/modal.md:63) - A copy affordance on a status value inside modal chrome is banned. (
packages/ui/docs/modal.md:73)
PageInfo, PageLoading, PageError#
PageInfo(directly or throughEmptyState/PageLoading/PageError) is the one primitive for this; every real instance in both apps routes through it. (packages/ui/docs/page-info.md:52)- Do not pick
tone="neutral"(the default) for an absence that is actually good news, and do not picktone="success"for an ordinary, unremarkable absence. (packages/ui/docs/page-info.md:54) - Do not render a bare, generically-worded error string in place of
PageError'sdescription. (packages/ui/docs/page-info.md:56)
PageSkeleton#
- A
cardsheight you did not measure. A generic card promises a shape that never arrives, which is the defect this component exists to fix. Open the page, measure it, pass that. (packages/ui/docs/page-skeleton.md:60) PageSkeletonandPageLoadingon the same route. They are two answers to one question. Pick by whether the shape is known. (packages/ui/docs/page-skeleton.md:61)- Omitting
classNameon a page whose container carries layout. Without it the placeholder draws in a different box than the page, which is the original 45px-narrow failure with extra steps. (packages/ui/docs/page-skeleton.md:62)
PageTitle#
backis a slot for the link's content only;page-title.tsx:62supplies theLeftArrowicon and the muted-to-solid hover transition itself. (packages/ui/docs/page-title.md:56)- Do not reach for
skeletonto blank a title that is already known. (packages/ui/docs/page-title.md:58) - Do not style a JSX
titleexpectingrenderNodeto do it. (packages/ui/docs/page-title.md:60)
SchemaForm#
- Never a required dot or a bare asterisk without its tooltip; the required mark is the kit's
RequiredMark(a red asterisk with a "Required" tooltip), the same oneInputdraws (owner ruling W01, doc 67). (packages/ui/docs/schema-form.md:53) - Never a native
<select>for a mode with behavioural weight; a discriminatedoneOfis a mode chooser or a segmented control. (packages/ui/docs/schema-form.md:54) - Never a full-width numeric field; numbers default to a third of the grid. (
packages/ui/docs/schema-form.md:55) - Never a spinner as the form's loading state; the loading form is a shape-matched skeleton. (
packages/ui/docs/schema-form.md:56) - Never a silent fallback when a schema is outside the subset; the refusal renders in place of the form. (
packages/ui/docs/schema-form.md:57) - Never the schema's
descriptionas an error message. (packages/ui/docs/schema-form.md:58) - Never layout keys in a schema beyond
x-widthandx-section; pixels, columns and colours belong to the kit. (packages/ui/docs/schema-form.md:59) - Never a second field anatomy in an app beside the kit's. (
packages/ui/docs/schema-form.md:60)
Select#
- A second multi-select control built beside this one. The private copies are retired:
FacetBarnow composesSelect multiplefor its multi facets, and walmart'sMultiFacetControlis the shape this mode replaces. If a surface needs somethingmultiplecannot express, add the prop here rather than starting a third control. (packages/ui/docs/select.md:88) - A facet whose values can contain the URL list delimiter, built as a multi-select. Canon doc 7 §A4 states this as a hard constraint independent of the kit: "a facet over values that can contain the URL list delimiter (part types with commas) must be single-select." This is why walmart's part-type facet stays single-select. The
multiplemode does not lift it, because the constraint is about the URL, not the control. (packages/ui/docs/select.md:89) - A
Selectpanel inside aModalwithoutnoPortal. It will paint underneath the native<dialog>'s top layer and be invisible or unreachable; see the sanctioned-combinations row above for the fix. (packages/ui/docs/select.md:90)
SidePanel#
- Do not render a
SidePaneldirectly from a page component. (packages/ui/docs/side-panel.md:41) - Do not use
SidePanelfor a view where the detail IS the task and context can be safely obscured; that isModalterritory per doc 10 §A1's decision tree, not a peek. (packages/ui/docs/side-panel.md:43) - Do not confuse
walmart-mvp/frontend/src/features/catalog/ReviewTab.tsx:1096's localfunction SidePanel({...})with the kit component. (packages/ui/docs/side-panel.md:45)
Spinner / Skeleton / SkeletonGroup / Progress#
- Do not use
Spinnerin place of aSkeleton/SkeletonGroupfor content that has a known shape (a table's rows, a card's fields, a list). (packages/ui/docs/spinner.md:40) - Do not blank text that is already known ahead of the data. (
packages/ui/docs/spinner.md:42) - Do not drive
Progress's adjacent color-coded text from a hardcoded or dynamically-interpolated Tailwind class instead of the same prop-driven color the bar itself uses. (packages/ui/docs/spinner.md:44)
StatTile#
- Do not use
StatTileas a per-row element inside a list, "in place of table columns" per canon doc 5 §A6. (packages/ui/docs/stat-tile.md:45) - Do not set both
onClickandhref;hrefsilently wins, and since real usage always reaches forhref+linkAsfor navigation, anonClick-only tile is untested territory in either app. (packages/ui/docs/stat-tile.md:47) - Do not size an unexplained number's tile without a
hintor atooltip. (packages/ui/docs/stat-tile.md:49)
StatusPill / StatusDot#
- Do not use
StatusPill/StatusDotfor a pending mutation (a cancel in flight, a save in progress). (packages/ui/docs/status.md:49) - Do not use
StatusPillwhere the canon calls for aChiporFieldChipinstead: selecting/filtering values, or naming a field or file. (packages/ui/docs/status.md:51) - Do not import
walmart-mvp's localpipeline.tsxStatusDotexpecting the kit'skind/label/pulseprops, or vice versa. (packages/ui/docs/status.md:53) - Do not paint an identity concept (a module, a chain, a data source) in
StatusKindcolors. (packages/ui/docs/status.md:55)
Stepper#
- Do not colour labels or connectors through
className; the tones are the contract (success for done, primary for active, warning for blocked). (packages/ui/docs/stepper.md:55) - Do not use
onStepto run mutations; a step click navigates, it never submits. (packages/ui/docs/stepper.md:55) - Do not hand-build a stage strip again; this component exists because three copies drifted. (
packages/ui/docs/stepper.md:55)
Table#
- Do not pass a checkbox selection column via
SelectionCell/SelectAllCellwhile leavingclickSelectat its defaulttrue. (packages/ui/docs/table.md:288) - Do not reach for the kit's
FilterBarcomponent expecting it to match either app's shipped toolbar look. (packages/ui/docs/table.md:290) - Do not put
copyCellson an editable grid. (packages/ui/docs/table.md:292) - Do not rely on the default
emptyfallback (<EmptyState compact Icon="Table" title="No data">) as a shipped list's actual empty state. (packages/ui/docs/table.md:294) - Do not build a column with a hand-rolled
renderfor a shape acol.*factory already covers, such as a mono id, a right-aligned number, a formatted date, or aStatusPillstatus, without first checking whether the factory's contract fits. (packages/ui/docs/table.md:296)
Tabs#
- Do not build canon doc 7 §A9's filter chip row (an exhaustive category partition with live counts, sitting above a list's toolbar) out of
Tabs variant="chip". (packages/ui/docs/tabs.md:42) - Do not confuse the kit's
Tabswithwalmart-mvp/frontend/src/components/ui/tabs.tsx, a wholly separate local wrapper over RadixTabsPrimitive, exporting its ownTabs/TabsList/TabsTrigger/TabsContent. (packages/ui/docs/tabs.md:44)
Timestamp#
- Do not wrap
Timestampin a secondTooltip. (packages/ui/docs/timestamp.md:35) - Do not treat
walmart-mvp/frontend/src/pages/Admin.tsx:751-761's localfunction Timestamp({ value })as the kit component. (packages/ui/docs/timestamp.md:37)
Toast#
.promiserethrows by design so the caller can branch on failure (skip a refresh, keep a form open), not so the caller can report the error again. (packages/ui/docs/toast.md:57)- Do not give a spinner toast (
spinner: true) an explicit finiteduration. (packages/ui/docs/toast.md:59) - Do not rely on content-hash de-duping for JSX toast content. (
packages/ui/docs/toast.md:61)
Tooltip#
- Do not wrap
Timestamp,Button(with its owntooltipprop set),Chip(withtooltipset), orStatTile(withtooltipset) in a second<Tooltip>. (packages/ui/docs/tooltip.md:68) - Do not conditionally render
<Tooltip>around a trigger based on whethercontentis present. (packages/ui/docs/tooltip.md:70)
By canon doc#
Every sentence in the twelve design-language docs containing “never”, “do not”, “must not”, “is a defect”, “banned”, or “not a pattern to copy” (case-insensitive, deduplicated per doc). The scan runs the whole doc, not only its principles section, so a rule stated anywhere still surfaces here.
0 · Overview and mental model#
- The hard-won rule: app tokens must read the semantic layer, never the palette layer. (
docs/design-language/00-overview.md:64) - WHAT a thing is ... never how it's doing. (
docs/design-language/00-overview.md:67) - Never encode state in an identity color or vice versa. (
docs/design-language/00-overview.md:67) PAGE_METAis the canonical example: the sidebar, page titles, and tooltips all read the same entry, so icon and label can never drift (app/lib/page-meta.ts:5-8). (docs/design-language/00-overview.md:70)- Feedback is local and layered, never global dimming. (
docs/design-language/00-overview.md:72) - Content is never dimmed: "The sweep bar says 'working'; the route's skeleton says 'here is what is arriving'" (
speedway app.css:1093-1102). (docs/design-language/00-overview.md:73) - Where a misuse can be made impossible, the API change beats the Banned entry:
PageSkeletonrequireslayoutrather than defaulting to a guess about a page nobody looked at (packages/ui/src/page-skeleton/page-skeleton.types.ts:12), the split Button derives its caret's accessible name rather than asking every caller to remember one (packages/ui/src/button/button.tsx:264-265), and presets carry ruled combinations so a caller picks a name instead of reassembling the rule. (docs/design-language/00-overview.md:82) - A Banned entry is the fallback for what the API cannot reach, never the first tool. (
docs/design-language/00-overview.md:82)
1 · Foundations#
- The dark-mode bug this prevents is documented at speedway
app.css:50-59: palette values do not know what theme they are in. (docs/design-language/01-foundations.md:14) - An app's own tokens alias the kit's ramp, never a value of their own. (
docs/design-language/01-foundations.md:16) - Interactive:
SemanticColor= primary | secondary | accent | neutral | info | success | warning | error (internal/variants.ts:2-10), resolved through theSURFACE/BTN_COLORtables, never ad hoc Tailwind classes. (docs/design-language/01-foundations.md:32) - Identity:
--id-parttype-ink,--id-content-ink, and siblings say WHAT a thing is, "never how it's doing" (colors.css:231-236). (docs/design-language/01-foundations.md:34) - Do not mint new steps. (
docs/design-language/01-foundations.md:40) - Mono is for identifiers (SKUs, ids, code), never decoration. (
docs/design-language/01-foundations.md:43) - Both
htmlandbodycarry the paper background so overscroll never flashes white. (docs/design-language/01-foundations.md:46) - A tone at less than full ink is a rung on the tint ladder, never a number. (
docs/design-language/01-foundations.md:51) - Those are the rungs, and like the type ramp (doc 12 §A1) they are decided here, never typed as a fresh number on a page. (
docs/design-language/01-foundations.md:52) - Never invent
/8or/30because it looked right, and never stack a wash on a wash: the ladder has one rung per role, and a surface that needs two is asking for a different component (a Card, a Chip). (docs/design-language/01-foundations.md:52) - Consumer checklist: load the fonts, mount both theme blocks, point app tokens at semantic slots, adopt the kicker idiom, and never hand-pick palette steps in component code. (
docs/design-language/01-foundations.md:74)
2 · Buttons and actions#
- A button that cannot act is hidden or explains itself, never silently disabled. (
docs/design-language/02-buttons-and-actions.md:27) - Pending state lives on the button, never the status badge. (
docs/design-language/02-buttons-and-actions.md:31) - Never a synchronous alert. (
docs/design-language/02-buttons-and-actions.md:35) - Never toast logic owned by a component that unmounts on success (the bulk-bar lesson, doc 10). (
docs/design-language/02-buttons-and-actions.md:35) - Never import react-icons at a callsite. (
docs/design-language/02-buttons-and-actions.md:41) - A card's, a section's or a page title's right-hand action is never the kit's zero: bare
variant="outline" color="neutral"with no icon, no shade and no tone reads as a placeholder beside the title it belongs to. (docs/design-language/02-buttons-and-actions.md:47) - Never ship that combination as a toolbar action. (
docs/design-language/02-buttons-and-actions.md:47) - It also lives in the toolbar slot, never in a row of its own under the title (the "Swap sides" button on
apps/playground/src/app/patterns/shape-matched-skeleton/was the case). (docs/design-language/02-buttons-and-actions.md:47) - Two companions ride with it: a Button never underlines, at rest or on hover, in any variant (hover feedback is the shade wash; underline belongs to prose links in
Markdown), and a text button's count or identifier (the "See all 67 docs" shape) inherits the button's colour and size with no separate opacity (doc 60 §4, §5, §6;button.mdBanned). (docs/design-language/02-buttons-and-actions.md:47)
3 · Status language#
- Three small-tag species, never interchangeable. (
docs/design-language/03-status-language.md:13) - Misusing one for another's job is a defect. (
docs/design-language/03-status-language.md:14) - It never dumps raw signals. (
docs/design-language/03-status-language.md:19) - IngestOutcomeBanner paints rejected files error-tinted and no-data-rows files neutral, "the banner must not paint the two the same colour." (
docs/design-language/03-status-language.md:34) - Identity ink never signals health. (
docs/design-language/03-status-language.md:37)
4 · Loading, empty, and error states#
- Never blank what you already know. (
docs/design-language/04-loading-and-states.md:21) - Optional sections are never placeholdered: "a placeholder that appears and then vanishes is a worse jump than one that was never drawn." (
docs/design-language/04-loading-and-states.md:25) - A promised number that may never arrive gets NO skeleton (nav badges render nothing until real). (
docs/design-language/04-loading-and-states.md:25) - Content is never dimmed. (
docs/design-language/04-loading-and-states.md:31) - Delays exist so instant transitions never flash feedback. (
docs/design-language/04-loading-and-states.md:31) - An auth-boot frame that does not yet know which page it is landing on still must not show a generic placeholder. (
docs/design-language/04-loading-and-states.md:43) - The worked case is walmart's expired cloud credential, which surfaced for a day as a polished generic sentence advising the user to retry, while retrying could never work: three layers each replaced a specific fact with a vaguer one, and each replacement looked like good defensive practice on its own. (
docs/design-language/04-loading-and-states.md:46)
5 · Cards, surfaces, and stat tiles#
- A mutating action scoped to a card goes through
toolbar={...}, never a sibling div. (docs/design-language/05-cards-and-surfaces.md:19) - Nesting cards inside a framed overlay is a defect. (
docs/design-language/05-cards-and-surfaces.md:22) - The hand-rolled equivalent is
scrollClassName="-mx-6 ... border-t border-base-200"with edge padding carried by first/last cells, "never doubled onto every cell (that overflowed 2px)", and it stays the right choice when the surface also needs a custom scrollport. (docs/design-language/05-cards-and-surfaces.md:25) - The strip never reached this surface. (
docs/design-language/05-cards-and-surfaces.md:33) - What stays banned is a tile per row of a list, which is what the principle's title is actually about. (
docs/design-language/05-cards-and-surfaces.md:33) - Content meeting a framed edge: cancel the container's inset, never add a second (
docs/design-language/05-cards-and-surfaces.md:53)
6 · Tables#
DataTable(Layer B): filter → search → sort → paginate pipeline (fixed order, so search never narrows just the current page), auto selection column, rowMenu, default toolbar, pager. (docs/design-language/06-tables.md:17)- Raw
<table>: legitimate only for dense inline-EDITABLE form grids (field editors, sample previews), never for browsable lists. (docs/design-language/06-tables.md:18) - Never two. (
docs/design-language/06-tables.md:21) - Cell helpers are explicit, "a value only becomes a link/date/code because you asked, never by accident." (
docs/design-language/06-tables.md:27) - Width discipline: scroll, never overlap, shed for peeks. (
docs/design-language/06-tables.md:29) table-fixed+ explicitminWidthmakes narrow viewports scroll sideways ("owner: never overlap"). (docs/design-language/06-tables.md:30)position: stickyresolves against the nearest scroll container, so any wrapper between the header and the real scrollport captures it and pins it to a box that never moves. (docs/design-language/06-tables.md:42)
7 · Toolbars and filters#
- Facets are never bare labels. (
docs/design-language/07-toolbars-and-filters.md:18) - Selections must be visible; a chips ROW is banned, invisible filters more so (owner rulings 2026-07-30 and 2026-08-12). (
docs/design-language/07-toolbars-and-filters.md:24) - Placement was refined again on 2026-08-12: chips gather AFTER all of the row's controls, never inline beside the one that owns them. (
docs/design-language/07-toolbars-and-filters.md:25) - A module raised standalone and inside a workflow are separate facets ("never overlap", owner 2026-07-30). (
docs/design-language/07-toolbars-and-filters.md:37) - The test is exhaustiveness: if the options do not partition the list, it is a facet. (
docs/design-language/07-toolbars-and-filters.md:40) - List toolbars never do; they filter and refresh. (
docs/design-language/07-toolbars-and-filters.md:62)
8 · Navigation and shell#
- Two shell layers, and pages never draw chrome. (
docs/design-language/08-navigation-and-shell.md:14) - The mental model: chrome is infrastructure a route declares into, never markup a page repeats. (
docs/design-language/08-navigation-and-shell.md:15) - The sidebar, the page title, and any tooltip naming a page all read the same entry, "so a page's icon/label can never drift from its sidebar entry" (
page-meta.ts:5-8). (docs/design-language/08-navigation-and-shell.md:18) - Active state is explicit, never inferred by prefix. (
docs/design-language/08-navigation-and-shell.md:20) - Prefix matching is banned by scar tissue. (
docs/design-language/08-navigation-and-shell.md:21) - A pending spinner appears only after 120ms so instant transitions never flash it (
WorkspaceNav.tsx:78-119). (docs/design-language/08-navigation-and-shell.md:24) ShellCrumbsderives the trail from URL segments with a label map, resolves dynamic segments to loaded doc names (job name, org name) with a skeleton while pending, hides the entire workspace concept (doc-29), and never renders a single-crumb trail: "the trail earns its row only two or more levels deep" (owner 2026-07-29,ShellCrumbs.tsx:143-145). (docs/design-language/08-navigation-and-shell.md:33)- Themed assets swap by CSS visibility, never by themed
img src(flash risk,app.css:185-197). (docs/design-language/08-navigation-and-shell.md:39) - A long nav column shows its hierarchy by weight, indent and a rail, never by a lighter kicker; the treatment is opt-in. (
docs/design-language/08-navigation-and-shell.md:41) - Never make a kicker lighter than
/65to push it back (AA at 11px, the owner's 2026-08-17 ruling); a kicker that still reads louder than its rows is a weight problem, not an ink problem. (docs/design-language/08-navigation-and-shell.md:42) - An
undefinedcount renders no badge at all: "a skeleton here promised a number some pages never deliver" (WorkspaceNav.tsx:175-176). (docs/design-language/08-navigation-and-shell.md:48) - It exists for a bar that is
fixedorstickyover a scrolling surface from first paint, so toggling the background never shifts layout. (docs/design-language/08-navigation-and-shell.md:52) - Do not opt in over dense content, a table, a form, a dashboard, where blur costs legibility exactly where the reader needs it, and never on the kit
TopbarasAppShellmounts it (seeapp-shell.md: that row never sits over scrolled content in the first place). (docs/design-language/08-navigation-and-shell.md:52)
9 · Page composition#
- Prose inside still caps:
.statement { max-width: 68ch }so text never stretches even on a wide panel (app.css:1306-1311). (docs/design-language/09-page-composition.md:18) - Never hand-build viewport bounding per page. (
docs/design-language/09-page-composition.md:23) - Overscroll never flashes. (
docs/design-language/09-page-composition.md:34) - One default reading width (Speedway: 1200px), one prose cap (68ch), applied by container class, never per-element. (
docs/design-language/09-page-composition.md:62)
10 · Overlays#
- Deviation is therefore legitimate and never free: the surface that deviates records its reason in its own doc, and the reason names the UX the break buys. (
docs/design-language/10-overlays.md:22) - Two sidebars can never sit side by side. (
docs/design-language/10-overlays.md:25) - Modals mount at the callsite, parked at the bottom of the page's JSX, never centrally. (
docs/design-language/10-overlays.md:28) - A failed load renders the structured error block, never a bare line of red text. (
docs/design-language/10-overlays.md:54) - Copy affordances attach to the data plane, never the control plane. (
docs/design-language/10-overlays.md:56) - Copy belongs on values naming the thing (identifiers, attributes, generated content, error messages), never on values reporting how the work is going (status pills, coverage chips, completion ratios). (
docs/design-language/10-overlays.md:57) - The test is mechanical rather than tasteful: if it renders in the status vocabulary, it must not carry copy. (
docs/design-language/10-overlays.md:57) - A modal footer over a body short enough that nothing ever scrolls beneath it needs none of this: the gate never fires, so the bar sits in its resting state and the opt-in buys nothing. (
docs/design-language/10-overlays.md:63)
11 · Motion#
- Placeholders do not animate in. (
docs/design-language/11-motion.md:16) - Where a pulsing dot already says "working", a second spinner is banned ("the stage badge's pulsing dot is the working cue, no second spinner"). (
docs/design-language/11-motion.md:20) - Instant transitions never flash feedback. (
docs/design-language/11-motion.md:26) - IconCycle cross-fades three glyphs (700ms opacity, 2200ms interval) with all icons mounted "so the fade never shifts layout." (
docs/design-language/11-motion.md:29) - A keyframe outside the block is a defect. (
docs/design-language/11-motion.md:35) - Never leave a sibling of an animated element to appear with no entrance at all; either it rides the segment or it has its own. (
docs/design-language/11-motion.md:38) - Progress fill: width transition 0.45s (mock) or static fill; never bounce (
docs/design-language/11-motion.md:51)
12 · Typography#
- This doc says why and what not to do, and never repeats what they show. (
docs/design-language/12-typography.md:7) - A fourth working size is a design decision made in this doc, never a class typed on a page. (
docs/design-language/12-typography.md:14) - Never type an arbitrary pixel size (
text-[13px]) without a reason written beside it; R6 counted 84 across the tree, 41 in walmart, and each is a size outside the ramp. (docs/design-language/12-typography.md:14) - Mono is for identifiers, never decoration. (
docs/design-language/12-typography.md:19) - Inside an alert, an identifier is the inline code chip, never a mono paragraph, and the alert title is never mono. (
docs/design-language/12-typography.md:20) - Never use mono to make text look technical. (
docs/design-language/12-typography.md:20) - One kicker idiom, one component, never re-typed. (
docs/design-language/12-typography.md:22) - Never type
uppercase tracking-wide text-xson a page again; R6 counted about 76 hand-typed copies and 176uppercaseclasses in walmart alone, which is how the same label ended up in three recipes on one landing page (doc 59 V1). (docs/design-language/12-typography.md:23) - A page never invents a fifth. (
docs/design-language/12-typography.md:25) - The icon never sits in a row of its own above the title. (
docs/design-language/12-typography.md:26) - Never place a section's action under its title in a row of its own. (
docs/design-language/12-typography.md:26) - Never bold a phrase inside body text to make it louder. (
docs/design-language/12-typography.md:29) - Never uppercase a button, a table header or a status pill. (
docs/design-language/12-typography.md:32) - A tone on text says status or intent (the five semantic tones,
docs/design-language/03-status-language.md), never emphasis or decoration. (docs/design-language/12-typography.md:35) - A kicker never carries a tone (a coloured label reads as a status). (
docs/design-language/12-typography.md:35) - Never colour a paragraph or a title for effect. (
docs/design-language/12-typography.md:35) - Never invent
/55because it "looked right", and never drop small text below/65. (docs/design-language/12-typography.md:38) - Spacing around type comes from the containers (doc 9 §A5's 30/26px section rhythm,
Card's padding, the title stack'sgap-2), never from margins typed onto a text node. (docs/design-language/12-typography.md:41) - Never truncate a value the reader must be able to read in full without giving them the tooltip. (
docs/design-language/12-typography.md:44) - A button never underlines. (
docs/design-language/12-typography.md:46) - Never mix the two on one surface (doc 59 D4). (
docs/design-language/12-typography.md:47) - Help text and hints: never smaller than
xs, never lighter than/65(docs/design-language/12-typography.md:62) - Message text: one size step between title and body, never two (
docs/design-language/12-typography.md:65)
13 · Forms and fields#
advancedis collapsed by default and its header carries a count of values that differ from the baseline, the active preset else the defaults ("Advanced · 2 changed"), the same measure review mode marks with, so a closed section never hides a change silently. (docs/design-language/13-forms-and-fields.md:22)- A mode with behavioural weight is a row of cards, never a native select. (
docs/design-language/13-forms-and-fields.md:24) - A table inside a form never grows sideways. (
docs/design-language/13-forms-and-fields.md:27) - Messages are sentences a person would say ("Enter a whole number between 1 and 20", "Choose at least one column"), never the schema's
descriptionreused as an error, never a keyword name. (docs/design-language/13-forms-and-fields.md:34) - Warnings (
x-warn, a value outside a suggestion list, the app's warning-tier issues) never block; a warned control carries the warning edge in the same tone as its line until an error takes over. (docs/design-language/13-forms-and-fields.md:34) - Validation states live on the border, never on an outline: the kit's field frame keeps a constant 1px edge that changes colour (
packages/ui/theme/fields.css:69-89), the focus ring is the only outline, and every coloured edge takes one step towards the ink on hover with the field transition (the same.field-edge-*rule for the field frame, Select and every grouped control; doc 01 §A8). (docs/design-language/13-forms-and-fields.md:34) - The form never renders its own Save. (
docs/design-language/13-forms-and-fields.md:37) - Never a spinner over nothing (doc 4 §A). (
docs/design-language/13-forms-and-fields.md:40) - An option's label starts upper case ("Neutral", "Part type"), never the raw value, unless the value is an identifier (a SKU, a code, a path), which shows as typed. (
docs/design-language/13-forms-and-fields.md:46) - All of it comes from
x-option-metaon the schema or from the option object the app resolves (packages/ui/docs/schema-form.md,x-option-meta), never from a hand-styled row. (docs/design-language/13-forms-and-fields.md:46) - There is never a second field anatomy in an app beside the kit's; if a screen needs a shape this doc does not have, the shape is added here first. (
docs/design-language/13-forms-and-fields.md:49) - Never full-width numerics. (
docs/design-language/13-forms-and-fields.md:57) - Never a native
<select>for a mode with behavioural weight; never a required dot or a bare asterisk without its tooltip (the mark isRequiredMark); never an outline for a validation state (the edge carries the tone); never a lower-case option label unless the value is an identifier; never a full-width numeric field; never a spinner as the loading state; never an error message that is the schema'sdescription; never a silent fallback when a schema is outside the kit's subset (the form refuses by name,packages/ui/docs/schema-form.md); never layout keys in a schema; never a second field anatomy in an app beside the kit's. (docs/design-language/13-forms-and-fields.md:78)
App patterns#
AP-10: a browsable list#
From the recipe's own Do-nots section, mined from both apps' correction history.
- Do not let a row's state have two sources of truth. (
docs/app-patterns/10-recipe-browsable-list.md:139) - Do not render a caught fetch failure as the empty state. (
docs/app-patterns/10-recipe-browsable-list.md:144) - Do not batch nothing. (
docs/app-patterns/10-recipe-browsable-list.md:148) - Do not let a nav badge count go stale. (
docs/app-patterns/10-recipe-browsable-list.md:157) - Do not size columns by habit. (
docs/app-patterns/10-recipe-browsable-list.md:161) - Do not make a clickable cell look like text. (
docs/app-patterns/10-recipe-browsable-list.md:165) - Do not separate a table header from its body by weight alone. (
docs/app-patterns/10-recipe-browsable-list.md:169) - Do not mismatch the type scale of controls that sit in one row. (
docs/app-patterns/10-recipe-browsable-list.md:172) - Do not hand-write skeleton headers. (
docs/app-patterns/10-recipe-browsable-list.md:176) - Do not put chips inline beside their own facet. (
docs/app-patterns/10-recipe-browsable-list.md:181)
AP-11: a record detail#
From the recipe's “Where the do-nots live” list.
- A confirm modal closing before its async action starts, so the user sees no evidence the action began. Row 9. (
docs/app-patterns/11-recipe-record-detail.md:111) - A component that carries its own tooltip wrapped in another tooltip, stacking two. Row 17. (
docs/app-patterns/11-recipe-record-detail.md:113) - A modal whose panes scroll internally while the kit assumes page-level scrolling, which walmart works around by overriding
scrollbar-gutterin its own stylesheet. Row 7, and a live kit tension rather than an app defect. (docs/app-patterns/11-recipe-record-detail.md:115)
AP-16: a module settings form#
From the recipe's own Do-nots section, the forms trait in the recipe's voice plus the one that belongs to the recipe alone.
- Do not put a native select where a mode has behavioural weight. (
docs/app-patterns/16-recipe-module-settings-form.md:63) - Do not mark a required field with a dot or a bare asterisk. (
docs/app-patterns/16-recipe-module-settings-form.md:65) - Do not stretch a numeric field to full width. (
docs/app-patterns/16-recipe-module-settings-form.md:67) - Do not show a spinner as the form's loading state. (
docs/app-patterns/16-recipe-module-settings-form.md:69) - Do not reuse the schema's
descriptionas an error message. (docs/app-patterns/16-recipe-module-settings-form.md:71) - Do not fall back silently when a schema is outside the subset. (
docs/app-patterns/16-recipe-module-settings-form.md:73) - Do not put layout keys in a schema. (
docs/app-patterns/16-recipe-module-settings-form.md:75) - Do not build a second field anatomy beside the kit's. (
docs/app-patterns/16-recipe-module-settings-form.md:77) - Do not render a Save button inside the form. (
docs/app-patterns/16-recipe-module-settings-form.md:79)
Doc 12's classification#
Doc 12 sorts every rule the list and detail recipes carry, do-nots included, into one of three buckets (docs/app-patterns/12-primitives-and-rules.md:26): FREE (docs/app-patterns/12-primitives-and-rules.md:49), a shipped kit primitive already makes the mistake impossible; BUILDABLE (docs/app-patterns/12-primitives-and-rules.md:288), no primitive exists yet, ranked by how often the rule was actually broken; JUDGMENT (docs/app-patterns/12-primitives-and-rules.md:385), no primitive can decide it, and it stays law. Read doc 12 directly for which bucket a given do-not is in today; several have moved buckets as primitives shipped, and a per-item mapping copied onto this page would itself be a fourth place to go stale.
How to use this page#
Read in this order: a canon doc says WHY a rule exists, a kit contract doc says WHAT it means for one component's props, an app-patterns recipe shows a WORKED composite where several rules travel together. This page only says WHAT TO AVOID, gathered from all three; it is a checklist, not a replacement for reading the doc a finding cites. Page-versus-shell rules (packages/ui/docs/app-shell.md's Banned combinations, and canon doc 8) bind every builder even outside AppShell itself: a hand-rolled sidebar or topbar is held to the same rules a shell component would enforce for you.