# 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](../UI_ROADMAP_2026_V3_CROSS_REPO.md).
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.**
```tsx
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:
```tsx
```
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`:
```tsx
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 `` + filtered `
`.
**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.**
```tsx
import { TagInput, Combobox } from '@bytelyst/ui';
```
See `/showcase/ui/tag-input-combobox` for the canonical demo.
---
## 4. Raw API responses inside React state
**Smell.**
```tsx
const [data, setData] = useState(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:
```tsx
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:
```tsx
import { Reveal, StaggerList, Spotlight, Magnetic, MeshBackground, Parallax } from '@bytelyst/motion';
```
For hand-rolled animation, gate the listener:
```ts
import { prefersReducedMotion } from '@bytelyst/motion';
if (prefersReducedMotion()) return;
```
---
## 7. SSR-unsafe ID generation
**Smell.**
```tsx
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:
```tsx
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.
```ts
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//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 ``
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.md` will
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.