W Metropolis Quantum, najnowocześniejszym mieście przyszłości, inżynierowie opracowali modułowy system budowy, który pozwala tworzyć zaawansowane struktury z gotowych, prefabrykowanych komponentów. Ten system umożliwia szybkie wznoszenie budynków, przy jednoczesnym zachowaniu spójnej estetyki całego miasta. W świecie programowania frontendowego, Tailwind CSS pełni podobną rolę - jest to narzędzie, które pozwala na szybkie tworzenie interfejsów użytkownika za pomocą klas użytkowych.
Tailwind CSS to framework CSS typu "utility-first", który dostarcza zestaw klas użytkowych pozwalających na tworzenie projektów bezpośrednio w HTML, bez pisania CSS. Zamiast definiować własne klasy, używasz gotowych, niskopoziomowych klas, które odpowiadają konkretnym stylom CSS.
Na przykład, zamiast pisać:
1.button {
2 display: flex;
3 align-items: center;
4 padding: 0.5rem 1rem;
5 background-color: #3b82f6;
6 color: white;
7 border-radius: 0.25rem;
8}W Tailwind używasz kombinacji klas:
1<button class="flex items-center px-4 py-2 bg-blue-500 text-white rounded">
2 Kliknij mnie
3</button>md:w-1/2)Next.js 15 ma doskonałą integrację z Tailwind CSS. Oto jak skonfigurować Tailwind w projekcie Next.js 15:
1npm install -D tailwindcss postcss autoprefixer1npx tailwindcss init -pTo polecenie utworzy dwa pliki:
tailwind.config.js - konfiguracja Tailwind CSSpostcss.config.js - konfiguracja PostCSSNastępnie należy zaktualizować plik
tailwind.config.js, aby wskazać, gdzie Tailwind powinien szukać klas do uwzględnienia w ostatecznym CSS:1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 content: [
5 "./app/**/*.{js,ts,jsx,tsx,mdx}",
6 "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 ],
9 theme: {
10 extend: {},
11 },
12 plugins: [],
13}Teraz należy dodać dyrektywy Tailwind do głównego pliku CSS (zwykle
app/globals.css):1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;Te dyrektywy importują:
@tailwind base - style resetujące i podstawowe@tailwind components - style komponentów@tailwind utilities - klasy użytkoweUpewnij się, że
globals.css jest importowany w głównym layoucie aplikacji: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}Teraz możesz zacząć używać klas Tailwind w swoich komponentach:
1// app/page.tsx
2export default function Home() {
3 return (
4 <main className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
5 <div className="max-w-7xl mx-auto">
6 <h1 className="text-4xl font-bold text-gray-900 mb-6">
7 Witaj w Metropolis Quantum
8 </h1>
9 <p className="text-xl text-gray-700 mb-8">
10 Miasto przyszłości, gdzie innowacja spotyka się z tradycją.
11 </p>
12 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
13 <div className="bg-white rounded-lg shadow-md p-6">
14 <h2 className="text-2xl font-semibold text-gray-900 mb-4">
15 Kwantowa Mobilność
16 </h2>
17 <p className="text-gray-600">
18 Poznaj nasz zaawansowany system transportu, który łączy różne
19 wymiary przestrzeni.
20 </p>
21 <button className="mt-4 bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded transition duration-200">
22 Dowiedz się więcej
23 </button>
24 </div>
25 {/* Więcej kart */}
26 </div>
27 </div>
28 </main>
29 );
30}Tailwind oferuje rozbudowane możliwości konfiguracji, które pozwalają dostosować framework do specyficznych potrzeb projektu.
Możesz dostosować kolory, odstępy, breakpointy i inne wartości w
tailwind.config.js:1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 content: [
5 "./app/**/*.{js,ts,jsx,tsx,mdx}",
6 "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 ],
9 theme: {
10 extend: {
11 colors: {
12 quantum: {
13 50: '#f0f9ff',
14 100: '#e0f2fe',
15 200: '#bae6fd',
16 300: '#7dd3fc',
17 400: '#38bdf8',
18 500: '#0ea5e9',
19 600: '#0284c7',
20 700: '#0369a1',
21 800: '#075985',
22 900: '#0c4a6e',
23 950: '#082f49',
24 },
25 neon: {
26 500: '#39ff14',
27 600: '#32e613',
28 },
29 },
30 spacing: {
31 '72': '18rem',
32 '84': '21rem',
33 '96': '24rem',
34 },
35 borderRadius: {
36 'quantum': '0.75rem',
37 },
38 fontFamily: {
39 'quantum': ['Quantum Sans', 'sans-serif'],
40 'display': ['Quantum Display', 'sans-serif'],
41 },
42 boxShadow: {
43 'quantum': '0 4px 14px 0 rgba(0, 118, 255, 0.39)',
44 },
45 },
46 // Nadpisanie domyślnych wartości
47 screens: {
48 'sm': '640px',
49 'md': '768px',
50 'lg': '1024px',
51 'xl': '1280px',
52 '2xl': '1536px',
53 },
54 },
55 plugins: [],
56}Po zdefiniowaniu niestandardowych wartości w konfiguracji, możesz ich używać w klasach:
1<button className="bg-quantum-600 hover:bg-quantum-700 text-white font-medium py-2 px-4 rounded-quantum shadow-quantum transition duration-200">
2 Kwantowa Akcja
3</button>Tailwind ma wbudowany system responsywności. Możesz używać prefiksów dla różnych breakpointów:
1<div className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4 xl:w-1/5">
2 Responsywny element
3</div>Ten div będzie zajmował:
Możesz definiować własne klasy użytkowe za pomocą dyrektywy
@layer utilities:1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5
6@layer utilities {
7 .text-shadow-quantum {
8 text-shadow: 0 0 8px rgba(0, 118, 255, 0.5);
9 }
10 .scrollbar-hide {
11 -ms-overflow-style: none;
12 scrollbar-width: none;
13 }
14 .scrollbar-hide::-webkit-scrollbar {
15 display: none;
16 }
17}Mimo że Tailwind jest frameworkiem typu "utility-first", czasami warto stworzyć powtarzalne komponenty. Możesz to zrobić za pomocą dyrektywy
@layer components:1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5
6@layer components {
7 .quantum-card {
8 @apply bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow duration-300;
9 }
10 .quantum-button {
11 @apply bg-quantum-600 hover:bg-quantum-700 text-white font-medium py-2 px-4 rounded-quantum shadow-quantum transition duration-200;
12 }
13 .quantum-input {
14 @apply w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-quantum-500 focus:border-transparent;
15 }
16}Teraz możesz używać tych klas bezpośrednio:
1<div className="quantum-card">
2 <h2 className="text-2xl font-semibold mb-4">Mój komponent</h2>
3 <input type="text" className="quantum-input mb-4" />
4 <button className="quantum-button">Akcja</button>
5</div>Możesz również dostosować style bazowe, używając dyrektywy
@layer base:1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5
6@layer base {
7 h1 {
8 @apply text-4xl font-bold mb-6;
9 }
10 h2 {
11 @apply text-3xl font-semibold mb-4;
12 }
13 h3 {
14 @apply text-2xl font-medium mb-3;
15 }
16 a {
17 @apply text-quantum-600 hover:text-quantum-800 hover:underline;
18 }
19}Tailwind ma bogaty ekosystem wtyczek, które dodają nowe funkcjonalności. Oto kilka popularnych:
Dodaje lepsze style domyślne dla elementów formularzy.
1npm install -D @tailwindcss/forms1// tailwind.config.js
2module.exports = {
3 //
4 plugins: [
5 require('@tailwindcss/forms'),
6 //
7 ],
8}Dodaje klasę
prose, która zapewnia dobre style typograficzne dla zawartości generowanej z CMS, Markdown, itp.1npm install -D @tailwindcss/typography1// tailwind.config.js
2module.exports = {
3 //
4 plugins: [
5 require('@tailwindcss/typography'),
6 //
7 ],
8}Użycie:
1<article className="prose lg:prose-xl">
2 <h1>Kwantowa teoria wszystkiego</h1>
3 <p>Długi artykuł z wieloma paragrafami, listami, itp.</p>
4 {/* ... */}
5</article>Zapewnia utrzymanie proporcji elementów (np. dla obrazów, wideo).
1npm install -D @tailwindcss/aspect-ratio1// tailwind.config.js
2module.exports = {
3 //
4 plugins: [
5 require('@tailwindcss/aspect-ratio'),
6 //
7 ],
8}Użycie:
1<div className="aspect-w-16 aspect-h-9">
2 <iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
3</div>Pozwala na ograniczenie liczby wierszy tekstu (np. do tytułów w kartach).
1npm install -D @tailwindcss/line-clamp1// tailwind.config.js
2module.exports = {
3 //
4 plugins: [
5 require('@tailwindcss/line-clamp'),
6 //
7 ],
8}Użycie:
1<p className="line-clamp-2">
2 Bardzo długi tekst, który zostanie przycięty po dwóch liniach i zakończony wielokropkiem...
3</p>Tailwind generuje tylko style dla klas, które faktycznie używasz w swoim projekcie. To znaczy, że plik CSS używany w produkcji jest zazwyczaj bardzo mały. Jednak w trybie developerskim, Tailwind może generować duży plik CSS, co może spowalniać kompilację.
W Tailwind CSS v2.0 i nowszych, PurgeCSS jest wbudowany. Konfiguracja
content w tailwind.config.js informuje Tailwind, gdzie szukać klas do uwzględnienia w ostatecznym CSS:1// tailwind.config.js
2module.exports = {
3 content: [
4 "./app/**/*.{js,ts,jsx,tsx,mdx}",
5 "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 ],
8 //
9}Next.js automatycznie minifikuje CSS w trybie produkcyjnym.
Next.js automatycznie dzieli CSS na mniejsze pliki i ładuje je tylko wtedy, gdy są potrzebne.
1// components/QuantumCard.tsx
2import { ReactNode } from 'react';
3
4interface QuantumCardProps {
5 title: string;
6 description: string;
7 icon?: ReactNode;
8 actionText?: string;
9 onAction?: () => void;
10}
11
12export function QuantumCard({
13 title,
14 description,
15 icon,
16 actionText,
17 onAction,
18}: QuantumCardProps) {
19 return (
20 <div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow duration-300">
21 {icon && (
22 <div className="flex justify-center items-center w-12 h-12 rounded-full bg-quantum-100 text-quantum-600 mb-4">
23 {icon}
24 </div>
25 )}
26 <h3 className="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
27 <p className="text-gray-600 mb-4">{description}</p>
28 {actionText && onAction && (
29 <button
30 onClick={onAction}
31 className="inline-flex items-center text-quantum-600 hover:text-quantum-800 font-medium"
32 >
33 {actionText}
34 <svg
35 className="ml-1 w-4 h-4"
36 fill="none"
37 stroke="currentColor"
38 viewBox="0 0 24 24"
39 xmlns="http://www.w3.org/2000/svg"
40 >
41 <path
42 strokeLinecap="round"
43 strokeLinejoin="round"
44 strokeWidth={2}
45 d="M9 5l7 7-7 7"
46 />
47 </svg>
48 </button>
49 )}
50 </div>
51 );
52}1// components/QuantumNav.tsx
2'use client';
3
4import { useState } from 'react';
5import Link from 'next/link';
6import { usePathname } from 'next/navigation';
7
8const navItems = [
9 { name: 'Strona główna', href: '/' },
10 { name: 'Funkcje', href: '/funkcje' },
11 { name: 'Cennik', href: '/cennik' },
12 { name: 'O nas', href: '/o-nas' },
13 { name: 'Kontakt', href: '/kontakt' },
14];
15
16export function QuantumNav() {
17 const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
18 const pathname = usePathname();
19
20 return (
21 <nav className="bg-white shadow-sm">
22 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
23 <div className="flex justify-between h-16">
24 <div className="flex items-center">
25 <div className="flex-shrink-0 flex items-center">
26 <span className="text-quantum-600 font-display text-2xl font-bold">Quantum</span>
27 </div>
28 <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
29 {navItems.map((item) => {
30 const isActive = pathname === item.href;
31 return (
32 <Link
33 key={item.name}
34 href={item.href}
35 className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
36 isActive
37 ? 'border-quantum-500 text-gray-900'
38 : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
39 }`}
40 >
41 {item.name}
42 </Link>
43 );
44 })}
45 </div>
46 </div>
47 <div className="hidden sm:ml-6 sm:flex sm:items-center">
48 <button className="bg-quantum-500 hover:bg-quantum-600 text-white py-2 px-4 rounded-md text-sm font-medium">
49 Rozpocznij
50 </button>
51 </div>
52 <div className="-mr-2 flex items-center sm:hidden">
53 <button
54 type="button"
55 className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-quantum-500"
56 aria-controls="mobile-menu"
57 aria-expanded="false"
58 onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
59 >
60 <span className="sr-only">Otwórz menu główne</span>
61 {/* Ikona hamburgera */}
62 <svg
63 className={`${mobileMenuOpen ? 'hidden' : 'block'} h-6 w-6`}
64 xmlns="http://www.w3.org/2000/svg"
65 fill="none"
66 viewBox="0 0 24 24"
67 stroke="currentColor"
68 aria-hidden="true"
69 >
70 <path
71 strokeLinecap="round"
72 strokeLinejoin="round"
73 strokeWidth={2}
74 d="M4 6h16M4 12h16M4 18h16"
75 />
76 </svg>
77 {/* Ikona X */}
78 <svg
79 className={`${mobileMenuOpen ? 'block' : 'hidden'} h-6 w-6`}
80 xmlns="http://www.w3.org/2000/svg"
81 fill="none"
82 viewBox="0 0 24 24"
83 stroke="currentColor"
84 aria-hidden="true"
85 >
86 <path
87 strokeLinecap="round"
88 strokeLinejoin="round"
89 strokeWidth={2}
90 d="M6 18L18 6M6 6l12 12"
91 />
92 </svg>
93 </button>
94 </div>
95 </div>
96 </div>
97
98 {/* Menu mobilne */}
99 <div
100 className={`${mobileMenuOpen ? 'block' : 'hidden'} sm:hidden`}
101 id="mobile-menu"
102 >
103 <div className="pt-2 pb-3 space-y-1">
104 {navItems.map((item) => {
105 const isActive = pathname === item.href;
106 return (
107 <Link
108 key={item.name}
109 href={item.href}
110 className={`block pl-3 pr-4 py-2 border-l-4 text-base font-medium ${
111 isActive
112 ? 'bg-quantum-50 border-quantum-500 text-quantum-700'
113 : 'border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800'
114 }`}
115 >
116 {item.name}
117 </Link>
118 );
119 })}
120 </div>
121 <div className="pt-4 pb-3 border-t border-gray-200">
122 <div className="mt-3 px-2 space-y-1">
123 <button className="w-full block bg-quantum-500 hover:bg-quantum-600 text-white py-2 px-4 rounded-md text-base font-medium">
124 Rozpocznij
125 </button>
126 </div>
127 </div>
128 </div>
129 </nav>
130 );
131}1// app/dashboard/profile/page.tsx
2'use client';
3
4import { useState } from 'react';
5
6type Tab = 'general' | 'security' | 'notifications' | 'billing';
7
8export default function ProfilePage() {
9 const [activeTab, setActiveTab] = useState<Tab>('general');
10
11 const tabs = [
12 { id: 'general', name: 'Informacje ogólne' },
13 { id: 'security', name: 'Bezpieczeństwo' },
14 { id: 'notifications', name: 'Powiadomienia' },
15 { id: 'billing', name: 'Płatności' },
16 ];
17
18 return (
19 <div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
20 <div className="md:grid md:grid-cols-3 md:gap-6">
21 <div className="md:col-span-1">
22 <div className="px-4 sm:px-0">
23 <h2 className="text-lg font-medium leading-6 text-gray-900">Profil</h2>
24 <p className="mt-1 text-sm text-gray-600">
25 Zarządzaj swoim profilem i ustawieniami w systemie Metropolis Quantum.
26 </p>
27 </div>
28
29 <div className="mt-6">
30 <nav className="space-y-1" aria-label="Tabs">
31 {tabs.map((tab) => (
32 <button
33 key={tab.id}
34 onClick={() => setActiveTab(tab.id as Tab)}
35 className={`${
36 activeTab === tab.id
37 ? 'bg-quantum-50 border-quantum-500 text-quantum-700'
38 : 'border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800'
39 } w-full flex items-center px-3 py-2 text-sm font-medium border-l-4`}
40 aria-current={activeTab === tab.id ? 'page' : undefined}
41 >
42 {tab.name}
43 </button>
44 ))}
45 </nav>
46 </div>
47 </div>
48
49 <div className="mt-5 md:mt-0 md:col-span-2">
50 <div className="bg-white shadow sm:rounded-lg">
51 {activeTab === 'general' && (
52 <div className="p-6">
53 <h3 className="text-lg font-medium leading-6 text-gray-900 mb-4">
54 Informacje o profilu
55 </h3>
56
57 <div className="space-y-6">
58 <div>
59 <label htmlFor="name" className="block text-sm font-medium text-gray-700">
60 Imię i nazwisko
61 </label>
62 <input
63 type="text"
64 name="name"
65 id="name"
66 className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-quantum-500 focus:ring-quantum-500 sm:text-sm"
67 defaultValue="Maria Nowak"
68 />
69 </div>
70
71 <div>
72 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
73 Email
74 </label>
75 <input
76 type="email"
77 name="email"
78 id="email"
79 className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-quantum-500 focus:ring-quantum-500 sm:text-sm"
80 defaultValue="maria.nowak@quantum.com"
81 />
82 </div>
83
84 <div>
85 <label htmlFor="about" className="block text-sm font-medium text-gray-700">
86 O mnie
87 </label>
88 <textarea
89 id="about"
90 name="about"
91 rows={3}
92 className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-quantum-500 focus:ring-quantum-500 sm:text-sm"
93 defaultValue="Jestem inżynierem kwantowym mieszkającym w Nowym Dystrykie. Pracuję nad rozwojem nowych technologii transportu w Metropolis Quantum."
94 />
95 </div>
96
97 <div className="flex justify-end">
98 <button
99 type="button"
100 className="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-quantum-500"
101 >
102 Anuluj
103 </button>
104 <button
105 type="submit"
106 className="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-quantum-600 hover:bg-quantum-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-quantum-500"
107 >
108 Zapisz
109 </button>
110 </div>
111 </div>
112 </div>
113 )}
114
115 {activeTab === 'security' && (
116 <div className="p-6">
117 <h3 className="text-lg font-medium leading-6 text-gray-900 mb-4">
118 Bezpieczeństwo
119 </h3>
120 {/* Zawartość zakładki bezpieczeństwa */}
121 </div>
122 )}
123
124 {activeTab === 'notifications' && (
125 <div className="p-6">
126 <h3 className="text-lg font-medium leading-6 text-gray-900 mb-4">
127 Powiadomienia
128 </h3>
129 {/* Zawartość zakładki powiadomień */}
130 </div>
131 )}
132
133 {activeTab === 'billing' && (
134 <div className="p-6">
135 <h3 className="text-lg font-medium leading-6 text-gray-900 mb-4">
136 Płatności
137 </h3>
138 {/* Zawartość zakładki płatności */}
139 </div>
140 )}
141 </div>
142 </div>
143 </div>
144 </div>
145 );
146}Jeśli używasz dynamicznych klas, PurgeCSS może je pominąć:
1// To może nie działać w produkcji
2<div className={`text-${color}-500`}>Tekst</div>Rozwiązanie:
1// Definiuj wszystkie możliwe klasy
2<div className={color === 'red' ? 'text-red-500' : color === 'blue' ? 'text-blue-500' : 'text-black'}>Tekst</div>Tailwind v3.0 i nowsze domyślnie używają silnika JIT, co przyspiesza kompilację. Jeśli korzystasz ze starszej wersji, możesz włączyć JIT w konfiguracji:
1// tailwind.config.js (dla starszych wersji)
2module.exports = {
3 mode: 'jit',
4 // inne opcje
5}Jeśli masz konflikty stylów z innymi bibliotekami, możesz użyć prefiksu dla wszystkich klas Tailwind:
1// tailwind.config.js
2module.exports = {
3 prefix: 'tw-',
4 // inne opcje
5}Teraz wszystkie klasy będą miały prefiks
tw-:1<div className="tw-flex tw-items-center tw-p-4">
2 Tekst z prefiksem
3</div>Tailwind CSS to potężne narzędzie, które może znacząco przyspieszyć proces tworzenia interfejsów użytkownika w Next.js 15. Dzięki podejściu "utility-first", możesz tworzyć niestandardowe projekty bez opuszczania HTML i bez pisania własnego CSS.
Najważniejsze punkty:
W następnym rozdziale przyjrzymy się komponentom Shadcn/UI, które są zbudowane na bazie Tailwind CSS i dostarczają gotowe komponenty interfejsu użytkownika, które można łatwo dostosować do specyficznych potrzeb projektu.