Cours HTML et CSS · Module 9: CSS moderne

Projet - Page portfolio moderne avec des techniques CSS avancées

14 min de lecture
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>&copy; 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é

  1. View Transitions API - ajouter des transitions fluides entre les sections
  2. CSS Scroll Snap - défilement avec accrochage entre les sections
  3. CSS @property - propriétés personnalisées animées
  4. Intersection Observer - lazy loading et animations
  5. Service Worker - mise en cache pour la fonctionnalité hors ligne

Niveau expert

  1. CSS Houdini Paint API - dégradés personnalisés
  2. CSS Parser API - styles dynamiques
  3. Web Animations API - animations JS avancées
  4. 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.1

Résumé du projet

Ce projet illustre l'application pratique de toutes les techniques CSS avancées apprises dans ce module :

  1. Architecture CSS - Cascade Layers, CSS Nesting, BEM
  2. Mise en page moderne - Container Queries, CSS Grid, Subgrid
  3. Stylisation avancée - Variables CSS, propriétés personnalisées
  4. Interactions - animations, effets au survol, animations pilotées par le défilement
  5. Accessibilité - conformité WCAG, gestion du focus
  6. Performance - containment CSS, lazy loading
  7. 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. 1. Qu'utilise-t-on dans le projet de portfolio pour définir des design tokens tels que les couleurs et l'espacement ?

  2. 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 :

Articles utiles