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:
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.
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}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.cssreset.cssFor example, you can install
normalize.css via npm and import it at the beginning of the globals.css file:1npm install normalize.css1/* app/globals.css */
2@import 'normalize.css';
3
4/* Rest of global styles */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.
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.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 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}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.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}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}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 ×
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}In real-world applications, both approaches are most commonly used:
Global styles for:
CSS Modules for:
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 └──Besides global styles and CSS Modules, Next.js 15 also supports other styling approaches:
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}Next.js also supports Sass/SCSS. Simply install
sass:1npm install sassThen 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}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}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}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}Maintain a consistent structure
app/globals.cssComponentName.module.css)Use CSS variables for recurring values
Create utility classes for frequently used styles
.text-center, .mb-2, itp.Use comments for better organization
1/*
2 * TYPOGRAPHY
3 * -------------
4 */
5
6/*
7 * LAYOUT
8 * -------------
9 */Apply CSS methodologies (if needed)
Prefer composition over inheritance
Next.js 15 automatically optimizes CSS:
<head> tagFor larger applications, it is worth considering:
Below we present an example of an extended component system using CSS Modules in a Next.js 15 application:
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// 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// 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}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:
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.