Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
307 lines
22 KiB
Markdown
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*
|