Files

22 KiB

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:

@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.

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:

@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):

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

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 andid={`${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


Pitfalls research for: UI polish overhaul (MD3, dark mode, accent colors) on Ready2Blob v1.2 Researched: 2026-03-31