The Toolcrib Taste-Test

Disclaimer: This report was produced by Claude (an AI), working under a human’s direction. Every claim below was tested against the real toolcrib source, not assumed — but the testing, code, and conclusions are still AI output. Review before citing a number externally.

Date: 2026-08-14
Subject: Does toolcrib (an npm-installed, AI-oriented React component library) improve on unguided “vibe coding,” and in what sense? Started as a comparison of visual styling control; the evidence extended it to interaction correctness and architecture as well — see Findings 9–10.
Scope note: This reflects toolcrib with the five patches from this evaluation applied — a WCAG contrast fix; manifest type-definition resolution (across two files); DataTable.rowSubtheme; and the matching Anti-Patterns doc entry — not the state it shipped in before this test.


Verdict

The evidence in this report adds up to something stronger than “toolcrib is nicer to work with”: uncontrolled, unguided output has multiple genuine risk surfaces, not one, and none of them are a taste preference — they fail silently. Four separate results in this evaluation demonstrate that, not just assert it, spanning both styling and interaction correctness:

  • A restyle attempt collapsed three distinct spacing roles (table cells, inputs, buttons) into one identical value — the script ran without error, the page rendered without error, and the task was reportable as complete. Nothing caught it (Finding 3/4).
  • One of three independently-built, ordinary-looking unguided trials shipped text at 2.54:1 contrast — below WCAG AA, genuinely hard to read for a meaningful fraction of real users, and nothing in that build process flagged it (Finding 7).
  • Given the ability to freely pick a UI accent color, 4 out of 4 plausible, ordinary-looking choices (a light yellow, a mint green, a pale pink, a light gray) produced a button with white text at 1.2–1.4:1 contrast — not “hard to read,” effectively invisible. This wasn’t an adversarial edge case; it’s what happens by default the moment color choice is unconstrained (Finding 8).
  • Not a styling defect at all: an unguided build of one of the most common interactive elements in ordinary app work — a delete-confirmation dialog — had zero of the four standard accessible-dialog behaviors: no focus trap, no Escape-to-close, no ARIA dialog semantics, no focus restoration to the triggering element. Confirmed by direct inspection of the generated code, not assumed (Finding 9). This one is arguably harder to catch than the others, since a sighted developer testing with a mouse never trips any of the four.

None of these four were caught by a compiler, a linter, a test, or the build process itself. Each surfaced only because this evaluation went looking for it from outside the ordinary request-response loop. That’s the actual danger, and it isn’t specific to CSS: unguided output’s failures don’t announce themselves, in styling or in behavior. They ship, and stay shipped, until a human happens to notice — which for accessibility specifically (contrast or keyboard/screen-reader access) can mean a real user locked out of a control entirely, a support ticket, or in jurisdictions with accessibility law (ADA in the US, the EU Accessibility Act, and similar), a genuine compliance exposure. (Not legal advice — a general observation that this category of defect has real-world stakes beyond aesthetics, worth flagging to whoever owns that risk at an organization, not just to whoever owns the CSS.)

This is not an argument that toolcrib specifically is required. It’s an argument that something needs to make color/spacing/contrast/interaction decisions structural rather than hoped-for — a design-token system with enforced contrast, automated accessibility testing in CI (axe-core, Lighthouse CI), a linting rule that rejects known-bad patterns, headless accessible primitives (Radix, React Aria, or similar) as the default rather than hand-rolled markup, a mandatory human accessibility review — toolcrib is simply the one mechanism this evaluation tested, and it happens to close all four of the failure modes above by construction rather than by someone remembering to check. The comparison that matters isn’t “toolcrib vs. Tailwind.” It’s “some enforced control vs. none” — and this report’s evidence is about what “none” actually produces when nobody’s looking, in more than one subsystem.

How toolcrib specifically controls the styling risk surface, evidenced across this report: multiple, coordinated levels, not one blunt rule.

  • Compile-time: style/className aren’t valid props on any toolcrib component — a type error, not a lint warning that can be silenced or ignored (Finding 1).
  • Per-instance: components with a registered theme slice expose a typed, sparse overrides prop for one-off adjustments — enumerable values only, never a raw style object.
  • App-wide: ThemeProvider‘s parameters set color/radius/density/mode once, read by every component from one shared source, which is why a global restyle can’t produce the cross-component cascading collision an ad hoc rewrite did (Finding 3).
  • Semantic: a bounded four-category subtheme vocabulary (error/success/warning/info) applies uniformly across component types and, since this evaluation’s patch, individual table rows too (Finding 5).
  • Underneath all of the above: contrast enforcement runs automatically regardless of which level actually produced a given color — the theme parameters, a subtheme, or an end user’s own pick (Finding 7).
  • Beyond the running app: the same guarantees extend past build-time to a designer tuning the shipped default and to an end user personalizing at runtime, through one shared component rather than two separately-engineered systems (Finding 8).
  • Around all of it: a machine-readable manifest describes exactly what’s controllable at each level, so the vocabulary is discoverable by reading documentation rather than requiring a dive into source (Finding 10, and the patches in this report that closed real gaps in it).

No single level here is airtight on its own — Finding 4 found where semantic-only control (four subthemes) runs out for a non-semantic need, Finding 2 found a harmony-mode gap in the theme level’s own reach for a neutral secondary color. But layered together, they’re the reason ad hoc styling doesn’t have anywhere convenient to hide in this system by default: every path back to arbitrary style either fails to compile, or requires actively bypassing an enumerated, typed, contrast-guaranteed alternative that was sitting right there instead.

And the fourth bullet above — the dialog with zero accessible behaviors — is controlled the same way, in a separate subsystem. Building Modal/Popup/SlideOut/AlertDialog on Radix’s tested primitives instead of hand-rolled markup, and using a typed event bus (aiBus) for cross-tree state instead of prop-drilling, is architecturally the same move as everything in the styling list above: replace a decision an unguided build has to re-earn correctly every single time with one made once, inherited automatically. 60% of toolcrib’s component set inherits it this way (Finding 9); an unguided build re-risks the same absence — focus trap, Escape handling, ARIA semantics, focus restoration — per component, every time, with nothing in an ordinary sighted-developer testing pass likely to catch it.

Toolcrib is a conditional improvement, not a universal one. It reliably eliminates the failure mode it targets — ad hoc, inconsistent, hard-to-globally-restyle utility-class soup — by construction: no toolcrib component’s props accept style or className at all, a compile-time guarantee rather than a convention someone can forget. It also enforces a WCAG contrast floor on every color it generates dynamically, something unguided output has no equivalent of.

That guarantee is narrower than “no ad hoc styling anywhere,” though. It only restricts toolcrib’s own component props — plain JSX inside a slot, a child, or a render callback is exactly as stylable as ever. The real constraint bites only where a component’s own DOM is generated entirely internally and never exposed as a prop or slot at all.

A frame that applies throughout: nothing here is automatically enforced by the AI maintaining the toolkit. Every fix reflected in this report — the contrast patch, the new row-flagging prop, the doc update — required a human to decide it was worth institutionalizing and direct it. That’s not a limitation of toolcrib specifically; it’s the shape of how any of this stays correct. What toolcrib changes is the leverage of each such decision: once a shared, constrained vocabulary exists (paddingMode, cornerRadiusMode, subtheme, rowSubtheme), a short human imperative (“make this row a warning,” “make corners sharp app-wide”) lands on one precise, testable primitive shared by the whole codebase — instead of requiring a fresh, ad hoc translation into raw styling every time, the exact failure mode demonstrated below.

The cost this doesn’t show up in Finding 6’s token count: every unguided defect surfaced in this report — a3’s failing contrast (Finding 7), the restyle cascade bug (Finding 3), all four unreadable end-user color picks (Finding 8) — was not caught by the unguided build process itself. Nothing flagged any of them; they only surfaced because this evaluation went looking from outside the normal request-response loop. In an ordinary session, each ships silently until a human happens to notice, and only then does correction start — an indeterminate number of turns, unknowable in advance, with no guarantee a fix doesn’t introduce a new problem. Every toolcrib defect in this report required the same kind of deliberate effort to find, but once found, closed structurally and permanently — a compile-time guarantee, a shared resolver, a manifest generator — inherited by every future project built against the patched library at zero further cost. The asymmetry isn’t “toolcrib has fewer bugs.” It’s that unguided defects are per-project and non-transferable — a fresh session tomorrow re-risks the identical mistake with none of today’s corrections carried forward — while toolcrib’s, once patched, are closed for every future consumer at once.

Project shapeRecommendation
Long-lived app, restyled/rethemed repeatedly, multiple contributors (human or AI)Toolcrib wins clearly
App needs end-user-facing theme customization (presets, live color picking, persistence)Toolcrib wins clearly — see Finding 8
One-shot prototype, thrown away after a demoRoughly a wash — onboarding/context cost isn’t recouped
Frequent narrow, non-semantic one-off visual tweaks (outside the existing subtheme/theme-axis vocabulary)Unguided/Tailwind wins on flexibility — see Findings 5–6
Non-React stackNot applicable

Methodology

Prompt (identical for every trial in both conditions):

“Build a single-page admin settings screen: a left sidebar nav (5 items), a top toolbar with a page title and a save button, and a main content area with three stacked sections — a profile card (avatar, name, email fields), a notification preferences section (4 toggle rows with label + description each), and a billing section (a small table of past invoices). End with a sticky footer bar with Save/Cancel buttons. Make it look like a polished SaaS product, fully responsive.”

Session A (control):

npm init -y

No framework, bundler, or CSS approach specified — the agent was free to choose anything. Three independent trials (a1, a2, a3), each a fresh build from the same prompt with no cross-contamination (later trials weren’t shown earlier trials’ output before being built). All three independently converged on the same unprompted default: a static index.html pulling in Tailwind via the CDN <script> tag — the common “quick vibe-coding” reflex — but with genuine structural variation between them (different layout approaches: flex-column sidebar vs. CSS grid template; different accent colors: indigo, blue, violet; different spacing scales).

Session B (toolcrib):

npm init -y
npx toolcrib init --version 0.1.0 --situation new
# reviewed the staged patches in ./toolcrib-patches/
toolcrib apply

This vendors the component library into ./toolcrib/, wires the #toolcrib import alias into package.json, and stages an AGENTS.md/CLAUDE.md block plus the ai-docs/ folder (CORE.md, NEW_APP.md, component-manifest.json) the toolkit’s own README says an AI assistant should read before building. Those docs were read in full before any component code was written, per their own instructions. Since toolcrib is a React library, this necessarily pulled in React/Vite/TypeScript tooling — a stack difference from Session A’s static HTML that’s a genuine confound (Finding 1’s zero-Tailwind result is partly attributable to a completely different toolchain, not toolcrib alone). Three independent trials (b1, b2, b3), each composed with different component choices (Card vs. CardSimple, Toggle vs. a manual toggle group, different Toolbar slot usage) to avoid just re-running the same file three times.

Caveat carried through every finding below: one model (Claude) built both arms of every trial, with full knowledge of the hypothesis going in. This is not a blinded experiment — a real version of this test would use separate model instances or separate human developers unaware of the comparison being drawn.


Finding 1 — Utility-class soup

TrialTailwind utility-class tokens
a1386
a2307
a3412
A mean368.3
b1, b2, b30, 0, 0

Mann-Whitney U (one-sided, A > B): U = 9.0, p = 0.032 — the smallest p-value obtainable at n=3 vs n=3, so it mostly just confirms zero overlap rather than being independently strong evidence. The stronger claim comes from source, not statistics: no toolcrib component’s TypeScript interface accepts className or style at all, confirmed by tsc --noEmit succeeding on all three B trials with zero style-prop usage. This is a compile-time guarantee, not a per-trial tendency — running more trials wouldn’t add evidence beyond what reading one interface already established with certainty.


Finding 2 — Structural and visual similarity

Structural (wireframe) similarity: 100% — sidebar, toolbar, profile card, notification rows, billing table, sticky footer all present in both conditions, reflecting that one agent mapped the same spec faithfully onto both toolkits.

Visual similarity out of the box: low. Toolcrib defaults to isDarkMode: true, base hue 88° (lime, #89d82f) — nowhere near the light-mode indigo/blue/violet accents unguided sessions converged on independently (hue distance 129°–174° of a possible 180°, inverted background luminance).

Retheming to match, using only ThemeProvider‘s own parameters (no style props):

  • Accent hue, light/dark mode, padding density: one config object, exact match.
  • Corner radius: cornerRadiusMode: 'rounded' reproduces Tailwind’s own rounded/md/lg/xl scale exactly (0.25/0.375/0.5/0.75rem) — not a coincidence, the scale appears deliberately copied.
  • Background neutrality: close but not exact — every surface color is architecturally tied to the accent’s own hue (s=4 residual tint vs. Tailwind’s fully achromatic s=0), a deliberate tradeoff, not a config gap.
  • The secondary palette role: not trivial. Every harmony mode (monochromatic/analogous/split-complementary/triadic/tetradic) hue-shifts it away from the base color by construction — none has a “keep this neutral” option. A plain gray “Cancel” button — trivial in Tailwind at zero saturation — has no equivalent one-line path here; it requires overriding --ai-color-secondary directly, outside the parameter API.

Why exact matching has a ceiling regardless of configuration: toolcrib generates its whole palette procedurally from one seed color. Tailwind’s actual palette doesn’t work that way — checking its real published blue-family stops (500→800) shows hue drifting (217°→226°) and saturation moving non-monotonically (76%→86.6%→82.9%) as Value falls steadily, the signature of independently hand-tuned stops per family, not one formula. Matching is only exact where something reduces to an unchanged scalar (a hue number, a copied radius constant) — never for anything toolcrib has to actually derive.

Methodology note, corrected in hindsight: the retheming above was done by computing HSV values by hand (converting target hex codes, predicting resulting backgrounds via the same formulas the engine uses). That’s not the workflow toolcrib’s own docs describe for this exact task — see Finding 8, which covers ThemeEditor as a live, drag-and-see design tool meant to replace this kind of manual color math entirely. The numbers above still hold; they likely overstate the real effort involved.


Finding 3 — Uniform restyling

Task: accent color, corner radius, and density changed app-wide.

ToolcribUnguided (Tailwind)
Edit6 lines, 1 file (ThemeProvider params)~20 separate find/replace rules across scattered literal classes
Structural/decorative judgment neededNo — Avatar/Toggle hardcode their own radius (--ai-avatar-radius, literal 50%), decoupled from the shared corner-radius token by constructionYes — every rounded-full instance needs manual inspection to tell “circle” from “card corner”
ResultClean, uniform, every instance updatedA real, reproducible bug: sequential text-rewrite rules matched each other’s output — px-2/px-3/px-4 (three distinct roles: table cells, inputs, buttons) all collapsed into a single px-5, silently destroying the original spacing hierarchy

The mechanism: Tailwind classes are independent literal strings with no shared reference — “restyle uniformly” decomposes into N rewrites that must not collide with each other. Toolcrib’s theme has one value per axis, read by every component from a shared source — nothing for a second rule to collide with.


Finding 4 — turn count for styling changes

Finding 3 measured the size of a restyle edit. A different question: how many conversational turns does it take to reach an actually-correct end state, not just a completed-looking one?

Toolcrib: bounded at one turn. The restyle in Finding 3 was a single edit, and when an actual mistake was made in this session while producing it — an earlier attempt used an invalid literal (paddingMode="loose", not one of the three real values) — tsc --noEmit caught it immediately, in the same turn, before anything was reported as done. The correction happened inside the attempt itself, via a compiler forcing function, not as a separate turn triggered by someone noticing later.

Unguided: the first turn can complete “successfully” while silently wrong, with no bound on when that’s discovered. The same restyle’s sed-cascade bug (Finding 3) is the direct evidence: the script ran without error, the HTML rendered without error, and the task was reportable as done after one turn — while three distinct spacing roles had actually collapsed into one, undetected by anything automated. No compiler, linter, or test in that stack flags a semantically-wrong-but-syntactically-valid utility class. That defect surfaced in this report only because this evaluation went back and specifically diffed spacing token counts across the before/after files — a deliberate audit step, not something the original restyle turn would have triggered on its own. In an ordinary session, correctness would depend entirely on whether and when a human happens to notice the visual regression: could be the next turn, could be never.

ToolcribUnguided
Turns to a reportable result11
Turns to a verified-correct result1 (compiler gates it before completion)1 + N, N indeterminate (depends on a human noticing)
What catches a mistaketsc --noEmit, in-turnNothing automated; a later, separate review pass

This is the same asymmetry as the discovery-cost point in the Verdict, scoped specifically to the styling-change task Finding 3 already measured: toolcrib’s guarantee isn’t just that the edit is smaller, it’s that the edit’s correctness is checked before the turn ends, where the unguided equivalent has no such gate at all.


Finding 5 — Row-level and other narrow customization

Toolcrib’s components fall into two categories:

  • Slot/child/render-based (Card, Form, Toolbar, DataTable‘s own cell render): a plain, fully-stylable wrapper element is always one JSX node away. The “no style/className” rule only restricts the toolkit’s own component props here — your own elements inside are unrestricted.
  • Internally-owned DOM never exposed as a prop or slot: here the restriction genuinely bites. DataTable‘s row (<tr>/<td>) is now the one exception, closed via rowSubtheme?: (record, index) => 'error'|'success'|'warning'|'info'|undefined — the row-level equivalent of the subtheme prop already used on Button/Toast/Progress. Flagging a row (e.g. a pending invoice) is one line, tinting the row’s real background/border with the same WCAG-guaranteed colors used everywhere else in the system — not a per-cell approximation. Rows with no match render exactly as before.

Deliberately not added: a generic rowClassName/rowStyle escape hatch — that would reopen exactly the raw-style door the toolkit’s uniform-restyle guarantee (Finding 3) depends on staying closed. The boundary that remains: a row needing flagging for a non-semantic reason (“recently edited,” “selected”) still has no hook, since only the four existing subthemes are covered. General lesson for any other component with the same shape of internal-DOM restriction: check whether it exposes a slot before assuming a workaround is needed.


Finding 6 — Token cost

Toolcrib’s own recommended reading set (CORE.md + NEW_APP.md + component-manifest.json) is ≈83.5K characters (≈21K tokens, chars/4 estimate) of onboarding cost per fresh session — Tailwind needs none.

Output cost is shape-dependent, not uniformly cheaper or more expensive:

TaskUnguidedToolcrib
Global restyle (color/corners/density, app-wide)Full-file regen — no single edit point233 chars, one config block
Row-level flag (semantic: error/success/warning/info)~35 chars, one class~79 chars, one prop — comparable
Row-level flag (non-semantic category)~35 chars, one classNo hook exists; would require a new theme slice — a materially bigger task

Breakeven for the onboarding cost against repeated global restyles: roughly 11 cycles (≈1,880 tokens saved per restyle against the ≈21K upfront cost). Caveats: chars/4 is a rough proxy, not a real tokenizer; prompt caching could push the effective onboarding cost toward zero on repeat sessions against the same project; output tokens typically price higher than input tokens on API pricing, which isn’t reflected in the raw counts above.


Finding 7 — WCAG contrast enforcement

Every dynamically-generated palette color — primary/accent, all four subthemes, on-fill button/badge text — is nudged via ensureWCAGContrast()/pickReadableTextColor() to clear a minimum contrast ratio (3.0:1 for large text/UI, 4.5:1 for normal text) against its actual background, regardless of which base hue is chosen. Verified across five tested hues (lime/indigo/teal/blue-600/red), including --ai-text-secondary against all three surface tiers (bgPrimary/bgSurface/bgContainer) — every combination clears 4.5:1 or better.

For comparison: one of three independent unguided trials shipped text-gray-400 on white at 2.54:1 — failing WCAG AA outright, with nothing in an unguided session catching it. That’s the exact class of defect this machinery is built to prevent, and does, for every color it touches.


Finding 8 — dynamic theme customization has two consumers, not one

Every finding above (except the retheme exercise in Finding 2) concerns build-time decisions locked in once. A different question: what does it take to let someone other than the original build-time decision-maker pick or change the theme, with their choice persisting? There are two distinct someones, and toolcrib’s own docs treat them as two separate, intentional use cases for the same infrastructure:

  1. The app’s own designer/developer, at development time. NEW_APP.md‘s own guidance: “Drop <ThemeEditor trigger={...}> somewhere reachable early in development… use it to pick the base colour, harmony mode, and spacing scale interactively. Once you’re happy, read the resulting parameters off useTheme()… Don’t hand-pick colours.” This is a live, visual alternative to exactly the manual HSV computation Finding 2’s retheme exercise did by hand (colorsys.rgb_to_hsv, computing predicted backgrounds via Python) — the documented workflow is drag sliders, watch the app update live, read off the result. Finding 2 didn’t use it, and in hindsight that means the real dev-time cost of matching a target palette is likely lower than the “one config object” already measured there, since the intended path removes the color-math step entirely rather than just shortening it.
  2. An end user of the deployed app, at runtime. This is what the rest of Finding 8 below actually tested.

Toolcrib: one component serves both roles, pre-built. ThemeEditor (hosted in a SlideOut/Modal/Popup of your choosing — it has no overlay chrome of its own) bundles continuous H/S/V sliders, 6 shipped presets, a “your saved themes” library (named save/load/delete via localStorage), and file export/import, all wired to the same ThemeProvider state validated in Finding 7. Wiring it into the settings screen and adding auto-persistence via the existing theme:changed event-bus channel cost 17 lines / 660 characters across two files — and every color it can produce, whether picked by a designer tuning the shipped default or an end user personalizing later, still goes through ensureWCAGContrast()/pickReadableTextColor().

Unguided: neither role has an equivalent tool. A developer manually edits hex codes in utility classes and reloads to see the result — no live preview, no interactive picker, nothing beyond what Finding 2’s own manual HSV math demonstrates. An end user has nothing at all unless a bespoke picker is built from scratch, tested next:

Unguided end-user customization has to be engineered from scratch, and the accessibility guarantee doesn’t come for free. Tailwind’s utility classes are static — runtime end-user color customization means abandoning most literal color classes for CSS-variable-driven arbitrary values (bg-[var(--accent)]), hand-rolling shade derivation (a “hover” and a light tint via plain RGB math), and hand-rolling persistence. Built the equivalent for the same settings screen: 35 lines / 2,729 characters, with a single flat accent color, no presets, no named saved-theme library, no file export — and critically, no contrast checking of any kind.

That gap isn’t hypothetical. Tested 4 plausible end-user picks (a light yellow, mint green, pale pink, light gray) against the unguided version’s text-white Save button:

End-user pickWhite-text contrast ratioResult
Light yellow #fde0471.32:1Unreadable
Mint green #86efac1.40:1Unreadable
Pale pink #fbcfe81.38:1Unreadable
Light gray #e5e7eb1.24:1Unreadable

All four fail outright — not edge cases, the default outcome for any light pick with the natural unguided implementation. Toolcrib’s pickReadableTextColor() provably reaches ≥4.58:1 against any background (it’s a closed-form choice between pure black/white, not a per-color check that can be forgotten) — the same guarantee applies automatically to a color the end user picked, not just ones a developer chose and could manually spot-check.


Finding 9 — components are load-bearing architecture, not styling convenience

Every finding above treats toolcrib’s components as, in effect, styled <div>s — measuring how easy they are to color, restyle, and keep accessible. But components like Modal, AlertDialog, SlideOut, TabStrip, and Splitter also carry real behavioral infrastructure that has nothing to do with color: focus management, portal rendering, cross-tree state coordination, and layout-aware geometry. Tested directly with a concrete feature, not just described.

Task: add a Delete button to each row of the billing table. Clicking it opens a confirmation dialog — not nested in the row itself, since the table may be virtualized or paginated — that must correctly trap focus while open, dismiss on Escape, and return focus to the row’s Delete button on close.

Toolcrib: the row’s button calls aiBus.openAlertDialog('confirm-delete-invoice', { invoice: record.invoice }) — one line, no callback prop threaded through DataTable at all. A single <AlertDialog> mounted once at the app root listens for that id and renders the confirmation. Total: 31 lines / 1,231 characters, and every one of the following came from AlertDialog wrapping Radix UI’s AlertDialogPrimitive correctly, not from anything hand-rolled in this feature:

  • Full focus trap while open (verified: Radix’s own accessible-dialog behavior, not toolcrib-original logic — toolcrib’s actual contribution here is guaranteeing this primitive gets used via its own anti-pattern guidance, “don’t hand-roll a popup/modal/drawer,” rather than reimplementing focus-trapping itself).
  • Escape-to-close, matching native confirm() convention (confirmed against Radix’s own source per the component’s code comments — outside-click is deliberately disabled instead, appropriate for a destructive-action dialog, not an oversight).
  • Correct ARIA dialog semantics and focus restoration to the triggering element on close.

Unguided: built the same feature — a delete button per row, a confirmation dialog, cancel/confirm — with no special accessibility instruction given, matching how an ordinary request would actually be phrased. 32 lines / 2,019 characters (comparable cost, prop-drilling avoided here only because a flat HTML file’s global function scope happens to sidestep it — a real componentized React app without an equivalent to aiBus would need actual prop-drilling or lifted state, making this an underestimate of the real unguided cost). Checked the result directly for the four standard behaviors above, not assumed:

BehaviorFound in unguided implementation?
Any .focus() call (focus trap)0 occurrences
Any keydown/keyup/Escape handling0 occurrences
role="dialog", aria-modal, aria-labelledby, or aria-describedby0 occurrences
Any tracking of the triggering element (focus restoration)0 occurrences

Not “missing some” — missing all four, confirmed by direct inspection of the code produced, the same style of check as Finding 1’s className/style grep. A keyboard user could still tab through the page behind this “modal,” nothing closes it via Escape, a screen reader has no indication a dialog opened at all, and focus is simply lost on close rather than returned to the trigger. This is the same class of silent, uninstructed omission demonstrated in Findings 7 and 8, in a different subsystem: not a color contrast failure, a keyboard-accessibility and screen-reader failure, equally undetectable by anything in the unguided build process itself.

This isn’t one component’s incidental correctness — it’s a systematic architectural choice, verified across the whole library. Checked which of toolcrib’s 30 components sit directly on a named Radix primitive versus which are hand-rolled markup:

CountExamples
Built on a Radix primitive18 of 30 (60%)Modal(Dialog), AlertDialog, Popup(Popover), SlideOut(Portal), TabStrip(Tabs), DropdownMenu, ContextMenu, Select, RadioGroup, Slider, Accordion, Collapsible, Toast, Tooltip, Avatar, Separator, ToggleGroup, Checkbox/Switch
Hand-rolled, no Radix foundation12 of 30 (40%)DataTable, Splitter, Card/CardSimple, AppShell, Toolbar, UIGroup, layout primitives (Grid/Stack/Content), ThemeEditor, AIErrorBoundary

This correlates exactly with where this report already found gaps, not by coincidence: DataTable — the one component whose internal row markup had no escape hatch at all until Finding 5’s patch — is in the hand-rolled column. TabStrip, by contrast, sits on Radix’s Tabs primitive and inherits its roving-tabindex keyboard model and ARIA tab semantics for free. This also sharpens a speculative note in Threats to Validity below: Splitter (also hand-rolled, no Radix) is the more plausible candidate for a similar internal-DOM gap to DataTable‘s; TabStrip (Radix-backed) is less so, on the same logic that predicted DataTable‘s gap in the first place. For 60% of the library, accessible focus management, keyboard navigation, and ARIA semantics aren’t something toolcrib re-implements per component and could get subtly wrong each time — they’re inherited once, from one well-tested foundation, everywhere that foundation is used.

The generalizable point: a component library earns “load-bearing” status when removing it would require reimplementing correctness-critical behavior, not just re-picking colors. aiBus (cross-tree dispatch without prop-drilling or context restructuring), the layout-domain system (LayoutDomainContext/useLayoutDomain, which lets a <Card> nested inside a <Splitter.Panel> automatically square its corner adjacent to the resize handle — geometry-aware behavior no styling system provides), and the systematic Radix foundation underneath 60% of the component set are all in this category. None of them are visible in a screenshot comparison; all of them are visible the moment a feature requires cross-component coordination or correct keyboard/screen-reader behavior, which most real features eventually do.


Finding 10 — the docs are AI-maintained, and what that does and doesn’t imply

All of toolcrib’s ai-docs/*.md are themselves AI-generated and AI-maintained — only the repo’s root README isn’t. That reframes every gap in Findings 5/7 above (not Finding 8, which found no gap — its shipped capability worked as documented): not a scattered collection of unrelated bugs, but one recurring shape — documentation and tooling produced for AI consumption, by an AI, without something adversarially trying to use it the way a consumer would. Generation and adversarial-use-checking are different activities; being capable of one doesn’t guarantee the other happened.

Where this needs tempering: this is a maintenance-process observation, not evidence of an inevitable failure loop. A human is still the one deciding a gap matters enough to fix and directing the correction — which is exactly how the five patches referenced throughout this report came to exist: gaps surfaced by actually trying to build something, a human decided they were worth institutionalizing rather than left as one-off workarounds, and verified patches came out the other side. That’s the normal corrective mechanism any human-governed codebase relies on, AI-authored docs or not.

What this doesn’t guarantee: if toolcrib’s maintenance pipeline regenerates these docs again without incorporating this specific pattern, a structurally similar gap could reappear somewhere else — a different component’s manifest entry, a different anti-pattern the table doesn’t cover yet. The patches in this report close the specific gaps this evaluation happened to hit; they don’t retroactively audit every other component for the same shape of issue.

Worth noting for calibration: every automated check that existed throughout this evaluation caught real regressions reliably — tsc --noEmit, the 212-test suite, and the doc-drift checks all passed cleanly on every patch. Those checks are good at “does generated output match what the generator would produce” and “does existing behavior still work.” That’s a different thing from “is the generated output complete” — which is exactly the class of gap that only surfaced by trying to build the settings screen, hitting a wall, and going looking for why.


Patch verification methodology

The five patches referenced above (WCAG contrast fix, manifest type-definition resolution across two files, DataTable.rowSubtheme, and the matching Anti-Patterns doc entry) were verified against the real upstream repository, not just written and assumed correct:

  1. Fresh clone of github.com/escape-llc/toolcrib (not the pre-existing working copy the patches were drafted against).
  2. Applied all five diffs in sequence via git apply --check followed by git apply — confirmed each applies cleanly with zero conflicts, in dependency order, on the clean checkout.
  3. Dependency install: npm install. One unrelated environment finding surfaced here: the repo pins "typescript": "^7.0.2", but that package’s root export (. → lib/version.cjs) is now just a version stub — the actual Compiler API moved to typescript/unstable/ast and sibling subpaths. The pre-existing, unmodified scripts/lib/extract.js (classic ts.createSourceFile/ts.ScriptTarget API, used throughout the file, not only by these patches) doesn’t run against that as installed. Verification below used typescript@5.7 installed locally (--no-save, not committed to the project) purely to execute and test the patches; this incompatibility is separate from anything in the patches themselves and would need its own fix.
  4. Typecheck: npx tsc --noEmit → clean, whole project.
  5. Manifest regeneration: node scripts/generate-manifest.js --write → 32 component(s), 39 event channel(s), 10 helper method(s), 156 CSS variable(s), including 8 newly-resolved $defs entries (Column plus 7 others with the same latent type-flattening gap: AccordionItemData, CardOverrides, MenuItemData, RadioOption, SelectOptionData, TabItem, ToggleGroupOption).
  6. Docs regeneration: node scripts/generate-docs.js --write → regenerates ai-docs/CORE.md cleanly, rowSubtheme present in both the auto-generated Component Reference table and the newly-added Anti-Patterns row.
  7. Drift checks (both must report no difference between generated output and what’s committed): node scripts/generate-docs.js --check → “ai-docs/CORE.md matches the template + source exactly”; node scripts/generate-manifest.js --check → “component-manifest.json matches source exactly”.
  8. Full test suite: npx vitest run → 48 test files, 212 tests, all passing — including the project’s own docsInSync.test.ts (which independently re-runs the drift checks above as part of its assertions) and DataTable.test.tsx (6/6, confirming the new rowSubtheme prop doesn’t change behavior for rows that don’t set it) and harmonies.test.ts (4/4, confirming the WCAG patch doesn’t break existing theme-generation tests).

No step above was skipped or assumed to pass based on the patch’s own internal logic looking correct — each ran against the actual real source tree, post-patch, before being reported as verified.


Threats to validity

  1. Same-agent bias. One model built both arms of every trial, aware of the hypothesis throughout. No blinding. A rigorous version of this test would use separate model instances or separate human developers unaware of the comparison being drawn.
  2. N=3 per condition for the stochastic metrics (Tailwind token counts) — enough to demonstrate the effect, not a rigorous population estimate. Doesn’t matter for the Tailwind-count result specifically (a type-system guarantee, not a sample statistic), but does matter for anything not backed by an equivalent structural proof.
  3. Stack confound. Toolcrib forces React/TSX/Vite; Session A was free to pick anything and defaulted to static HTML + Tailwind CDN. Some of every result above is inseparable from “different tooling entirely,” not purely “toolcrib vs. nothing.”
  4. No real browser rendering. A true pixel-level visual diff (Playwright/Chromium) was attempted and blocked by this environment’s network allowlist. All visual-similarity findings are derived analytically from theme/CSS source values, not screenshots.
  5. Token cost estimates are approximate (chars/4), not measured from real API usage logs or an actual tokenizer.
  6. The DataTable row-hook gap (Finding 5, now patched) was one example of one component. Finding 9’s Radix-foundation survey narrows this rather than resolving it: Splitter (also hand-rolled, no Radix) is now the more plausible candidate for a similar internal-DOM gap; TabStrip (Radix-backed) is less so. That’s an informed prediction from architectural pattern, not a verified finding — Splitter itself wasn’t tested the way DataTable was.
  7. Finding 9’s accessible-dialog behavior was tested for one component pair (AlertDialog/DataTable) and verified by static inspection of the generated code (.focus() calls, ARIA attributes present), not by driving an actual browser with a screen reader or keyboard-only navigation. The same network restriction that blocked visual screenshot testing (Threat 4) would also block that kind of live behavioral verification here.

Further work

None of the following would change the verdict — they’d sharpen the decimals:

  • Real screenshot-based visual diff (needs network access to a Chromium download host, or a pre-installed browser).
  • Blinded trials across genuinely separate model instances or human developers, to remove same-agent bias.
  • Real API-log token costs in place of the chars/4 proxy, ideally with prompt caching enabled to measure the true amortized onboarding cost.
  • An unguided-retheming test — does a session using toolcrib reach for ThemeEditor/initialParameters without being told to, the way the unguided sessions reached for Tailwind without being told to? (Out of scope here: theming is treated as a human-in-the-loop decision point in both conditions, not something either tool should autonomously do.)
  • Larger-N global-restyle trials to firm up the token-cost breakeven estimate.
  • A survey of which other toolcrib components expose internally-owned, unreachable DOM (like DataTable‘s row used to) versus which already expose enough slots that the constraint never bites.

Bottom line

Toolcrib removes ad hoc styling from its own components by construction, and that removal is real, uniform, and doesn’t have a style-prop backdoor anywhere in the toolkit’s own API surface. It doesn’t remove styling flexibility from your own code — a plain wrapper inside a slot is unrestricted — and the one place the restriction used to have no answer at all (a data table’s own row markup) now has one, scoped to the four existing semantic categories rather than reopened as a general escape hatch.

It also turns a category of feature that’s genuinely hard to build well — end-user-facing runtime theme customization with guaranteed accessible contrast — into a near-zero-cost addition, where the unguided equivalent isn’t just more code, it’s code that fails silently the moment a real user picks a plausible color.

None of that is the whole story, and treating it as a styling toolkit undersells what’s actually load-bearing in it. aiBus (cross-tree dispatch without prop-drilling), the Radix foundation underneath 60% of its components (Finding 9), and the layout-domain corner-squaring system are not styling conveniences — they’re what stands between “confirmation dialog” and a dialog that silently fails at keyboard access and screen-reader semantics, the same silent-failure pattern demonstrated for color throughout this report, just in a subsystem a sighted developer testing with a mouse is far less likely to ever notice is broken. An unguided build of that same feature had zero of the four standard accessible-dialog behaviors. Evaluating toolcrib only on how it handles color, and stopping there, would have missed roughly half of what this evaluation actually found.

Adopt it where the actual pain is inconsistent, hard-to-globally-restyle output across many contributors or iterations, where interaction correctness (focus management, cross-tree state, keyboard/screen-reader access) matters as much as appearance, and where a project can accept React/TSX as the price of entry. The accessibility floor is a genuine bonus most unguided output has no equivalent of, extending automatically to colors an end user picks and to every component built on the Radix foundation, not just ones a developer happened to get right. Expect the remaining friction to be narrow and specific — non-semantic per-instance styling treatments on components that don’t expose a slot for them, and the ~40% of the component set (DataTable, Splitter, and similar) that doesn’t inherit the Radix foundation — rather than pervasive.

The real cost comparison isn’t “tokens to generate the first draft.” It’s tokens (and turns, and calendar time) to discover a defect exists at all, since nothing in an unguided session flags one on its own — followed by an indeterminate number of correction turns once someone finally does. Toolcrib doesn’t eliminate that discovery cost; the five patches in this report all took real, deliberate effort to find. What it changes is what happens after: a fix closes structurally, once, for every future project — where an unguided project’s fix is local to that one file, and the next unguided session re-risks the identical mistake with nothing carried forward.

If one sentence has to carry this report past the specifics of any one library: unconstrained, unguided output is not a stylistic inconvenience, it’s an unmonitored failure surface, in styling and in interaction correctness alike — this evaluation produced a silent functional regression, a genuine WCAG failure, a 100% failure rate on plausible end-user color picks, and a confirmation dialog with zero of four standard accessible-dialog behaviors, all without the build process itself raising a single flag in any case. Whether the fix is toolcrib, a different design-token system, automated accessibility testing in CI, headless accessible primitives as the default rather than hand-rolled markup, or a human review step that actually happens every time — the specific mechanism matters far less than the fact that some mechanism needs to exist, covering behavior as much as appearance. This evaluation’s evidence is about what happens when none does.