# 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
<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:

```tsx
<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`:

```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 `<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.**

```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<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:

```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/<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.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.
