Files
Ready2Blob/.planning/research/PITFALLS.md
T

307 lines
22 KiB
Markdown

# Pitfalls Research
**Domain:** UI polish overhaul -- Material Design 3, dark mode, accent colors added to existing Tailwind v4 + React wizard app
**Researched:** 2026-03-31
**Confidence:** HIGH (based on codebase analysis + verified Tailwind v4 docs + community patterns)
---
## Critical Pitfalls
### Pitfall 1: Tailwind v4 Dark Mode Requires CSS-First Config, Not tailwind.config.js
**What goes wrong:**
Developers reach for `tailwind.config.js` with `darkMode: 'class'` which does not exist in Tailwind v4. The app currently has only `@import "tailwindcss";` in `index.css` with no config file at all. Using v3 dark mode patterns produces zero effect and wastes debugging time.
**Why it happens:**
Most tutorials and Stack Overflow answers still reference Tailwind v3 syntax. Tailwind v4 moved to a fully CSS-first configuration model. The `darkMode` config key is gone.
**How to avoid:**
Add the `@custom-variant` directive in `index.css` for class-based toggling:
```css
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
```
This enables manual toggle via a `.dark` class on `<html>`. The `:where()` wrapper keeps specificity at zero, preventing cascade conflicts. Verified against [Tailwind v4 dark mode docs](https://tailwindcss.com/docs/dark-mode).
**Warning signs:**
- `dark:` prefixed classes have no visible effect
- Dark mode only responds to OS preference, not the toggle button
**Phase to address:**
Phase 1 (Theme Foundation) -- this must be the very first CSS change before any `dark:` classes are added to components.
---
### Pitfall 2: Hardcoded Color Values Across 73 className Usages
**What goes wrong:**
The codebase has 73 `className=` usages with hardcoded Tailwind color classes (`text-gray-700`, `border-gray-300`, `focus:ring-blue-300`, `text-red-500`, `bg-gray-50`, etc.). Adding dark mode by appending a `dark:` counterpart to every single one creates unreadable className strings and guarantees missed spots -- invisible text, invisible borders, or unreadable error messages against dark backgrounds.
**Why it happens:**
When building light-mode-only, hardcoded color classes are natural. The cost is deferred until dark mode arrives. Developers add `dark:` to the visible components and miss the less-obvious ones (help text, error messages, placeholders, disabled states).
**How to avoid:**
Define semantic CSS custom properties (design tokens) mapped to Tailwind's `@theme` directive:
```css
@theme {
--color-surface: #ffffff;
--color-on-surface: #1a1a1a;
--color-primary: #2563eb;
--color-error: #dc2626;
}
```
Then use `bg-surface`, `text-on-surface` throughout components. Dark mode changes the token values once (on `.dark`), not every component. This is the Material Design 3 approach (surface, on-surface, primary, on-primary, etc.).
**Warning signs:**
- `dark:` classes appearing in JSX alongside light classes, creating 100+ character className strings
- Text disappearing on dark backgrounds during manual testing
- Error messages (`text-red-600`) becoming unreadable against dark backgrounds
**Phase to address:**
Phase 1 (Theme Foundation) defines tokens. Phase 2 (Component Overhaul) replaces hardcoded colors with token references.
---
### Pitfall 3: Dark Mode Color Contrast Failures (WCAG AA)
**What goes wrong:**
Text that passes 4.5:1 contrast in light mode fails in dark mode. The most common failures: gray help text on dark gray backgrounds, red error text on dark surfaces, blue links on dark blue-gray backgrounds. The current app uses `text-gray-500` for help text and `text-red-600` for errors -- both will fail against typical dark backgrounds.
**Why it happens:**
Developers assume inverting colors preserves contrast ratios. They do not. Concrete example from this codebase: `text-gray-500` (#6b7280) on `bg-white` (#ffffff) gives 4.6:1 contrast -- barely passing AA. The same `text-gray-500` on `bg-gray-900` (#111827) gives only 3.5:1 -- failing AA for normal text.
**How to avoid:**
Define separate color values per theme within the token system. In dark mode, help text must use a lighter gray (equivalent of `text-gray-400`), errors must use a lighter red (equivalent of `text-red-400`). Semantic tokens centralize these mappings so each value is defined once. Verify every text/background pair with the browser DevTools accessibility panel or WebAIM contrast checker against WCAG AA 4.5:1 minimum for normal text, 3:1 for large text.
**Warning signs:**
- Help text feels "hard to read" in dark mode during visual review
- Browser DevTools accessibility audit flagging contrast ratios below 4.5:1
- Error states visually blending into background colors
**Phase to address:**
Phase 1 (Token Definition) for color values. Phase 2 (Component Overhaul) for application. Each component restyle must include a contrast verification before marking complete.
---
### Pitfall 4: Breaking 131 Test Selectors During Component Restyling
**What goes wrong:**
The test suite uses 131 occurrences of `getByText`, `getByRole`, `getByTestId`, `getByLabelText`, and `queryBy` selectors across 5 test files (App.test.tsx, StepIndicator.test.tsx, ReviewStep.test.tsx, RemoteConfigStep.test.tsx, BackendSelectionStep.test.tsx). Restyling components breaks tests by: changing visible text content, wrapping elements in new containers that alter DOM hierarchy, replacing native elements with styled equivalents (changing roles), or removing/renaming aria attributes.
**Why it happens:**
UI overhauls touch the same JSX that tests query. Specific examples from this codebase:
- `getByText(/Backend/)` in StepIndicator.test.tsx breaks if the label text changes or gets wrapped in a `<span>` that splits the text node
- `getAllByRole('button')` breaks if buttons become styled `<a>` tags or `<div>` elements
- `screen.findByText('2', { selector: '[data-testid="step"]' })` breaks if data-testid attributes are renamed during refactoring
**How to avoid:**
1. Run the full 159-test suite after every single component change, not in a batch at the end.
2. Restyle one component, verify tests, commit. Never batch-restyle all components then fix all tests.
3. When restructuring JSX, preserve text content and element roles. A `<button>` must remain a `<button>`.
4. If adding wrapper elements, ensure text nodes are not split (e.g., `getByText(/Backend/)` matches a single text node, not text across siblings).
**Warning signs:**
- More than 3 test failures appearing simultaneously after a restyle
- Tests failing with "Unable to find element" errors
- `getByRole` queries returning unexpected counts
**Phase to address:**
Every phase -- each component change must include a "159 tests green" gate. This is the single most likely source of rework.
---
### Pitfall 5: Flash of Unstyled Content (FOUC) on Dark Mode Load
**What goes wrong:**
The app loads with light mode CSS, then JavaScript runs and toggles the `.dark` class, causing a visible white flash. For IT professionals who often use dark OS themes, this flash is jarring and signals low quality.
**Why it happens:**
React runs after the initial paint. If dark mode preference is stored in `localStorage` and applied via `useEffect` or React state, the first paint is always light mode. The class toggle happens milliseconds later, but the flash is visible.
**How to avoid:**
Add a synchronous inline `<script>` in the `<head>` of `index.html` (before any CSS or React bundle loads):
```html
<script>
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
```
This executes before first paint, preventing any flash. The React toggle component then reads and syncs with the already-applied state.
**Warning signs:**
- White flash visible when loading the app with dark mode previously enabled
- Users on dark OS themes seeing a brief light flash on every page load
**Phase to address:**
Phase 1 (Theme Foundation) -- the FOUC prevention script must ship together with the dark mode toggle implementation, not as a later fix.
---
### Pitfall 6: Theme Context Re-renders Causing Full Wizard Re-render
**What goes wrong:**
Adding a `ThemeContext` that stores `{ theme: 'dark', accentColor: 'blue' }` causes every `useTheme()` consumer to re-render when any theme value changes. Since the app already has a `WizardContext` with `useReducer`, adding another context that triggers re-renders on toggle will cause all 4 wizard steps to re-render, potentially resetting form input focus or scroll position.
**Why it happens:**
React Context re-renders every consumer when the provider value changes (referential equality). Tutorials show `<ThemeProvider value={{ theme, setTheme }}>` where a new object is created on every render. Even with `useMemo`, toggling theme changes the value and re-renders all consumers.
**How to avoid:**
Do NOT store theme in React Context. Apply the theme via the `.dark` CSS class on `<html>` element and CSS custom properties. Theme toggling becomes a DOM class toggle (zero React re-renders). Store the toggle state in a small component-local state that only the toggle button uses:
```tsx
function ThemeToggle() {
const [isDark, setIsDark] = useState(() =>
document.documentElement.classList.contains('dark')
);
const toggle = () => {
document.documentElement.classList.toggle('dark');
setIsDark(d => !d);
localStorage.theme = isDark ? 'light' : 'dark';
};
return <button onClick={toggle}>...</button>;
}
```
Only the toggle button re-renders. No context, no provider, no cascade. Accent color works the same way -- set a CSS variable on `<html>`, no React re-render.
**Warning signs:**
- Form inputs losing focus when toggling dark mode
- Visible flicker across the entire wizard when toggling
- React DevTools profiler showing all components re-rendering on theme change
**Phase to address:**
Phase 1 (Theme Foundation) -- architecture decision: CSS class approach, not React Context for theming.
---
### Pitfall 7: Form Accessibility Regressions During Restyling
**What goes wrong:**
The current FieldRenderer has proper `<label htmlFor>` and `<input id>` associations, error message display, and tooltip buttons with `aria-label`. During restyling, these connections break: labels get separated from inputs by decorative wrapper divs, error messages lose their visual proximity, or card wrappers introduce unexpected tab stops.
**Why it happens:**
Visual-focused restyling treats JSX as a canvas for layout. Developers restructure DOM for card layouts, add icon containers, or wrap form groups in Material-style "outlined" containers. The label-input-error chain relies on specific DOM relationships. The FieldRenderer currently does NOT use `aria-describedby` for error messages -- this is already noted as v1.1 tech debt. Restyling is the right time to fix this, but also the highest risk time to break what works.
**How to avoid:**
1. Fix the `aria-describedby` gap as part of the restyling, not separately. Add `aria-describedby={error ? \`${field.key}-error\` : undefined}` to inputs and `id={\`${field.key}-error\`}` to error paragraphs.
2. After restyling each form component, verify: label click focuses the input, error messages are associated with inputs, tab order follows visual order.
3. Keep the `<label htmlFor={field.key}>` + `<input id={field.key}>` pattern intact regardless of wrapper changes.
**Warning signs:**
- Clicking a label no longer focuses its input
- Tab key skips inputs or gets trapped in decorative elements
- Browser form autofill stops working on restyled inputs
**Phase to address:**
Phase 2 (Component Overhaul) -- every form component restyle must include an accessibility verification step. Resolve the v1.1 aria tech debt item here rather than deferring again.
---
## Technical Debt Patterns
Shortcuts that seem reasonable but create long-term problems.
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|----------|-------------------|----------------|-----------------|
| Adding `dark:` to every className instead of tokens | Fast, no refactor needed | 73+ locations to maintain, every new component needs dual classes | Never -- token approach costs the same upfront and scales |
| Using `!important` to fix specificity issues | Immediate visual fix | Cascading specificity arms race, impossible to override later | Never |
| Storing theme only in React state (not localStorage) | Simpler code | Preference lost on refresh, FOUC on every load | Never -- localStorage + inline script is trivial |
| Skipping contrast verification "will check later" | Faster shipping | Accessibility failures discovered post-ship, painful to retroactively audit all 73 class locations | Never -- check during each component restyle |
| Building a full design system with token categories for every MD3 role | "Complete" spec adherence | Over-engineered for a 4-step wizard with ~10 components; 80% of tokens go unused | Never for this app -- pick the 15-20 tokens that matter |
| Copying MD3 token names verbatim (md-sys-color-surface-container-highest) | Matches Google spec exactly | Verbose, unfamiliar to Tailwind developers, poor DX for a small team | Never -- use simplified semantic names (surface, on-surface, primary) |
| Adding MUI or another component library for "proper" MD3 | Instant MD3 components | +200KB bundle, specificity wars with Tailwind, two styling systems to maintain | Never for this app -- 10 components do not justify a library |
## Integration Gotchas
Common mistakes when connecting theme infrastructure to existing systems.
| Integration | Common Mistake | Correct Approach |
|-------------|----------------|------------------|
| react-hook-form + restyled inputs | Wrapping `<input>` in a custom component that breaks `register()` ref forwarding | Use `React.forwardRef` on any custom input wrapper, or keep native `<input>` with Tailwind classes (preferred for this app) |
| Zod validation + error display | Moving error `<p>` tags away from their input during restyle, breaking visual association | Keep error message immediately after its input in DOM order; add `aria-describedby` |
| WizardContext + theme toggle | Creating a ThemeContext provider that causes WizardProvider consumers to re-render | Theme via CSS class on `<html>` (zero React re-renders), NOT via React context |
| CSS hidden auth toggles (AzureAuthToggle / SftpAuthToggle) | Restyling visible state but forgetting the hidden state, breaking `className="hidden"` pattern | Verify both auth toggle states render correctly in both light and dark modes |
| StepIndicator inline styles | Replacing `style={{ fontWeight: 'bold' }}` with Tailwind classes but altering text content structure | Replace inline styles with Tailwind classes (`font-bold`, `font-normal`, `text-muted`) while keeping text content strings identical for test compatibility |
| BackendCard selection state | Changing selection indicator (e.g., border color) to use tokens but forgetting dark mode variant | Selected card must be visually distinct in both themes; test with all 7 backends |
## Performance Traps
| Trap | Symptoms | Prevention | When It Breaks |
|------|----------|------------|----------------|
| Theme stored in React Context causing re-renders | All 4 wizard steps re-render on every toggle; form focus lost | CSS class on `<html>`, no React context for theme | Immediate on every toggle |
| Importing full component library for 10 components | Bundle doubles (+200KB gzipped for MUI) | Build MD3 styles with Tailwind tokens; zero additional dependencies | Immediate -- slower first load |
| CSS transition on every property during theme switch | 200ms lag on every element when toggling dark mode | Transition only `background-color` and `color` on body; skip borders/shadows | Noticeable with 50+ DOM elements |
| Over-using CSS custom properties on every element | Slow repaints when toggling theme on low-end devices | Define tokens on `:root` / `.dark`, let inheritance cascade naturally | On low-end devices or with 100+ custom properties |
## UX Pitfalls
| Pitfall | User Impact | Better Approach |
|---------|-------------|-----------------|
| Dark mode toggle buried in settings | IT pros who want dark mode cannot find it | Visible toggle in app header, immediately accessible |
| No system preference detection | User has OS dark mode, app loads light | Default to OS preference via `prefers-color-scheme`, with manual override stored in localStorage |
| Accent color picker with unlimited options | Analysis paralysis, clashing colors | 3-5 curated accent colors that all pass contrast checks in both themes |
| Theme transition animation on every element | Jarring, slow, distracting on toggle | Subtle 150ms transition on background-color and color on body only |
| Dark mode applied but OutputBlock code still light | Inconsistent feel in the most important step (Review/download) | OutputBlock must respect dark mode for generated config and script previews |
| Security warning banner lost in dark mode | Users miss the credential security warning before download | Warning must remain high-contrast and prominent (use error/warning token colors) in both modes |
## "Looks Done But Isn't" Checklist
Things that appear complete but are missing critical pieces.
- [ ] **Dark mode select dropdowns:** Browser renders `<option>` elements with OS colors -- white dropdown menus appear on dark backgrounds on some browsers. Verify on Chrome, Firefox, Edge.
- [ ] **Dark mode scrollbars:** Light scrollbars on dark backgrounds look broken. Apply `scrollbar-color` CSS property or use `dark` color-scheme.
- [ ] **Error text contrast:** `text-red-600` on dark backgrounds has insufficient contrast. Must use lighter red (equivalent of `text-red-400`) in dark mode via tokens.
- [ ] **Focus rings in dark mode:** `focus:ring-blue-300` is nearly invisible on dark backgrounds. Must use `focus:ring-blue-500` equivalent in dark mode.
- [ ] **Placeholder text in dark mode:** Light gray placeholder text vanishes on dark input backgrounds. Verify placeholder is visible in both modes.
- [ ] **Security warning banner:** The credential warning in ReviewStep must remain prominent and high-contrast in dark mode (not just "inverted").
- [ ] **Accent color + dark background:** Verify every accent color option still passes WCAG AA 4.5:1 against the dark surface background.
- [ ] **BackendCard hover and selected states:** Card states must be visually distinguishable in both modes with all 7 backends.
- [ ] **Disabled button contrast:** Disabled buttons using lower opacity reduce contrast further on dark backgrounds. Use distinct disabled token colors instead of opacity.
- [ ] **PasswordField show/hide toggle:** The eye icon/button must be visible in both themes.
- [ ] **Tooltip info boxes:** The `bg-blue-50 border-blue-200 text-blue-700` tooltip in FieldRenderer needs a dark mode equivalent that maintains readability.
## Recovery Strategies
When pitfalls occur despite prevention, how to recover.
| Pitfall | Recovery Cost | Recovery Steps |
|---------|---------------|----------------|
| Hardcoded colors everywhere (no tokens) | MEDIUM | Extract to CSS variables in one pass, then find-replace all 73 className usages. ~2 hours for this codebase. |
| Test suite broken by batch restyle | LOW-MEDIUM | `git stash` the batch change, restyle one component at a time verifying tests between each. |
| FOUC on dark mode | LOW | Add 5-line inline script to `index.html <head>`. 10-minute fix. |
| Specificity conflicts from component library | HIGH | Remove component library, rebuild styles with Tailwind. Prevention is far cheaper than recovery. |
| Accessibility regressions in forms | MEDIUM | Audit with browser accessibility tools, fix label/input/aria associations. Harder to find than to fix. |
| Dark mode contrast failures | LOW-MEDIUM | With centralized tokens: update token values once. Without tokens: hunt through all 73 className usages. |
| Theme context re-renders | LOW | Remove ThemeContext, move to CSS class approach. ~30 minutes if caught early. |
## Pitfall-to-Phase Mapping
| Pitfall | Prevention Phase | Verification |
|---------|------------------|--------------|
| Tailwind v4 dark mode config (P1) | Phase 1: Theme Foundation | `dark:bg-gray-900` toggles correctly via `.dark` class on `<html>` |
| Hardcoded colors (P2) | Phase 1 (Tokens) + Phase 2 (Overhaul) | Zero hardcoded Tailwind color classes remain in component JSX |
| Dark mode contrast failures (P3) | Phase 2: Component Overhaul | Every text/background pair checked, all pass WCAG AA 4.5:1 |
| Breaking test selectors (P4) | Every phase | All 159 tests pass after each individual component restyle |
| FOUC (P5) | Phase 1: Theme Foundation | Load app with `localStorage.theme = 'dark'`, verify no white flash |
| Theme context re-renders (P6) | Phase 1: Architecture Decision | Theme toggle causes zero React re-renders outside the toggle button itself |
| Form accessibility regressions (P7) | Phase 2: Component Overhaul | Label-input associations verified, `aria-describedby` added for all error messages |
| CSS specificity conflicts | Phase 1: Architecture Decision | Decision documented: no component library, Tailwind-only approach |
| Over-engineering design system | Phase 1: Token Definition | Token count stays under 20 semantic colors; no unused token categories |
## Sources
- [Tailwind CSS v4 Dark Mode docs](https://tailwindcss.com/docs/dark-mode) -- official, verified (HIGH confidence)
- [Tailwind v4 upgrade discussion #16517](https://github.com/tailwindlabs/tailwindcss/discussions/16517) -- community reports of broken dark mode after upgrade
- [Tailwind specificity discussion #12714](https://github.com/tailwindlabs/tailwindcss/discussions/12714) -- class collisions with component libraries
- [BOIA: Dark Mode and WCAG Contrast](https://www.boia.org/blog/offering-a-dark-mode-doesnt-satisfy-wcag-color-contrast-requirements) -- dark mode does not auto-satisfy WCAG
- [Complete Dark Mode Accessibility Guide (2026)](https://blog.greeden.me/en/2026/02/23/complete-accessibility-guide-for-dark-mode-and-high-contrast-color-design-contrast-validation-respecting-os-settings-icons-images-and-focus-visibility-wcag-2-1-aa/) -- WCAG 2.1 AA guidance for dark mode
- [MUI MD3 adoption discussion #29345](https://github.com/mui/material-ui/issues/29345) -- MD3 implementation complexity
- [React Context performance optimization](https://medium.com/zestgeek/performance-optimization-techniques-with-reacts-usecontext-5dc7e4ef6b25) -- re-render prevention patterns
- Codebase analysis: 73 className usages, 131 test selectors across 5 files, 3 inline styles in StepIndicator, FieldRenderer aria-describedby gap confirmed (HIGH confidence -- direct code inspection)
---
*Pitfalls research for: UI polish overhaul (MD3, dark mode, accent colors) on Ready2Blob v1.2*
*Researched: 2026-03-31*