Witaj w Metropolis Quantum, mieście przyszłości, gdzie każdy element przestrzeni miejskiej ma swój własny, unikalny styl, a jednocześnie całe miasto utrzymuje spójną estetykę. Podobnie jak w tym futurystycznym mieście, w aplikacjach Next.js 15 potrzebujemy zarówno globalnych reguł stylowania, które definiują ogólny wygląd, jak i lokalnych, modułowych stylów, które zapobiegają konfliktom i pozwalają na utrzymanie porządku.
W tym rozdziale poznamy dwa kluczowe podejścia do stylowania w Next.js 15:
W Metropolis Quantum podstawowe elementy infrastruktury, takie jak system transportu, oświetlenie czy oznakowanie, mają jednolity wygląd w całym mieście. Podobnie, w aplikacjach webowych potrzebujemy globalnych reguł stylowania dla podstawowych elementów, takich jak typografia, kolory, marginesy czy siatka.
W Next.js 15 globalne style są najczęściej definiowane w pliku
app/globals.css, który następnie jest importowany w głównym layoucie aplikacji. Tak wygląda typowa struktura:1// app/layout.tsx
2import './globals.css';
3import type { Metadata } from 'next';
4
5export const metadata: Metadata = {
6 title: 'Metropolis Quantum',
7 description: 'Twoja brama do miasta przyszłości',
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}Zawartość pliku
globals.css może wyglądać tak: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}Często dobrą praktyką jest rozpoczęcie od tzw. CSS Reset lub normalizacji, które niwelują różnice między domyślnymi stylami różnych przeglądarek. Możesz użyć popularnych rozwiązań, takich jak:
normalize.cssreset.cssPrzykładowo, można zainstalować
normalize.css za pomocą npm i zaimportować go na początku pliku globals.css:1npm install normalize.css1/* app/globals.css */
2@import 'normalize.css';
3
4/* Reszta globalnych stylów */Jak widać w powyższym przykładzie, zmienne CSS (custom properties) są potężnym narzędziem do definiowania globalnych wartości, które można następnie wykorzystać w całej aplikacji:
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}To podejście pozwala na łatwe zarządzanie paletą kolorów, odstępami, rozmiarami czcionek i innymi powtarzającymi się wartościami.
W Next.js 15 możesz również definiować style globalne, które będą stosowane tylko do określonych ścieżek lub layoutów. Służą do tego pliki
layout.tsx w poszczególnych katalogach: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}Te style będą stosowane tylko do stron wewnątrz katalogu
/dashboard.W Metropolis Quantum każda dzielnica ma swoją unikalną estetykę - Dystrykt Kwantowy pełen jest futurystycznych, holograficznych elementów, podczas gdy Stare Miasto łączy nowoczesność z architekturą retro. Mimo tych różnic, wszystkie dzielnice stanowią spójną całość.
CSS Modules działa podobnie - pozwala na tworzenie lokalnych, hermetycznych stylów dla komponentów, które nie będą wpływać na resztę aplikacji.
CSS Modules to technika, która automatycznie tworzy unikalne nazwy klas CSS, zapobiegając konfliktom. W Next.js 15 każdy plik z rozszerzeniem
.module.css jest traktowany jako moduł CSS.Tworzenie i używanie modułu CSS:
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}Gdy aplikacja jest budowana, Next.js przetwarza pliki
.module.css i generuje unikalne nazwy klas. Na przykład, klasa .button z powyższego przykładu może zostać przekształcona w .Button_button__XYZ123. Dzięki temu, nawet jeśli w innym komponencie zdefiniujesz klasę o tej samej nazwie, nie dojdzie do konfliktu.CSS Modules mogą korzystać z globalnych zmiennych CSS zdefiniowanych w
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 pozwala na komponowanie klas za pomocą składni
composes: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}Możesz dynamicznie stosować klasy w zależności od stanu komponentu:
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}W rzeczywistych aplikacjach najczęściej stosuje się oba podejścia:
Globalne style dla:
CSS Modules dla:
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 └──Oprócz globalnych stylów i CSS Modules, Next.js 15 wspiera również inne podejścia do stylowania:
Next.js ma wbudowane wsparcie dla Styled JSX, które pozwala na tworzenie stylów lokalnych bezpośrednio w komponentach:
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 wspiera również Sass/SCSS. Wystarczy zainstalować
sass:1npm install sassNastępnie możesz tworzyć pliki
.scss lub .sass, w tym moduły CSS z rozszerzeniem .module.scss: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 wspiera również biblioteki CSS-in-JS, takie jak
styled-components czy emotion. W przypadku tych bibliotek, potrzebne jest dodatkowe ustawienie, aby działały z 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}Aby
styled-components działały z SSR, musisz utworzyć własny dokument w 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}Wykorzystanie w komponencie:
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}Utrzymuj spójną strukturę
app/globals.cssComponentName.module.css)Używaj zmiennych CSS dla wartości powtarzających się
Twórz klasy użytkowe dla często używanych stylów
.text-center, .mb-2, itp.Używaj komentarzy dla lepszej organizacji
1/*
2 * TYPOGRAPHY
3 * -------------
4 */
5
6/*
7 * LAYOUT
8 * -------------
9 */Stosuj metodologie CSS (jeśli potrzebne)
Preferuj kompozycję nad dziedziczeniem
Next.js 15 automatycznie optymalizuje CSS:
<head>W przypadku większych aplikacji, warto rozważyć:
Poniżej przedstawiamy przykład rozwiniętego systemu komponentów wykorzystujących CSS Modules w aplikacji Next.js 15:
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>Przykłady przycisków</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 Przejdź dalej
32 </Button>
33 </div>
34
35 <h2>Pełna szerokość</h2>
36 <Button fullWidth>Przycisk na pełną szerokość</Button>
37
38 <h2>Wyłączony</h2>
39 <div className="button-group">
40 <Button disabled>Wyłączony przycisk</Button>
41 <Button variant="outline" disabled>Wyłączony outline</Button>
42 </div>
43 </div>
44 );
45}Stylowanie w Next.js 15 jest elastyczne i umożliwia stosowanie różnych podejść, od globalnych stylów po hermetyczne CSS Modules. Podobnie jak w Metropolis Quantum, gdzie estetyka całego miasta jest spójna, ale każda dzielnica zachowuje swoją unikalność, w aplikacjach Next.js możemy łączyć globalne reguły z lokalnymi stylami komponentów.
Najważniejsze punkty:
W następnym rozdziale przyjrzymy się integracji z Tailwind CSS, popularnym frameworkiem CSS opartym na klasach użytkowych, który może znacząco przyspieszyć proces stylowania w Next.js 15.