We use cookies to enhance your experience on the site
CodeWorlds

Global Styles and CSS Modules in Next.js 15

Welcome to Quantum Metropolis, the city of the future, where every element of the urban space has its own unique style, yet the entire city maintains a cohesive aesthetic. Just like in this futuristic city, in Next.js 15 applications we need both global styling rules that define the overall look, and local, modular styles that prevent conflicts and maintain order.

In this chapter, we will explore two key approaches to styling in Next.js 15:

  1. Global styles - affecting the entire application
  2. CSS Modules - enabling local, encapsulated component styling

Global Styles in Next.js 15

In Quantum Metropolis, fundamental infrastructure elements such as the transportation system, lighting, and signage have a uniform appearance throughout the city. Similarly, in web applications we need global styling rules for basic elements such as typography, colors, margins, and grids.

The global.css File

In Next.js 15, global styles are most commonly defined in the

app/globals.css
file, which is then imported in the main application layout. Here is what a typical structure looks like:

1// app/layout.tsx
2import './globals.css';
3import type { Metadata } from 'next';
4
5export const metadata: Metadata = {
6  title: 'Metropolis Quantum',
7  description: 'Your gateway to the city of the future',
8};
9
10export default function RootLayout({
11  children,
12}: {
13  children: React.ReactNode;
14}) {
15  return (
16    <html lang="pl">
17      <body>{children}</body>
18    </html>
19  );
20}

The contents of the

globals.css
file might look like this:

1/* app/globals.css */
2:root {
3  --primary-color: #3a86ff;
4  --secondary-color: #8338ec;
5  --accent-color: #ff006e;
6  --background-light: #f8f9fa;
7  --background-dark: #212529;
8  --text-light: #f8f9fa;
9  --text-dark: #212529;
10  --spacing-unit: 0.5rem;
11  --border-radius: 0.375rem;
12}
13
14* {
15  box-sizing: border-box;
16  padding: 0;
17  margin: 0;
18}
19
20html,
21body {
22  max-width: 100vw;
23  overflow-x: hidden;
24  font-family: 'Quantum Sans', sans-serif;
25}
26
27body {
28  color: var(--text-dark);
29  background: var(--background-light);
30}
31
32a {
33  color: var(--primary-color);
34  text-decoration: none;
35}
36
37h1, h2, h3, h4, h5, h6 {
38  font-family: 'Quantum Display', sans-serif;
39  margin-bottom: var(--spacing-unit);
40}
41
42.container {
43  width: 100%;
44  max-width: 1200px;
45  margin: 0 auto;
46  padding: 0 var(--spacing-unit);
47}
48
49@media (prefers-color-scheme: dark) {
50  body {
51    color: var(--text-light);
52    background: var(--background-dark);
53  }
54}

CSS Reset and Normalization

It is often a good practice to start with a so-called CSS Reset or normalization, which eliminates differences between default styles across different browsers. You can use popular solutions such as:

  • normalize.css
  • reset.css
  • Custom solution

For example, you can install

normalize.css
via npm and import it at the beginning of the globals.css file:

1npm install normalize.css
1/* app/globals.css */
2@import 'normalize.css';
3
4/* Rest of global styles */

Using CSS Variables

As you can see in the example above, CSS variables (custom properties) are a powerful tool for defining global values that can then be used throughout the application:

1:root {
2  --primary-color: #3a86ff;
3  --spacing-large: 2rem;
4}
5
6.button {
7  background-color: var(--primary-color);
8  padding: var(--spacing-large);
9}

This approach allows for easy management of color palettes, spacing, font sizes, and other recurring values.

Global Styles for Specific Routes

In Next.js 15, you can also define global styles that will be applied only to specific routes or layouts. This is done through

layout.tsx
files in specific directories:

1// app/dashboard/layout.tsx
2import '../dashboard-styles.css';
3
4export default function DashboardLayout({
5  children,
6}: {
7  children: React.ReactNode;
8}) {
9  return <div className="dashboard-container">{children}</div>;
10}

These styles will only be applied to pages inside the

/dashboard
directory.

CSS Modules: Style Encapsulation

In Quantum Metropolis, each district has its own unique aesthetic - the Quantum District is full of futuristic, holographic elements, while the Old Town combines modernity with retro architecture. Despite these differences, all districts form a cohesive whole.

CSS Modules works similarly - it allows creating local, encapsulated styles for components that will not affect the rest of the application.

CSS Modules Basics

CSS Modules is a technique that automatically creates unique CSS class names, preventing conflicts. In Next.js 15, every file with the

.module.css
extension is treated as a CSS module.

Creating and using a CSS module:

1/* Button.module.css */
2.button {
3  padding: 0.75rem 1.5rem;
4  border-radius: 0.375rem;
5  font-weight: 600;
6  transition: all 0.2s;
7}
8
9.primary {
10  background-color: var(--primary-color);
11  color: white;
12}
13
14.secondary {
15  background-color: var(--secondary-color);
16  color: white;
17}
18
19.button:hover {
20  transform: translateY(-2px);
21  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
22}
1// components/Button.tsx
2import styles from './Button.module.css';
3
4interface ButtonProps {
5  variant?: 'primary' | 'secondary';
6  children: React.ReactNode;
7  onClick?: () => void;
8}
9
10export default function Button({ variant = 'primary', children, onClick }: ButtonProps) {
11  return (
12    <button 
13      className={`${styles.button} ${styles[variant]}`}
14      onClick={onClick}
15    >
16      {children}
17    </button>
18  );
19}

How CSS Modules Work

When the application is built, Next.js processes

.module.css
files and generates unique class names. For example, the
.button
class from the above example may be transformed into
.Button_button__XYZ123
. Thanks to this, even if you define a class with the same name in another component, there will be no conflict.

Accessing Global CSS Variables

CSS Modules can use global CSS variables defined in

globals.css
:

1/* Card.module.css */
2.card {
3  background-color: white;
4  border-radius: var(--border-radius);
5  padding: calc(var(--spacing-unit) * 2);
6  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
7}

Composing Classes

CSS Modules allows composing classes using the

composes
syntax:

1/* Text.module.css */
2.base {
3  font-family: 'Quantum Sans', sans-serif;
4  line-height: 1.5;
5}
6
7.heading {
8  composes: base;
9  font-weight: 700;
10  line-height: 1.2;
11}
12
13.paragraph {
14  composes: base;
15  margin-bottom: 1rem;
16}

Dynamic Styles with CSS Modules

You can dynamically apply classes depending on the component state:

1// components/Alert.tsx
2import { useState } from 'react';
3import styles from './Alert.module.css';
4
5interface AlertProps {
6  type: 'info' | 'success' | 'warning' | 'error';
7  message: string;
8}
9
10export default function Alert({ type, message }: AlertProps) {
11  const [isVisible, setIsVisible] = useState(true);
12
13  if (!isVisible) return null;
14
15  return (
16    <div className={`${styles.alert} ${styles[type]} ${isVisible ? styles.visible : ''}`}>
17      <p>{message}</p>
18      <button 
19        className={styles.closeButton}
20        onClick={() => setIsVisible(false)}
21      >
22        &times;
23      </button>
24    </div>
25  );
26}
1/* Alert.module.css */
2.alert {
3  padding: 1rem;
4  border-radius: var(--border-radius);
5  margin-bottom: 1rem;
6  display: flex;
7  justify-content: space-between;
8  align-items: center;
9}
10
11.info {
12  background-color: #e3f2fd;
13  color: #0d47a1;
14}
15
16.success {
17  background-color: #e8f5e9;
18  color: #1b5e20;
19}
20
21.warning {
22  background-color: #fff3e0;
23  color: #e65100;
24}
25
26.error {
27  background-color: #ffebee;
28  color: #b71c1c;
29}
30
31.visible {
32  opacity: 1;
33  transition: opacity 0.3s ease-in;
34}
35
36.closeButton {
37  background: none;
38  border: none;
39  font-size: 1.5rem;
40  cursor: pointer;
41  opacity: 0.7;
42}
43
44.closeButton:hover {
45  opacity: 1;
46}

Combining Global Styles with CSS Modules

In real-world applications, both approaches are most commonly used:

  1. Global styles for:

    • CSS resets
    • Basic typography elements
    • Variables (colors, spacing, shadows, etc.)
    • Utility classes
  2. CSS Modules for:

    • Specific components
    • Elements that may have different styles depending on context

Example: Project Structure

1app/
2├── layout.tsx
3├── page.tsx
4├── globals.css
5├── dashboard/
6│   ├── layout.tsx
7│   ├── page.tsx
8│   └── dashboard.module.css
9└── components/
10    ├── Button/
11    │   ├── Button.tsx
12    │   └── Button.module.css
13    ├── Card/
14    │   ├── Card.tsx
15    │   └── Card.module.css
16    └──

Alternative Styling Approaches in Next.js 15

Besides global styles and CSS Modules, Next.js 15 also supports other styling approaches:

1. Styled JSX

Next.js has built-in support for Styled JSX, which allows creating local styles directly in components:

1export default function Button() {
2  return (
3    <>
4      <button>Kliknij mnie</button>
5      <style jsx>{`
6        button {
7          padding: 0.5rem 1rem;
8          background-color: #3a86ff;
9          color: white;
10          border: none;
11          border-radius: 0.25rem;
12        }
13        button:hover {
14          background-color: #2563eb;
15        }
16      `}</style>
17    </>
18  );
19}

2. Sass/SCSS

Next.js also supports Sass/SCSS. Simply install

sass
:

1npm install sass

Then you can create

.scss
or
.sass
files, including CSS modules with the
.module.scss
extension:

1// Button.module.scss
2$primary-color: #3a86ff;
3
4.button {
5  padding: 0.75rem 1.5rem;
6  background-color: $primary-color;
7  
8  &:hover {
9    background-color: darken($primary-color, 10%);
10  }
11  
12  .icon {
13    margin-right: 0.5rem;
14  }
15}

3. CSS-in-JS

Next.js also supports CSS-in-JS libraries such as

styled-components
and
emotion
. For these libraries, additional configuration is needed to work with SSR (Server-Side Rendering):

1// components/StyledButton.tsx
2import styled from 'styled-components';
3
4const StyledButton = styled.button`
5  padding: 0.75rem 1.5rem;
6  background-color: #3a86ff;
7  color: white;
8  border: none;
9  border-radius: 0.25rem;
10  
11  &:hover {
12    background-color: #2563eb;
13  }
14`;
15
16export default function Button({ children }) {
17  return <StyledButton>{children}</StyledButton>;
18}

For

styled-components
to work with SSR, you need to create your own document in Next.js:

1// app/registry.tsx
2'use client';
3
4import { useState } from 'react';
5import { useServerInsertedHTML } from 'next/navigation';
6import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
7
8export default function StyledComponentsRegistry({
9  children,
10}: {
11  children: React.ReactNode;
12}) {
13  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
14
15  useServerInsertedHTML(() => {
16    const styles = styledComponentsStyleSheet.getStyleElement();
17    styledComponentsStyleSheet.instance.clearTag();
18    return <>{styles}</>;
19  });
20
21  if (typeof window !== 'undefined') return <>{children}</>;
22
23  return (
24    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
25      {children}
26    </StyleSheetManager>
27  );
28}
1// app/layout.tsx
2import StyledComponentsRegistry from './registry';
3import './globals.css';
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <html>
12      <body>
13        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
14      </body>
15    </html>
16  );
17}

Practical Examples

Example 1: Creating a Grid System

1/* app/globals.css */
2.row {
3  display: flex;
4  flex-wrap: wrap;
5  margin: 0 -1rem;
6}
7
8.col {
9  padding: 0 1rem;
10}
11
12.col-1 { flex: 0 0 8.333%; max-width: 8.333%; }
13.col-2 { flex: 0 0 16.666%; max-width: 16.666%; }
14.col-3 { flex: 0 0 25%; max-width: 25%; }
15.col-4 { flex: 0 0 33.333%; max-width: 33.333%; }
16.col-5 { flex: 0 0 41.666%; max-width: 41.666%; }
17.col-6 { flex: 0 0 50%; max-width: 50%; }
18.col-7 { flex: 0 0 58.333%; max-width: 58.333%; }
19.col-8 { flex: 0 0 66.666%; max-width: 66.666%; }
20.col-9 { flex: 0 0 75%; max-width: 75%; }
21.col-10 { flex: 0 0 83.333%; max-width: 83.333%; }
22.col-11 { flex: 0 0 91.666%; max-width: 91.666%; }
23.col-12 { flex: 0 0 100%; max-width: 100%; }
24
25@media (max-width: 768px) {
26  .col-md-12 { flex: 0 0 100%; max-width: 100%; }
27  .col-md-6 { flex: 0 0 50%; max-width: 50%; }
28  .col-md-4 { flex: 0 0 33.333%; max-width: 33.333%; }
29}
30
31@media (max-width: 576px) {
32  .col-sm-12 { flex: 0 0 100%; max-width: 100%; }
33  .col-sm-6 { flex: 0 0 50%; max-width: 50%; }
34}

Usage in a component:

1// app/page.tsx
2export default function Home() {
3  return (
4    <div className="container">
5      <div className="row">
6        <div className="col col-4 col-md-6 col-sm-12">
7          <div className="card">Kolumna 1</div>
8        </div>
9        <div className="col col-4 col-md-6 col-sm-12">
10          <div className="card">Kolumna 2</div>
11        </div>
12        <div className="col col-4 col-md-12 col-sm-12">
13          <div className="card">Kolumna 3</div>
14        </div>
15      </div>
16    </div>
17  );
18}

Example 2: Styling Forms

1/* Form.module.css */
2.form {
3  margin-bottom: 2rem;
4}
5
6.formGroup {
7  margin-bottom: 1rem;
8}
9
10.label {
11  display: block;
12  margin-bottom: 0.5rem;
13  font-weight: 500;
14}
15
16.input {
17  display: block;
18  width: 100%;
19  padding: 0.75rem;
20  font-size: 1rem;
21  border: 1px solid #ddd;
22  border-radius: var(--border-radius);
23  background-color: white;
24}
25
26.input:focus {
27  outline: none;
28  border-color: var(--primary-color);
29  box-shadow: 0 0 0 3px rgba(58, 134, 255, 0.2);
30}
31
32.error {
33  color: var(--accent-color);
34  font-size: 0.875rem;
35  margin-top: 0.25rem;
36}
37
38.submitButton {
39  composes: button from './Button.module.css';
40  composes: primary from './Button.module.css';
41}

Best Practices

  1. Maintain a consistent structure

    • Global styles in
      app/globals.css
    • CSS modules alongside components
    • Consistent file naming (e.g.,
      ComponentName.module.css
      )
  2. Use CSS variables for recurring values

    • Colors
    • Spacing
    • Font sizes
    • Border radius
    • Shadows
  3. Create utility classes for frequently used styles

    • .text-center
      ,
      .mb-2
      , itp.
  4. Use comments for better organization

    1/* 
    2 * TYPOGRAPHY
    3 * -------------
    4 */
    5
    6/* 
    7 * LAYOUT
    8 * -------------
    9 */
  5. Apply CSS methodologies (if needed)

    • BEM (Block Element Modifier)
    • SMACSS (Scalable and Modular Architecture for CSS)
    • OOCSS (Object-Oriented CSS)
  6. Prefer composition over inheritance

    • Combine small, single-purpose classes instead of creating large, complex selectors

Performance and Styling

Next.js 15 automatically optimizes CSS:

  1. During production build, CSS is minified
  2. CSS Modules are processed and unique class names are generated
  3. Critical styles are automatically injected into the
    <head>
    tag

For larger applications, it is worth considering:

  1. Code splitting CSS
    • CSS is split into smaller files and loaded only when needed
    • In Next.js, this happens automatically for CSS Modules
  2. Using tools to eliminate unused CSS
    • PurgeCSS
    • Unused CSS (ucss)

Extended Examples and Applications

Component System Using CSS Modules

Below we present an example of an extended component system using CSS Modules in a Next.js 15 application:

  1. Directory structure:
1components/
2├── ui/
3│   ├── Button/
4│   │   ├── Button.tsx
5│   │   └── Button.module.css
6│   ├── Card/
7│   │   ├── Card.tsx
8│   │   └── Card.module.css
9│   ├── Input/
10│   │   ├── Input.tsx
11│   │   └── Input.module.css
12│   └──
13├── layout/
14│   ├── Header/
15│   │   ├── Header.tsx
16│   │   └── Header.module.css
17│   ├── Footer/
18│   │   ├── Footer.tsx
19│   │   └── Footer.module.css
20│   └──
21└──
  1. Button Component with different variants:
1// components/ui/Button/Button.tsx
2import styles from './Button.module.css';
3import { ReactNode } from 'react';
4
5type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'text';
6type ButtonSize = 'sm' | 'md' | 'lg';
7
8interface ButtonProps {
9  variant?: ButtonVariant;
10  size?: ButtonSize;
11  children: ReactNode;
12  onClick?: () => void;
13  disabled?: boolean;
14  fullWidth?: boolean;
15  icon?: ReactNode;
16  iconPosition?: 'left' | 'right';
17  className?: string;
18}
19
20export function Button({
21  variant = 'primary',
22  size = 'md',
23  children,
24  onClick,
25  disabled = false,
26  fullWidth = false,
27  icon,
28  iconPosition = 'left',
29  className = '',
30}: ButtonProps) {
31  const buttonClasses = [
32    styles.button,
33    styles[variant],
34    styles[size],
35    fullWidth ? styles.fullWidth : '',
36    icon ? styles.hasIcon : '',
37    icon && iconPosition === 'right' ? styles.iconRight : '',
38    disabled ? styles.disabled : '',
39    className
40  ].filter(Boolean).join(' ');
41
42  return (
43    <button 
44      className={buttonClasses}
45      onClick={onClick}
46      disabled={disabled}
47    >
48      {icon && iconPosition === 'left' && (
49        <span className={styles.icon}>{icon}</span>
50      )}
51      <span className={styles.text}>{children}</span>
52      {icon && iconPosition === 'right' && (
53        <span className={styles.icon}>{icon}</span>
54      )}
55    </button>
56  );
57}
1/* components/ui/Button/Button.module.css */
2.button {
3  display: inline-flex;
4  align-items: center;
5  justify-content: center;
6  font-weight: 500;
7  border: none;
8  border-radius: var(--border-radius);
9  cursor: pointer;
10  transition: all 0.2s ease;
11  font-family: inherit;
12}
13
14/* Variants */
15.primary {
16  background-color: var(--primary-color);
17  color: white;
18}
19
20.primary:hover {
21  background-color: var(--primary-dark);
22}
23
24.secondary {
25  background-color: var(--secondary-color);
26  color: white;
27}
28
29.secondary:hover {
30  background-color: var(--secondary-dark);
31}
32
33.tertiary {
34  background-color: var(--tertiary-color);
35  color: white;
36}
37
38.tertiary:hover {
39  background-color: var(--tertiary-dark);
40}
41
42.outline {
43  background-color: transparent;
44  border: 1px solid var(--primary-color);
45  color: var(--primary-color);
46}
47
48.outline:hover {
49  background-color: rgba(58, 134, 255, 0.1);
50}
51
52.text {
53  background-color: transparent;
54  color: var(--primary-color);
55  padding-left: 0;
56  padding-right: 0;
57}
58
59.text:hover {
60  background-color: transparent;
61  color: var(--primary-dark);
62  text-decoration: underline;
63}
64
65/* Sizes */
66.sm {
67  font-size: 0.875rem;
68  padding: 0.5rem 1rem;
69}
70
71.md {
72  font-size: 1rem;
73  padding: 0.75rem 1.5rem;
74}
75
76.lg {
77  font-size: 1.125rem;
78  padding: 1rem 2rem;
79}
80
81/* Modifiers */
82.fullWidth {
83  width: 100%;
84}
85
86.hasIcon {
87  gap: 0.5rem;
88}
89
90.iconRight {
91  flex-direction: row-reverse;
92}
93
94.disabled {
95  opacity: 0.6;
96  cursor: not-allowed;
97}
98
99.disabled:hover {
100  background-color: var(--primary-color);
101}
102
103.outline.disabled:hover {
104  background-color: transparent;
105}
106
107.text.disabled:hover {
108  background-color: transparent;
109  text-decoration: none;
110}
111
112.icon {
113  display: flex;
114  align-items: center;
115  justify-content: center;
116}
117
118.button .text {
119  display: inline-block;
120}
  1. Using the Button component:
1// app/examples/page.tsx
2import { Button } from '@/components/ui/Button/Button';
3import { FiDownload, FiHeart, FiArrowRight } from 'react-icons/fi';
4
5export default function ButtonExamplesPage() {
6  return (
7    <div className="container">
8      <h1>Button Examples</h1>
9      
10      <h2>Warianty</h2>
11      <div className="button-group">
12        <Button variant="primary">Primary</Button>
13        <Button variant="secondary">Secondary</Button>
14        <Button variant="tertiary">Tertiary</Button>
15        <Button variant="outline">Outline</Button>
16        <Button variant="text">Text</Button>
17      </div>
18      
19      <h2>Rozmiary</h2>
20      <div className="button-group">
21        <Button size="sm">Small</Button>
22        <Button size="md">Medium</Button>
23        <Button size="lg">Large</Button>
24      </div>
25      
26      <h2>Z ikonami</h2>
27      <div className="button-group">
28        <Button icon={<FiDownload />}>Pobierz</Button>
29        <Button icon={<FiHeart />} variant="outline">Polub</Button>
30        <Button icon={<FiArrowRight />} iconPosition="right">
31          Continue
32        </Button>
33      </div>
34      
35      <h2>Full Width</h2>
36      <Button fullWidth>Full Width Button</Button>
37      
38      <h2>Disabled</h2>
39      <div className="button-group">
40        <Button disabled>Disabled Button</Button>
41        <Button variant="outline" disabled>Disabled Outline</Button>
42      </div>
43    </div>
44  );
45}

Summary

Styling in Next.js 15 is flexible and allows for various approaches, from global styles to encapsulated CSS Modules. Just like in Quantum Metropolis, where the aesthetic of the entire city is cohesive but each district retains its uniqueness, in Next.js applications we can combine global rules with local component styles.

Key takeaways:

  1. Global styles are ideal for defining general rules, variables, and utility classes
  2. CSS Modules provide local styling without conflicts, ideal for components
  3. Next.js 15 also supports Sass, Styled JSX, and CSS-in-JS libraries
  4. Good style organization practices increase readability and facilitate code maintenance

In the next chapter, we will look at integration with Tailwind CSS, a popular utility-first CSS framework that can significantly speed up the styling process in Next.js 15.

Go to CodeWorlds