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
!importantcreates 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:
| Library | Type | Performance |
|---|---|---|
| Tailwind CSS | Utility classes | Zero runtime |
| CSS Modules | Scoped CSS | Zero runtime |
| vanilla-extract | Build-time CSS-in-JS | Zero runtime |
| Panda CSS | Build-time CSS-in-JS | Near-zero runtime |
| StyleX (Meta) | Compile-time CSS | Zero 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
| Problem | Recommended Solution |
|---|---|
| Specificity conflicts | CSS Modules / Tailwind / CSS Layers |
| Layout shifts | Explicit dimensions, aspect-ratio, font size-adjust |
| Responsive issues | Mobile-first, container queries, min()/clamp() |
| Z-index chaos | Token-based z-index scale + portals |
| Dark mode flash | Inline <head> script + CSS custom properties |
| CSS-in-JS perf | Zero-runtime solutions (Tailwind, vanilla-extract) |
| Janky animations | Animate only transform and opacity |