🎨 Styling, Images & Fonts · Lesson 15
CSS Modules & Global CSS
Styling options in Next.js
| Method | File | Best for |
|---|---|---|
| Global CSS | app/globals.css | Reset, variables, utility classes (.btn, .container) |
| CSS Modules | Card.module.css | Component-specific styles, no name clashes |
| Tailwind CSS | class="p-4 bg-white" | Fast utility-first styling (next lesson) |
| Inline style | style={{ }} | Dynamic one-off values (progress width) |
Global CSS (globals.css)
CSS – app/globals.css
/* app/globals.css (from the EduSite project) */
:root {
--brand: #4f46e5;
--brand-dark: #3730a3;
--ink: #0f172a;
--muted: #475569;
--line: #e2e8f0;
--bg: #f8fafc;
--radius: 16px;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink); line-height: 1.65; }
a { color: var(--brand); text-decoration: none; }
img { max-width: 100%; height: auto; }
.container { width: min(1120px, calc(100% - 32px)); margin: 0 auto; }
.grid { display: grid; gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.btn { display: inline-block; background: var(--brand); color: #fff;
padding: 12px 22px; border-radius: 999px; font-weight: 700; }
.btn:hover { background: var(--brand-dark); }
.badge { background: #eef2ff; color: var(--brand-dark); font-size: .78rem;
padding: 3px 10px; border-radius: 999px; }JSX – import once in the root layout
// app/layout.jsx import './globals.css' // once, at the top – applies to the whole site
CSS variables for a theme
All colours live in :root. Change --brand once and every button, link and badge updates — perfect for a client who says "make it green".
CSS Modules (*.module.css)
CSS – components/CourseCard.module.css
/* components/CourseCard.module.css */
.card { background: #fff; border: 1px solid var(--line);
border-radius: var(--radius); overflow: hidden; transition: transform .2s; }
.card:hover { transform: translateY(-4px); box-shadow: 0 12px 30px rgba(0,0,0,.08); }
.body { padding: 16px 18px; }
.title { margin: 8px 0 4px; font-size: 1.15rem; }
.fee { font-weight: 800; color: var(--brand); }JSX – components/CourseCard.jsx
// components/CourseCard.jsx
import Link from 'next/link'
import styles from './CourseCard.module.css'
export default function CourseCard({ course }) {
return (
<Link href={`/courses/${course.slug}`} className={styles.card}>
<div className={styles.body}>
<span className="badge">{course.level}</span> {/* global class */}
<h3 className={styles.title}>{course.title}</h3> {/* module class */}
<p className={styles.fee}>₹{course.fee}</p>
</div>
</Link>
)
}How CSS Modules avoid clashes
Next.js renames each class to something unique at build time:
Rendered HTML
<a class="CourseCard_card__x7Ab2" href="/courses/nextjs">
<div class="CourseCard_body__p0Qe1">
<h3 class="CourseCard_title__Lm3kZ">Next.js</h3>
Plain CSS
- .card in Card.css
- .card in Blog.css
- Last one wins → bugs
CSS Modules
- CourseCard_card__x7Ab2
- PostCard_card__9kP1s
- Never collide
Responsive mobile menu
CSS – components/Header.module.css
/* components/Header.module.css (EduSite) */
.header { position: sticky; top: 0; z-index: 50; background: rgba(255,255,255,.92);
backdrop-filter: blur(8px); border-bottom: 1px solid var(--line); }
.bar { display: flex; align-items: center; justify-content: space-between; height: 64px; }
.nav { display: flex; gap: 6px; }
.nav a { color: var(--ink); font-weight: 600; padding: 8px 14px; border-radius: 999px; }
.nav a.active { background: var(--brand); color: #fff; }
.toggle { display: none; } /* hamburger hidden on desktop */
@media (max-width: 760px) {
.toggle { display: block; }
.nav { display: none; position: absolute; top: 64px; left: 0; right: 0;
flex-direction: column; background: #fff; padding: 12px 16px; }
.nav.open { display: flex; } /* shown when state open = true */
}JSX – toggle with state
// components/Header.jsx (part)
const [open, setOpen] = useState(false)
...
<button className={styles.toggle} aria-expanded={open} onClick={() => setOpen(!open)}>
{open ? '✕' : '☰'}
</button>
<nav className={`${styles.nav} ${open ? styles.open : ''}`}>
...links
</nav>Conditional class names
JSX – three patterns
// 1) template string
<p className={`${styles.fee} ${course.fee === 0 ? styles.free : ''}`}>
// 2) array join (cleaner with many classes)
<p className={[styles.fee, isFree && styles.free].filter(Boolean).join(' ')}>
// 3) inline style for one-off dynamic values
<div style={{ width: `${progress}%`, background: 'var(--brand)', height: 6 }} />Common mistakes
- Writing
class=instead ofclassName=. - Using
styles.my-card→ hyphens break JS; usestyles['my-card']or camelCase.myCard. - Element selectors in a module (
h1 { }) → modules need a class; put element styles in globals.css. - Forgetting
* { box-sizing: border-box }→ widths overflow on mobile.
Summary
- globals.css (imported in layout) for variables, reset and shared utilities.
- CSS Modules for component styles — class names become unique automatically.
- Media queries + a state toggle give you a mobile menu.
✅ Practice
Create a PostCard component with its own PostCard.module.css. Change --brand to green and see the whole site change.
Frequently Asked Questions
Where can I import global CSS in Next.js?
Global stylesheets can be imported in any layout or page, but the usual place is app/layout.jsx so they load on every page.
Why use CSS Modules instead of plain CSS?
CSS Modules make class names unique per file, so .card in one component never breaks .card in another. They are built into Next.js with no setup.
Can I use Sass?
Yes. Run npm install sass and rename files to .scss or .module.scss. Next.js compiles them automatically.
