──────────────────────────────────────────────────────────────────
docs/design-system/ANTIPATTERNS.md (CC.6 — new file)
──────────────────────────────────────────────────────────────────
Twelve anti-patterns codified, every product engineer + AI agent
should treat as a hard 'no':
1. Hard-coded colour / spacing / radius values
2. Bespoke skeleton / spinner / empty-state per surface
3. Bespoke tag editor / searchable select
4. Raw API responses inside React state
5. Hidden privacy / cost / refusal state
6. Motion without prefers-reduced-motion
7. SSR-unsafe ID generation (Math.random)
8. console.log / console.error in production
9. Cross-product imports
10. `any` (especially in public API surfaces)
11. Untested primitives in @bytelyst/*
12. Animations that block keyboard focus
Each entry has Smell → Why it's wrong → Do this instead, with
canonical `@bytelyst/*` references.
──────────────────────────────────────────────────────────────────
packages/ai-ui/src/DebugOverlay.tsx (audit cleanup)
──────────────────────────────────────────────────────────────────
Dropped dead `patchSingleChild` helper. It cloned the single child
unchanged, doing literally nothing — the click handler always lived
on the outer <span>. Replaced the call site with `{children}` and
removed 4 unused React imports (Children, cloneElement,
isValidElement, ReactElement).
Same 98/98 tests pass.
──────────────────────────────────────────────────────────────────
Roadmap flips (this commit + the prior unflag pass)
──────────────────────────────────────────────────────────────────
9.E.1 <Markdown> + citation interop ai-ui@0.6.0
9.E.2 <CodeDiff> split + unified ai-ui@0.6.0
9.E.3 <ExplainThis> ai-ui@0.6.0
9.E.4 usePromptHistory ai-ui@0.6.0
9.E.5 useTokenCount ai-ui@0.6.0
9.E.6 /showcase/ai-ui/markdown showcase
13.C.4 <ProvenanceDrawer> ai-ui@0.5.0+
13.C.5 <DebugOverlay> ai-ui@0.5.0+
13.C.6 <PrivacyBadge> ai-ui@0.5.0+
13.D.1 <Parallax> motion@0.2.1
13.D.5 <TiltGallery> motion@0.2.1
CC.6 ANTIPATTERNS.md docs
MAG.8 /showcase/futurism/debug-overlay showcase
§11.2 counter rewrote (37 / 202 done · 18%)
Wave 9 Data: 9/42 → 15/42 (36%)
Wave 13 Futurism: 9/39 → 17/39 (44%)
Cross-cutting: 0/8 → 1/8 (13%)
Magnet demos: 2/8 → 3/8 (38%)
Three MAG.* magnets are now live:
✨ MAG.1 spatial-hero (Wave 13.D.6)
✨ MAG.3 trust-surfaces (Wave 13.C.7)
✨ MAG.8 debug-overlay (Wave 13.C.5)
7.7 KiB
ByteLyst UI Anti-Patterns
Audience: every engineer and AI agent shipping UI in the ByteLyst ecosystem. Status: living document — published as part of Wave 9 (cross-cutting CC.6). Last refresh: 2026-05-27.
This file catalogues the patterns we do not ship. Each entry has:
- Smell — how to spot it in a PR diff.
- Why it's wrong — the trust / a11y / consistency / perf cost.
- Do this instead — the canonical ByteLyst primitive or pattern.
The full roadmap reference is
UI_ROADMAP_2026_V3_CROSS_REPO.md §3.8.
This document is enforced by code review and (eventually) a custom ESLint
config in @bytelyst/eslint-config-ui (planned, see CC.4).
1. Hard-coded colour / spacing / radius values
Smell.
<div style={{ background: '#6366f1', padding: 12, borderRadius: 8 }} />
className="bg-indigo-500 p-3 rounded-lg"
Why it's wrong. Bypasses the token layer. Themes (dark, hi-contrast, generative branding, per-product accent) cannot affect this surface. Multi-product consistency dies one literal at a time.
Do this instead. Reference tokens by CSS custom property:
<div
style={{
background: 'var(--bl-accent)',
padding: 'var(--bl-space-3)',
borderRadius: 'var(--bl-radius-md)',
}}
/>
For Tailwind users, our token-aware utility plugin emits
bg-accent, p-3 (mapped to var(--bl-space-3)), rounded-md etc.
Never use a raw text-indigo-500.
2. Bespoke skeleton / spinner / empty-state per surface
Smell. A Skeleton.tsx or LoadingSpinner.tsx file inside a product
repo's src/components/.
Why it's wrong. Loading affordances are the most-replicated UI in the ecosystem. Every bespoke variant disagrees on size, easing, accessible labels, and dark-mode contrast. Users learn three "loading" languages.
Do this instead. Import from @bytelyst/ui:
import { Skeleton, SkeletonGroup, LoadingDots, LoadingSpinner, EmptyState } from '@bytelyst/ui';
If a product genuinely needs a custom shape, open an issue on
learning_ai_common_plat to extend the primitive — never fork it.
3. Bespoke tag editor / searchable select
Smell. Custom TagInput.tsx with Enter/comma commit logic; custom
Combobox.tsx with <input> + filtered <ul>.
Why it's wrong. Both surfaces have subtle keyboard / a11y / ARIA
requirements (role="combobox", aria-activedescendant, roving focus)
that bespoke versions almost always botch.
Do this instead.
import { TagInput, Combobox } from '@bytelyst/ui';
See /showcase/ui/tag-input-combobox for the canonical demo.
4. Raw API responses inside React state
Smell.
const [data, setData] = useState<any>(null);
fetch('/api/foo').then((r) => r.json()).then(setData);
Why it's wrong. No request de-dup, no caching, no error/loading states, no SSR hand-off. Every product reinvents the same five mistakes.
Do this instead. TanStack Query / React Query via our wrapper
factory (Wave 11.A). For one-off reads,
@bytelyst/api-client provides a typed fetchJson with auth +
retry baked in.
5. Hidden privacy / cost / refusal state
Smell. A chat surface that doesn't expose where inference happens, how much it costs, or why the model declined.
Why it's wrong. Trust collapses silently. Users discover on-device vs cloud routing the hard way (e.g. usage bills).
Do this instead. Wave 13.C surfaces are required wallpaper for every AI flow:
import {
CostMeter, // visible token + USD readout
ConfidenceTag, // per-answer confidence chip
PrivacyBadge, // on-device / cloud / hybrid pill
RefusalCard, // calm, structured "no"
ProvenanceDrawer, // every step the model took
DebugOverlay, // engineers Shift-click for raw JSON
} from '@bytelyst/ai-ui';
See /showcase/futurism/trust-surfaces (MAG.3).
6. Motion without prefers-reduced-motion
Smell. Any transition, animation, framer-motion, or
requestAnimationFrame loop that runs unconditionally.
Why it's wrong. Vestibular-disorder users (≈ 35 % of the population report some motion sensitivity) suffer. WCAG 2.3.3 mandates respect.
Do this instead. Use the @bytelyst/motion primitives — every one
honours prefers-reduced-motion automatically and accepts a
disableMotion escape hatch for tests:
import { Reveal, StaggerList, Spotlight, Magnetic, MeshBackground, Parallax } from '@bytelyst/motion';
For hand-rolled animation, gate the listener:
import { prefersReducedMotion } from '@bytelyst/motion';
if (prefersReducedMotion()) return;
7. SSR-unsafe ID generation
Smell.
const id = `gradient-${Math.random()}`;
Why it's wrong. Server and client compute different IDs → hydration mismatch → React tears down + re-renders → CLS + console errors.
Do this instead. React.useId() — stable across server + client:
import { useId } from 'react';
const gradientId = useId();
8. Console-error / console-log left in production code
Smell. console.log('debug'), console.error(err).
Why it's wrong. Leaks PII, noises up analytics, fails the secret
scan (see AGENTS.md).
Do this instead. Use the @bytelyst/logger wrapper. Pino-backed,
respects log level + structured fields, ships to Loki via the
ecosystem stack.
import { logger } from '@bytelyst/logger';
logger.info({ userId }, 'starting sync');
9. Cross-product imports
Smell. import { Foo } from '@/../../learning_ai_notes/...' or
deep imports into another product repo.
Why it's wrong. Couples release cycles, breaks tree-shaking, guarantees a circular dependency by Wave 14.
Do this instead. Promote shared code to a @bytelyst/* package.
The bar is low — even single-component packages are fine if two
products need them.
10. any (especially in public API surfaces)
Smell. function useFoo(opts: any): any.
Why it's wrong. Removes the entire reason TypeScript exists. Especially toxic on exported types — every consumer downstream inherits the hole.
Do this instead. Generic over T extends …, use unknown for
truly untyped boundaries, and add explicit type tests in
__tests__/types.test-d.ts for public surfaces.
11. Untested primitives in @bytelyst/*
Smell. A new .tsx in packages/<x>/src/ without a corresponding
test in __tests__/.
Why it's wrong. Shared primitives are the high-blast-radius layer. A regression here ships to 11 products.
Do this instead. Every public export must have at least one
render test + one behaviour test. The current ratio bar is ≈ 4 tests
per primitive (see Wave 13.C: CostMeter 5 · ConfidenceTag 5 ·
RefusalCard 4).
12. Animations that block keyboard focus
Smell. transform: scale(0) on the initial state of a mounted
input or button.
Why it's wrong. The element may be tab-focusable but visually hidden / outside the viewport, trapping keyboard users.
Do this instead. Mount + reveal in two phases (use <Reveal>
which animates opacity + translateY while keeping pointer-events
and tab-order correct), or use inert + aria-hidden for genuinely
hidden surfaces.
Appendix · how to add to this document
- Open a PR with a new section in this file + (if codifiable) a custom
ESLint rule in
@bytelyst/eslint-config-ui. - Add a one-line entry to
UI_ROADMAP_2026_V3_CROSS_REPO.md§3.8. - The cross-repo agent guidelines pointer in every
AGENTS.mdwill pick up your addition on the next refresh.
If you're an AI agent — when you spot a new anti-pattern across two or more product repos, drop a note here and link the commits. Don't silently fix one site; codify the prohibition.