Steven's Knowledge
Troubleshooting

Styling

CSS and styling problems in frontend applications with modern solutions

Styling Troubleshooting

CSS issues can be subtle and frustrating — from specificity conflicts to layout inconsistencies across browsers. This guide covers common styling problems and professional solutions.


1. CSS Specificity Conflicts — Styles Not Applying

Problem

/* Component A */
.container .button { color: blue; }

/* Component B — expects red, but blue wins due to higher specificity */
.button { color: red; }
  • Styles override each other unpredictably
  • Adding !important creates an escalation war
  • Styles break when components are reordered in the DOM

Root Cause

CSS global namespace means all selectors compete. Specificity calculation determines winners, not source order when specificity differs.

Solution

CSS Modules — scoped by default:

/* Button.module.css */
.button {
  color: red;
  padding: 8px 16px;
}
import styles from './Button.module.css';

function Button() {
  return <button className={styles.button}>Click</button>;
}
// Renders: <button class="Button_button_x7f2a">Click</button>

Tailwind CSS — utility-first, no specificity issues:

function Button() {
  return (
    <button className="text-red-500 px-4 py-2 hover:text-red-700 transition-colors">
      Click
    </button>
  );
}

CSS Layers for controlling cascade priority:

@layer base, components, utilities;

@layer base {
  button { color: gray; }
}

@layer components {
  .button { color: blue; }  /* Wins over base, loses to utilities */
}

@layer utilities {
  .text-red { color: red; }  /* Always wins — highest layer */
}

2. Layout Shifts — Content Jumping During Load

Problem

  • Images load and push content down
  • Fonts swap causing text reflow
  • Dynamic content insertion shifts layout
  • CLS (Cumulative Layout Shift) score is poor

Solution

Reserve space for images:

// Always specify dimensions
<img
  src="/photo.jpg"
  width={800}
  height={600}
  alt="Photo"
  style={{ aspectRatio: '4/3' }}
/>

// Or use CSS aspect-ratio
<div style={{ aspectRatio: '16/9', width: '100%' }}>
  <img src="/photo.jpg" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</div>

Font loading strategy:

/* Preload critical fonts */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: optional; /* Prevents layout shift — uses fallback if not loaded quickly */
}

/* Size-adjust for fallback font matching */
@font-face {
  font-family: 'CustomFont-Fallback';
  src: local('Arial');
  size-adjust: 105%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}
<link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin />

Skeleton screens for dynamic content:

function ProductCard({ product }: { product?: Product }) {
  if (!product) {
    return (
      <div className="animate-pulse">
        <div className="bg-gray-200 h-48 rounded" />
        <div className="bg-gray-200 h-4 mt-4 w-3/4 rounded" />
        <div className="bg-gray-200 h-4 mt-2 w-1/2 rounded" />
      </div>
    );
  }

  return (
    <div>
      <img src={product.image} width={400} height={300} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  );
}

3. Responsive Design Breakdowns

Problem

  • Layout breaks at certain viewport widths
  • Text overflows containers on mobile
  • Touch targets too small on mobile devices
  • Horizontal scrollbar appears unexpectedly

Solution

Mobile-first breakpoints with container queries:

/* Mobile-first base styles */
.card {
  padding: 1rem;
  display: grid;
  gap: 1rem;
}

/* Tablet and up */
@media (min-width: 768px) {
  .card {
    grid-template-columns: 1fr 1fr;
  }
}

/* Container queries — respond to parent, not viewport */
.card-container {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    grid-template-columns: 1fr 1fr;
  }
}

Prevent text overflow:

.text-content {
  overflow-wrap: break-word;
  word-break: break-word;
  hyphens: auto;

  /* Truncate with ellipsis */
  &.truncate {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Multi-line truncation */
  &.line-clamp {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

Minimum touch target size (WCAG 2.5.8):

button, a, [role="button"] {
  min-width: 44px;
  min-height: 44px;
  padding: 12px;
}

Fix unexpected horizontal scroll:

/* Debug: find the element causing overflow */
* {
  outline: 1px solid red !important;
}

/* Fix: contain overflow at the root */
html, body {
  overflow-x: hidden; /* Last resort */
}

/* Better: find and fix the actual cause */
.problematic-element {
  max-width: 100%;
  box-sizing: border-box;
}

img, video, iframe {
  max-width: 100%;
  height: auto;
}

4. Z-Index Stacking Issues

Problem

.modal { z-index: 9999; }
.tooltip { z-index: 99999; }
.dropdown { z-index: 999999; }
/* Z-index inflation — values keep increasing */

Root Cause

z-index only works within stacking contexts. Creating unintended stacking contexts traps elements.

Solution

Establish a z-index scale system:

:root {
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-overlay: 300;
  --z-modal: 400;
  --z-popover: 500;
  --z-toast: 600;
  --z-tooltip: 700;
}

.modal { z-index: var(--z-modal); }
.tooltip { z-index: var(--z-tooltip); }

Use React portals to escape stacking contexts:

import { createPortal } from 'react-dom';

function Modal({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay" style={{ zIndex: 'var(--z-modal)' }}>
      <div className="modal-content">{children}</div>
    </div>,
    document.body
  );
}

5. Dark Mode Implementation Issues

Problem

  • Colors hardcoded throughout the codebase
  • Flash of wrong theme on page load
  • Third-party components don't respect theme
  • Images and media don't adapt to dark mode

Solution

CSS custom properties with system preference detection:

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-border: #e5e7eb;
  --color-surface: #f9fafb;
  --color-primary: #3b82f6;
}

[data-theme="dark"] {
  --color-bg: #0f172a;
  --color-text: #f1f5f9;
  --color-border: #334155;
  --color-surface: #1e293b;
  --color-primary: #60a5fa;
}

/* Respect system preference as default */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #0f172a;
    --color-text: #f1f5f9;
    --color-border: #334155;
    --color-surface: #1e293b;
    --color-primary: #60a5fa;
  }
}

Prevent flash of wrong theme:

<!-- Inline script in <head> — runs before paint -->
<script>
  const theme = localStorage.getItem('theme') ||
    (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  document.documentElement.setAttribute('data-theme', theme);
</script>

Adaptive images for dark mode:

img.adaptive {
  filter: brightness(0.9) contrast(1.1);
}

[data-theme="dark"] img.adaptive {
  filter: brightness(0.8) contrast(1.2);
}
<picture>
  <source srcset="/hero-dark.png" media="(prefers-color-scheme: dark)" />
  <img src="/hero-light.png" alt="Hero" />
</picture>

6. CSS-in-JS Performance Overhead

Problem

Runtime CSS-in-JS libraries (styled-components, Emotion) can cause:

  • Increased JavaScript bundle size
  • Runtime style computation and injection
  • Hydration mismatches in SSR
  • Double rendering in React 18 Strict Mode

Solution

Migrate to zero-runtime solutions:

LibraryTypePerformance
Tailwind CSSUtility classesZero runtime
CSS ModulesScoped CSSZero runtime
vanilla-extractBuild-time CSS-in-JSZero runtime
Panda CSSBuild-time CSS-in-JSNear-zero runtime
StyleX (Meta)Compile-time CSSZero runtime
// vanilla-extract example — type-safe, zero runtime
import { style } from '@vanilla-extract/css';

export const button = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '8px 16px',
  ':hover': {
    backgroundColor: 'darkblue',
  },
});

7. Animation Performance — Janky Transitions

Problem

Animations are choppy, especially on mobile devices:

/* Triggers layout recalculation on every frame — 60fps impossible */
.animate {
  transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}

Root Cause

Animating width, height, top, left, margin, padding triggers layout recalculation (reflow) on every frame.

Solution

Animate only compositor-friendly properties:

.animate {
  /* ✓ Only animate transform and opacity — GPU-accelerated */
  transition: transform 0.3s ease, opacity 0.3s ease;
  will-change: transform; /* Hint to browser for GPU layer */
}

.animate:hover {
  transform: translateX(10px) scale(1.05);
  opacity: 0.9;
}

Use CSS @keyframes for complex animations:

@keyframes slideIn {
  from {
    transform: translateY(100%);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.modal-enter {
  animation: slideIn 0.3s ease-out forwards;
}

View Transitions API for page-level transitions:

function navigate(url: string) {
  if (!document.startViewTransition) {
    updateDOM(url);
    return;
  }

  document.startViewTransition(() => updateDOM(url));
}

Summary: Styling Best Practices

ProblemRecommended Solution
Specificity conflictsCSS Modules / Tailwind / CSS Layers
Layout shiftsExplicit dimensions, aspect-ratio, font size-adjust
Responsive issuesMobile-first, container queries, min()/clamp()
Z-index chaosToken-based z-index scale + portals
Dark mode flashInline <head> script + CSS custom properties
CSS-in-JS perfZero-runtime solutions (Tailwind, vanilla-extract)
Janky animationsAnimate only transform and opacity

On this page