/* Base styles apply to the SMALLEST screens by default */
.card {
display: flex;
flex-direction: column;
padding: 16px;
}
/* Then progressively ADD styles for larger screens */
@media (min-width: 768px) {
.card {
flex-direction: row;
padding: 24px;
}
}"Mobile-first" means writing your base CSS for the smallest screen, then layering on adjustments as the screen grows using min-width media queries โ generally more maintainable than starting from desktop and fighting to compress things down.
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
}repeat(auto-fill, minmax(220px, 1fr)) is a genuinely powerful one-liner: it automatically fits as many 220px-minimum columns as will comfortably fit the container's current width, reflowing seamlessly from a single column on mobile to several on desktop โ with zero media queries needed for the grid itself.
function NavBar() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<nav>
<button
className="hamburger"
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-label="Toggle menu"
>
โฐ
</button>
<div className={isMenuOpen ? "menu open" : "menu"}>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</div>
</nav>
);
}This combines Chapter 12's useState with conditional class names (Chapter 6) โ the CSS itself handles hiding the hamburger button on desktop and the full menu on mobile via media queries, while React state tracks whether the mobile menu is currently open.
function ProductCard({ product, loading }) {
if (loading) {
return <div className="skeleton-card"></div>; // a gray pulsing placeholder shape
}
return <div className="card">{product.name}</div>;
}
// Dark mode via a data attribute + CSS variables
function ThemeToggle() {
const [theme, setTheme] = useState("light");
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Toggle</button>;
}A skeleton (a gray placeholder shaped like the real content) feels noticeably smoother than a blank screen or spinner during loading (Chapter 21). Combined with genuinely designed empty and error states (Chapter 9), these small polish details are what separate a tutorial project from a production-quality one.
Retrofitting a desktop-first layout to work on small screens usually results in cramped, awkward compromises. Starting with mobile-first CSS (as shown above) forces you to prioritize essential content from the beginning, and scaling up to larger screens is almost always easier than the reverse.
Build a simple responsive card component using CSS Grid's auto-fill/minmax pattern that automatically reflows from one column to multiple as the viewport widens.
function ProductGrid({ products }) {
return (
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: "16px"
}}>
{products.map(p => (
<div key={p.id} style={{ padding: "16px", border: "1px solid #ccc" }}>
{p.name}
</div>
))}
</div>
);
}
Q Should I use a CSS framework like Tailwind, or write plain CSS?
Both are valid, common approaches in real React projects. Plain CSS (or CSS Modules) gives full control with no dependency; utility frameworks like Tailwind speed up development once you're comfortable with their class-naming conventions. Neither is 'more correct' โ it's a team/project preference.
Q What's the difference between a loading skeleton and a spinner?
Both indicate loading, but a skeleton mimics the actual shape of the content that's about to appear (cards, text lines), which research shows feels faster and less jarring to users than a generic spinner, since the layout doesn't suddenly shift once content loads.