Cours HTML et CSS · Module 9: CSS moderne
Projet - Page portfolio moderne avec des techniques CSS avancées
Dans cette leçon6
Il est temps de créer un projet complet qui mettra en valeur toutes les techniques CSS avancées que tu as apprises dans ce module. Nous allons créer une page portfolio moderne utilisant les dernières capacités du CSS.
Description du projet
Tu vas créer une page portfolio réactive pour un développeur web qui comprendra :
- Une section hero avec des animations pilotées par le défilement
- Un portfolio de projets avec des container queries
- Une section de compétences avec CSS Grid et Subgrid
- Un formulaire de contact avec validation avancée
- Un bouton de bascule mode sombre/clair
- Des animations avancées et des micro-interactions
Exigences fonctionnelles
1. Structure de la page
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Jan Kowalski - Web Developer Portfolio</title>
7 <link rel="stylesheet" href="styles.css">
8</head>
9<body>
10 <header class="header">
11 <nav class="nav">
12 <div class="nav__brand">
13 <a href="#" class="nav__logo">JK</a>
14 </div>
15 <ul class="nav__menu">
16 <li class="nav__item">
17 <a href="#home" class="nav__link">Accueil</a>
18 </li>
19 <li class="nav__item">
20 <a href="#about" class="nav__link">À propos</a>
21 </li>
22 <li class="nav__item">
23 <a href="#projects" class="nav__link">Projets</a>
24 </li>
25 <li class="nav__item">
26 <a href="#skills" class="nav__link">Compétences</a>
27 </li>
28 <li class="nav__item">
29 <a href="#contact" class="nav__link">Contact</a>
30 </li>
31 </ul>
32 <button class="theme-toggle" aria-label="Basculer le thème">
33 <span class="theme-toggle__icon">Sombre</span>
34 </button>
35 </nav>
36 </header>
37
38 <main>
39 <section id="home" class="hero">
40 <div class="hero__content">
41 <h1 class="hero__title">Jan Kowalski</h1>
42 <p class="hero__subtitle">Développeur Frontend & Designer UI/UX</p>
43 <a href="#projects" class="hero__cta">Voir mes projets</a>
44 </div>
45 <div class="hero__animation">
46 <div class="floating-shapes">
47 <div class="shape shape--1"></div>
48 <div class="shape shape--2"></div>
49 <div class="shape shape--3"></div>
50 </div>
51 </div>
52 </section>
53
54 <section id="about" class="about">
55 <div class="container">
56 <h2 class="section__title">À propos de moi</h2>
57 <div class="about__content">
58 <div class="about__text">
59 <p>Je suis passionné par la création d'applications web modernes...</p>
60 </div>
61 <div class="about__image">
62 <img src="profile.jpg" alt="Photo de profil" loading="lazy">
63 </div>
64 </div>
65 </div>
66 </section>
67
68 <section id="projects" class="projects">
69 <div class="container">
70 <h2 class="section__title">Mes projets</h2>
71 <div class="projects__grid">
72 <article class="project-card" data-category="react">
73 <div class="project-card__image">
74 <img src="project1.jpg" alt="Projet 1" loading="lazy">
75 </div>
76 <div class="project-card__content">
77 <h3 class="project-card__title">Tableau de bord e-commerce</h3>
78 <p class="project-card__description">Un tableau de bord moderne pour gérer une boutique en ligne.</p>
79 <div class="project-card__tech">
80 <span class="tech-tag">React</span>
81 <span class="tech-tag">TypeScript</span>
82 <span class="tech-tag">Tailwind</span>
83 </div>
84 <div class="project-card__actions">
85 <a href="#" class="btn btn--primary">Démo live</a>
86 <a href="#" class="btn btn--secondary">GitHub</a>
87 </div>
88 </div>
89 </article>
90
91 <!-- Plus de cartes de projet... -->
92 </div>
93 </div>
94 </section>
95
96 <section id="skills" class="skills">
97 <div class="container">
98 <h2 class="section__title">Compétences</h2>
99 <div class="skills__grid">
100 <div class="skill-category">
101 <h3 class="skill-category__title">Frontend</h3>
102 <div class="skill-category__items">
103 <div class="skill-item">
104 <span class="skill-item__name">HTML/CSS</span>
105 <div class="skill-item__bar">
106 <div class="skill-item__progress" data-level="95"></div>
107 </div>
108 </div>
109 <!-- Plus de compétences... -->
110 </div>
111 </div>
112 </div>
113 </div>
114 </section>
115
116 <section id="contact" class="contact">
117 <div class="container">
118 <h2 class="section__title">Contact</h2>
119 <form class="contact-form" novalidate>
120 <div class="form-group">
121 <label for="name" class="form-label">Nom</label>
122 <input type="text" id="name" name="name" class="form-input" required>
123 <span class="form-error" id="name-error"></span>
124 </div>
125 <div class="form-group">
126 <label for="email" class="form-label">Email</label>
127 <input type="email" id="email" name="email" class="form-input" required>
128 <span class="form-error" id="email-error"></span>
129 </div>
130 <div class="form-group">
131 <label for="message" class="form-label">Message</label>
132 <textarea id="message" name="message" class="form-textarea" required></textarea>
133 <span class="form-error" id="message-error"></span>
134 </div>
135 <button type="submit" class="btn btn--primary btn--full">Envoyer le message</button>
136 </form>
137 </div>
138 </section>
139 </main>
140
141 <footer class="footer">
142 <div class="container">
143 <p>© 2024 Jan Kowalski. Tous droits réservés.</p>
144 </div>
145 </footer>
146
147 <script src="script.js"></script>
148</body>
149</html>2. CSS utilisant des techniques avancées
1/* ===== VARIABLES CSS & DESIGN TOKENS ===== */
2:root {
3 /* Couleurs - Thème clair */
4 --color-primary: #667eea;
5 --color-secondary: #764ba2;
6 --color-accent: #f093fb;
7 --color-text: #2d3748;
8 --color-text-muted: #718096;
9 --color-bg: #ffffff;
10 --color-surface: #f7fafc;
11 --color-border: #e2e8f0;
12
13 /* Typographie */
14 --font-primary: 'Inter', system-ui, sans-serif;
15 --font-heading: 'Poppins', sans-serif;
16
17 /* Espacement */
18 --space-xs: 0.25rem;
19 --space-sm: 0.5rem;
20 --space-md: 1rem;
21 --space-lg: 1.5rem;
22 --space-xl: 2rem;
23 --space-2xl: 3rem;
24 --space-3xl: 4rem;
25
26 /* Mise en page */
27 --container-max-width: 1200px;
28 --border-radius: 0.5rem;
29 --border-radius-lg: 1rem;
30
31 /* Ombres */
32 --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
33 --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
34 --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
35
36 /* Transitions */
37 --transition-fast: 150ms ease;
38 --transition-normal: 250ms ease;
39 --transition-slow: 350ms ease;
40}
41
42/* Variables du thème sombre */
43[data-theme="dark"] {
44 --color-text: #f7fafc;
45 --color-text-muted: #a0aec0;
46 --color-bg: #1a202c;
47 --color-surface: #2d3748;
48 --color-border: #4a5568;
49}
50
51/* ===== CASCADE LAYERS ===== */
52@layer reset, base, components, utilities;
53
54@layer reset {
55 *, *::before, *::after {
56 box-sizing: border-box;
57 margin: 0;
58 padding: 0;
59 }
60
61 html {
62 scroll-behavior: smooth;
63 }
64
65 body {
66 line-height: 1.6;
67 -webkit-font-smoothing: antialiased;
68 }
69
70 img, picture, video, canvas, svg {
71 display: block;
72 max-width: 100%;
73 }
74
75 input, button, textarea, select {
76 font: inherit;
77 }
78}
79
80@layer base {
81 body {
82 font-family: var(--font-primary);
83 color: var(--color-text);
84 background: var(--color-bg);
85 transition: background-color var(--transition-normal),
86 color var(--transition-normal);
87 }
88
89 h1, h2, h3, h4, h5, h6 {
90 font-family: var(--font-heading);
91 font-weight: 600;
92 line-height: 1.2;
93 }
94
95 a {
96 color: var(--color-primary);
97 text-decoration: none;
98 transition: color var(--transition-fast);
99
100 &:hover {
101 color: var(--color-secondary);
102 }
103 }
104}
105
106/* ===== CONTAINER QUERIES ===== */
107.projects {
108 container-type: inline-size;
109 container-name: projects;
110}
111
112.project-card {
113 background: var(--color-surface);
114 border-radius: var(--border-radius-lg);
115 overflow: hidden;
116 box-shadow: var(--shadow-md);
117 transition: transform var(--transition-normal),
118 box-shadow var(--transition-normal);
119
120 &:hover {
121 transform: translateY(-4px);
122 box-shadow: var(--shadow-lg);
123 }
124}
125
126@container projects (min-width: 768px) {
127 .projects__grid {
128 display: grid;
129 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
130 gap: var(--space-xl);
131 }
132
133 .project-card {
134 display: grid;
135 grid-template-rows: 200px 1fr;
136 }
137}
138
139@container projects (min-width: 1024px) {
140 .projects__grid {
141 grid-template-columns: repeat(3, 1fr);
142 }
143}
144
145/* ===== CSS GRID AVEC SUBGRID ===== */
146.skills__grid {
147 display: grid;
148 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
149 gap: var(--space-xl);
150}
151
152.skill-category {
153 display: grid;
154 grid-template-rows: auto 1fr;
155 background: var(--color-surface);
156 border-radius: var(--border-radius-lg);
157 padding: var(--space-lg);
158
159 /* Utiliser subgrid quand c'est pris en charge */
160 @supports (grid-template-rows: subgrid) {
161 grid-template-rows: subgrid;
162 }
163}
164
165/* ===== CSS NESTING ===== */
166.nav {
167 display: flex;
168 justify-content: space-between;
169 align-items: center;
170 padding: var(--space-md) var(--space-lg);
171 background: var(--color-surface);
172 backdrop-filter: blur(10px);
173 border-bottom: 1px solid var(--color-border);
174
175 &__brand {
176 font-weight: bold;
177 font-size: 1.5rem;
178 }
179
180 &__logo {
181 display: inline-flex;
182 align-items: center;
183 justify-content: center;
184 width: 40px;
185 height: 40px;
186 background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
187 color: white;
188 border-radius: 50%;
189 font-weight: bold;
190 }
191
192 &__menu {
193 display: flex;
194 list-style: none;
195 gap: var(--space-lg);
196
197 @media (max-width: 768px) {
198 display: none; /* Menu mobile à implémenter */
199 }
200 }
201
202 &__link {
203 position: relative;
204 font-weight: 500;
205
206 &::after {
207 content: '';
208 position: absolute;
209 bottom: -4px;
210 left: 0;
211 width: 0;
212 height: 2px;
213 background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
214 transition: width var(--transition-normal);
215 }
216
217 &:hover::after,
218 &.active::after {
219 width: 100%;
220 }
221 }
222}
223
224/* ===== ANIMATIONS PILOTÉES PAR LE DÉFILEMENT ===== */
225@supports (animation-timeline: scroll()) {
226 .hero__title {
227 animation: hero-fade-in linear;
228 animation-timeline: view();
229 animation-range: entry 0% cover 40%;
230 }
231
232 .section__title {
233 animation: slide-up linear;
234 animation-timeline: view();
235 animation-range: entry 20% cover 50%;
236 }
237
238 @keyframes hero-fade-in {
239 from {
240 opacity: 0;
241 transform: translateY(50px) scale(0.9);
242 }
243 to {
244 opacity: 1;
245 transform: translateY(0) scale(1);
246 }
247 }
248
249 @keyframes slide-up {
250 from {
251 opacity: 0;
252 transform: translateY(30px);
253 }
254 to {
255 opacity: 1;
256 transform: translateY(0);
257 }
258 }
259}
260
261/* ===== FONCTIONNALITÉS CSS MODERNES ===== */
262.hero {
263 min-height: 100vh;
264 display: grid;
265 grid-template-columns: 1fr 1fr;
266 align-items: center;
267 padding: var(--space-2xl);
268 background: linear-gradient(135deg,
269 color-mix(in srgb, var(--color-primary) 10%, transparent) 0%,
270 color-mix(in srgb, var(--color-secondary) 10%, transparent) 100%
271 );
272 position: relative;
273 overflow: hidden;
274
275 @media (max-width: 768px) {
276 grid-template-columns: 1fr;
277 text-align: center;
278 padding: var(--space-xl) var(--space-lg);
279 }
280}
281
282.floating-shapes {
283 position: absolute;
284 inset: 0;
285 pointer-events: none;
286}
287
288.shape {
289 position: absolute;
290 border-radius: 50%;
291 background: linear-gradient(45deg, var(--color-primary), var(--color-accent));
292 opacity: 0.1;
293 animation: float 6s ease-in-out infinite;
294
295 &--1 {
296 width: 100px;
297 height: 100px;
298 top: 20%;
299 left: 10%;
300 animation-delay: 0s;
301 }
302
303 &--2 {
304 width: 150px;
305 height: 150px;
306 top: 60%;
307 right: 20%;
308 animation-delay: 2s;
309 }
310
311 &--3 {
312 width: 80px;
313 height: 80px;
314 bottom: 20%;
315 left: 60%;
316 animation-delay: 4s;
317 }
318}
319
320@keyframes float {
321 0%, 100% {
322 transform: translateY(0) rotate(0deg);
323 }
324 50% {
325 transform: translateY(-20px) rotate(180deg);
326 }
327}
328
329/* ===== ÉLÉMENTS DE COMPÉTENCE ===== */
330.skill-item {
331 margin-bottom: var(--space-md);
332
333 &__name {
334 display: block;
335 margin-bottom: var(--space-xs);
336 font-weight: 500;
337 }
338
339 &__bar {
340 width: 100%;
341 height: 8px;
342 background: var(--color-border);
343 border-radius: 4px;
344 overflow: hidden;
345 }
346
347 &__progress {
348 height: 100%;
349 background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
350 border-radius: 4px;
351 transform-origin: left;
352 animation: progress-fill 2s ease-out;
353
354 @supports (animation-timeline: view()) {
355 animation: progress-fill linear;
356 animation-timeline: view();
357 animation-range: entry 50% cover 80%;
358 }
359 }
360}
361
362@keyframes progress-fill {
363 from {
364 transform: scaleX(0);
365 }
366 to {
367 transform: scaleX(1);
368 }
369}
370
371/* ===== BASCULE DE THÈME ===== */
372.theme-toggle {
373 background: var(--color-surface);
374 border: 2px solid var(--color-border);
375 border-radius: 50%;
376 width: 44px;
377 height: 44px;
378 display: flex;
379 align-items: center;
380 justify-content: center;
381 cursor: pointer;
382 transition: all var(--transition-normal);
383
384 &:hover {
385 transform: scale(1.1);
386 background: var(--color-primary);
387 border-color: var(--color-primary);
388 color: white;
389 }
390
391 &__icon {
392 font-size: 1.2rem;
393 transition: transform var(--transition-normal);
394 }
395
396 &[data-theme="dark"] &__icon {
397 transform: rotate(180deg);
398 }
399}
400
401/* ===== STYLISATION DU FORMULAIRE ===== */
402.contact-form {
403 max-width: 600px;
404 margin: 0 auto;
405 padding: var(--space-2xl);
406 background: var(--color-surface);
407 border-radius: var(--border-radius-lg);
408 box-shadow: var(--shadow-lg);
409}
410
411.form-group {
412 margin-bottom: var(--space-lg);
413 position: relative;
414}
415
416.form-label {
417 display: block;
418 margin-bottom: var(--space-xs);
419 font-weight: 500;
420 color: var(--color-text);
421}
422
423.form-input,
424.form-textarea {
425 width: 100%;
426 padding: var(--space-md);
427 border: 2px solid var(--color-border);
428 border-radius: var(--border-radius);
429 background: var(--color-bg);
430 color: var(--color-text);
431 transition: border-color var(--transition-fast);
432
433 &:focus {
434 outline: none;
435 border-color: var(--color-primary);
436 box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
437 }
438
439 &:invalid:not(:placeholder-shown) {
440 border-color: #ef4444;
441 }
442
443 &:valid:not(:placeholder-shown) {
444 border-color: #10b981;
445 }
446}
447
448.form-textarea {
449 min-height: 120px;
450 resize: vertical;
451}
452
453.form-error {
454 position: absolute;
455 bottom: -20px;
456 left: 0;
457 font-size: 0.875rem;
458 color: #ef4444;
459 opacity: 0;
460 transition: opacity var(--transition-fast);
461
462 &.visible {
463 opacity: 1;
464 }
465}
466
467/* ===== BOUTONS ===== */
468.btn {
469 display: inline-flex;
470 align-items: center;
471 justify-content: center;
472 padding: var(--space-md) var(--space-lg);
473 border: none;
474 border-radius: var(--border-radius);
475 font-weight: 500;
476 text-decoration: none;
477 cursor: pointer;
478 transition: all var(--transition-normal);
479 position: relative;
480 overflow: hidden;
481
482 &::before {
483 content: '';
484 position: absolute;
485 top: 0;
486 left: -100%;
487 width: 100%;
488 height: 100%;
489 background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
490 transition: left var(--transition-slow);
491 }
492
493 &:hover::before {
494 left: 100%;
495 }
496
497 &--primary {
498 background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
499 color: white;
500
501 &:hover {
502 transform: translateY(-2px);
503 box-shadow: var(--shadow-lg);
504 }
505 }
506
507 &--secondary {
508 background: transparent;
509 color: var(--color-primary);
510 border: 2px solid var(--color-primary);
511
512 &:hover {
513 background: var(--color-primary);
514 color: white;
515 }
516 }
517
518 &--full {
519 width: 100%;
520 }
521}
522
523/* ===== ACCESSIBILITÉ ===== */
524@media (prefers-reduced-motion: reduce) {
525 *,
526 *::before,
527 *::after {
528 animation-duration: 0.01ms !important;
529 animation-iteration-count: 1 !important;
530 transition-duration: 0.01ms !important;
531 scroll-behavior: auto !important;
532 }
533}
534
535@media (prefers-color-scheme: dark) {
536 :root:not([data-theme="light"]) {
537 color-scheme: dark;
538 }
539}
540
541/* Styles de focus */
542:focus-visible {
543 outline: 2px solid var(--color-primary);
544 outline-offset: 2px;
545}
546
547/* Lien d'évitement */
548.skip-link {
549 position: absolute;
550 top: -40px;
551 left: 6px;
552 background: var(--color-primary);
553 color: white;
554 padding: 8px;
555 text-decoration: none;
556 border-radius: 4px;
557 z-index: 1000;
558
559 &:focus {
560 top: 6px;
561 }
562}
563
564/* ===== CLASSES UTILITAIRES ===== */
565@layer utilities {
566 .container {
567 max-width: var(--container-max-width);
568 margin: 0 auto;
569 padding: 0 var(--space-lg);
570 }
571
572 .section__title {
573 font-size: 2.5rem;
574 text-align: center;
575 margin-bottom: var(--space-2xl);
576 background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
577 background-clip: text;
578 -webkit-background-clip: text;
579 -webkit-text-fill-color: transparent;
580 color: transparent;
581 }
582
583 .visually-hidden {
584 position: absolute !important;
585 width: 1px !important;
586 height: 1px !important;
587 padding: 0 !important;
588 margin: -1px !important;
589 overflow: hidden !important;
590 clip: rect(0, 0, 0, 0) !important;
591 white-space: nowrap !important;
592 border: 0 !important;
593 }
594
595 .text-gradient {
596 background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
597 background-clip: text;
598 -webkit-background-clip: text;
599 -webkit-text-fill-color: transparent;
600 }
601}3. JavaScript pour les interactions
1// Fonctionnalité de bascule de thème
2class ThemeManager {
3 constructor() {
4 this.theme = localStorage.getItem('theme') || 'light';
5 this.toggle = document.querySelector('.theme-toggle');
6 this.init();
7 }
8
9 init() {
10 this.setTheme(this.theme);
11 this.toggle.addEventListener('click', () => this.toggleTheme());
12 }
13
14 setTheme(theme) {
15 document.documentElement.setAttribute('data-theme', theme);
16 this.toggle.querySelector('.theme-toggle__icon').textContent =
17 theme === 'dark' ? 'Clair' : 'Sombre';
18 localStorage.setItem('theme', theme);
19 this.theme = theme;
20 }
21
22 toggleTheme() {
23 const newTheme = this.theme === 'light' ? 'dark' : 'light';
24 this.setTheme(newTheme);
25 }
26}
27
28// Validation du formulaire
29class FormValidator {
30 constructor(form) {
31 this.form = form;
32 this.fields = {
33 name: {
34 element: form.querySelector('#name'),
35 rules: ['required', 'minLength:2']
36 },
37 email: {
38 element: form.querySelector('#email'),
39 rules: ['required', 'email']
40 },
41 message: {
42 element: form.querySelector('#message'),
43 rules: ['required', 'minLength:10']
44 }
45 };
46 this.init();
47 }
48
49 init() {
50 Object.values(this.fields).forEach(field => {
51 field.element.addEventListener('blur', () => this.validateField(field));
52 field.element.addEventListener('input', () => this.clearError(field));
53 });
54
55 this.form.addEventListener('submit', (e) => this.handleSubmit(e));
56 }
57
58 validateField(field) {
59 const value = field.element.value.trim();
60 const errors = [];
61
62 field.rules.forEach(rule => {
63 if (rule === 'required' && !value) {
64 errors.push('Ce champ est obligatoire');
65 } else if (rule === 'email' && value && !this.isValidEmail(value)) {
66 errors.push('Veuillez saisir une adresse email valide');
67 } else if (rule.startsWith('minLength:')) {
68 const minLength = parseInt(rule.split(':')[1]);
69 if (value && value.length < minLength) {
70 errors.push(`Minimum ${minLength} caractères`);
71 }
72 }
73 });
74
75 this.showError(field, errors[0]);
76 return errors.length === 0;
77 }
78
79 showError(field, message) {
80 const errorElement = document.querySelector(`#${field.element.name}-error`);
81 if (message) {
82 errorElement.textContent = message;
83 errorElement.classList.add('visible');
84 field.element.setAttribute('aria-invalid', 'true');
85 } else {
86 this.clearError(field);
87 }
88 }
89
90 clearError(field) {
91 const errorElement = document.querySelector(`#${field.element.name}-error`);
92 errorElement.classList.remove('visible');
93 field.element.removeAttribute('aria-invalid');
94 }
95
96 isValidEmail(email) {
97 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
98 }
99
100 handleSubmit(e) {
101 e.preventDefault();
102
103 const isValid = Object.values(this.fields)
104 .map(field => this.validateField(field))
105 .every(valid => valid);
106
107 if (isValid) {
108 this.submitForm();
109 }
110 }
111
112 async submitForm() {
113 const submitButton = this.form.querySelector('button[type="submit"]');
114 const originalText = submitButton.textContent;
115
116 submitButton.textContent = 'Envoi en cours...';
117 submitButton.disabled = true;
118
119 try {
120 // Simulation de l'envoi
121 await new Promise(resolve => setTimeout(resolve, 2000));
122
123 // Message de succès
124 this.showSuccessMessage();
125 this.form.reset();
126 } catch (error) {
127 alert("Une erreur est survenue lors de l'envoi du message");
128 } finally {
129 submitButton.textContent = originalText;
130 submitButton.disabled = false;
131 }
132 }
133
134 showSuccessMessage() {
135 const message = document.createElement('div');
136 message.className = 'success-message';
137 message.textContent = 'Message envoyé avec succès !';
138 message.style.cssText = `
139 position: fixed;
140 top: 20px;
141 right: 20px;
142 background: #10b981;
143 color: white;
144 padding: 1rem 1.5rem;
145 border-radius: 0.5rem;
146 box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
147 z-index: 1000;
148 animation: slideIn 0.3s ease;
149 `;
150
151 document.body.appendChild(message);
152
153 setTimeout(() => {
154 message.style.animation = 'slideOut 0.3s ease';
155 setTimeout(() => message.remove(), 300);
156 }, 3000);
157 }
158}
159
160// Défilement fluide pour la navigation
161class SmoothScrolling {
162 constructor() {
163 this.init();
164 }
165
166 init() {
167 document.querySelectorAll('a[href^="#"]').forEach(link => {
168 link.addEventListener('click', (e) => {
169 e.preventDefault();
170 const target = document.querySelector(link.getAttribute('href'));
171 if (target) {
172 target.scrollIntoView({
173 behavior: 'smooth',
174 block: 'start'
175 });
176 }
177 });
178 });
179 }
180}
181
182// Animation des compétences
183class SkillsAnimation {
184 constructor() {
185 this.init();
186 }
187
188 init() {
189 if ('IntersectionObserver' in window) {
190 this.observeSkills();
191 } else {
192 this.animateSkillsDirectly();
193 }
194 }
195
196 observeSkills() {
197 const observer = new IntersectionObserver((entries) => {
198 entries.forEach(entry => {
199 if (entry.isIntersecting) {
200 this.animateSkill(entry.target);
201 observer.unobserve(entry.target);
202 }
203 });
204 }, { threshold: 0.5 });
205
206 document.querySelectorAll('.skill-item__progress').forEach(skill => {
207 observer.observe(skill);
208 });
209 }
210
211 animateSkill(skillElement) {
212 const level = skillElement.dataset.level;
213 skillElement.style.transform = `scaleX(${level / 100})`;
214 }
215
216 animateSkillsDirectly() {
217 document.querySelectorAll('.skill-item__progress').forEach(skill => {
218 setTimeout(() => this.animateSkill(skill), 500);
219 });
220 }
221}
222
223// Initialiser tout au chargement du DOM
224document.addEventListener('DOMContentLoaded', () => {
225 new ThemeManager();
226 new SmoothScrolling();
227 new SkillsAnimation();
228
229 const contactForm = document.querySelector('.contact-form');
230 if (contactForm) {
231 new FormValidator(contactForm);
232 }
233
234 // Ajouter les animations de succès
235 const style = document.createElement('style');
236 style.textContent = `
237 @keyframes slideIn {
238 from { transform: translateX(100%); opacity: 0; }
239 to { transform: translateX(0); opacity: 1; }
240 }
241 @keyframes slideOut {
242 from { transform: translateX(0); opacity: 1; }
243 to { transform: translateX(100%); opacity: 0; }
244 }
245 `;
246 document.head.appendChild(style);
247});Exigences techniques
1. Utiliser toutes les techniques apprises
- Container Queries - grille de projets réactive
- CSS Nesting - organisation du code CSS
- Variables CSS - système de design tokens
- Cascade Layers - gestion de la cascade
- Stylisation des Web Components - composants UI
- Conformité WCAG - accessibilité
- Méthodologie BEM - nommage des classes
- Stylelint - qualité du code CSS
- Support multi-navigateurs - compatibilité
2. Optimisation des performances
1/* Inlining du CSS critique */
2/* Seulement les styles les plus importants dans <head> */
3
4/* Lazy loading des images */
5img[loading="lazy"] {
6 content-visibility: auto;
7 contain-intrinsic-size: 200px 150px;
8}
9
10/* Containment CSS */
11.project-card {
12 contain: layout style paint;
13}
14
15/* Précharger les ressources importantes */
16/* <link rel="preload" href="fonts/inter.woff2" as="font" type="font/woff2" crossorigin> */3. Liste de vérification des tests
- Réactivité sur tous les appareils
- Accessibilité au clavier
- Compatibilité avec les lecteurs d'écran
- Performance (score Lighthouse > 90)
- Tests multi-navigateurs (Chrome, Firefox, Safari, Edge)
- Fonctionnalité du mode sombre/clair
- Validation du formulaire
- Animations fluides
Défis supplémentaires
Niveau avancé
- View Transitions API - ajouter des transitions fluides entre les sections
- CSS Scroll Snap - défilement avec accrochage entre les sections
- CSS @property - propriétés personnalisées animées
- Intersection Observer - lazy loading et animations
- Service Worker - mise en cache pour la fonctionnalité hors ligne
Niveau expert
- CSS Houdini Paint API - dégradés personnalisés
- CSS Parser API - styles dynamiques
- Web Animations API - animations JS avancées
- CSS Typed OM - manipulation programmatique du CSS
Documentation du projet
README.md
1# Portfolio Website - Modern CSS Techniques
2
3Responsive portfolio page utilizing the latest CSS techniques.
4
5## Technologies Used
6
7- HTML5 Semantic markup
8- CSS3 with latest features
9- Vanilla JavaScript ES6+
10- Container Queries
11- CSS Nesting
12- Cascade Layers
13- CSS Grid & Subgrid
14
15## Installation
16
171. Clone the repository
182. Open index.html in a browser
193. Run a local server for full functionality
20
21## Features
22
23- Responsive design with Container Queries
24- Dark/Light mode toggle
25- Smooth scroll animations
26- Form validation
27- WCAG 2.1 AA compliance
28- Cross-browser compatibility
29
30## Performance
31
32- Lighthouse Performance: 95+
33- First Contentful Paint: < 1.5s
34- Largest Contentful Paint: < 2.5s
35- Cumulative Layout Shift: < 0.1Résumé du projet
Ce projet illustre l'application pratique de toutes les techniques CSS avancées apprises dans ce module :
- Architecture CSS - Cascade Layers, CSS Nesting, BEM
- Mise en page moderne - Container Queries, CSS Grid, Subgrid
- Stylisation avancée - Variables CSS, propriétés personnalisées
- Interactions - animations, effets au survol, animations pilotées par le défilement
- Accessibilité - conformité WCAG, gestion du focus
- Performance - containment CSS, lazy loading
- Multi-navigateurs - amélioration progressive, replis
Le projet peut être enrichi de fonctionnalités supplémentaires et constitue une excellente base pour poursuivre le développement de tes compétences en CSS.
Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>@supports</title>
6 <link rel="stylesheet" href="style.css">
7</head>
8<body>
9 <h1>@supports - Amélioration progressive</h1>
10 <div class="grid-demo">
11 <div class="box">Élément 1</div>
12 <div class="box">Élément 2</div>
13 <div class="box">Élément 3</div>
14 <div class="box">Élément 4</div>
15 <div class="box">Élément 5</div>
16 <div class="box">Élément 6</div>
17 </div>
18 <div class="container-demo">
19 <div class="card">
20 <h2>Carte adaptative</h2>
21 <p>Utilise les container queries si disponibles.</p>
22 </div>
23 </div>
24</body>
25</html>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'utilise-t-on dans le projet de portfolio pour définir des design tokens tels que les couleurs et l'espacement ?
2. Comment le thème sombre/clair est-il implémenté directement dans le CSS du projet ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Mets dans l'ordre les tokens d'espacement du projet (--space-*), de la plus petite à la plus grande valeur :