Skip to content

julienborgeon/fem-four-card-feature-section

Repository files navigation

🇫🇷 Français · English

Frontend Mentor – Four card feature section (solution)

Vite Tailwind CSS v4 Netlify Live

Ce repo contient ma solution au défi Four card feature section de Frontend Mentor.

🎯 Objectif personnel : apprendre à maîtriser Tailwind V4 pour composer une grille responsive propre (mobile → tablette → desktop).


🔎 Aperçu

Le challenge

  • Reproduire la section “Four card feature” sur mobile et desktop.

Capture

Screenshot de la solution

Liens


🚀 Lancer le projet

npm install
npm run dev      # serveur local
npm run build    # build de production (dossier dist/)
npm run preview  # prévisualisation du build

🧱 Construit avec

  • HTML5 sémantique (main, section, header, footer, article + aria-labelledby)
  • Tailwind CSS v4 (approche CSS-first avec @theme pour définir des tokens)
  • Mobile-first workflow
  • Polices locales WOFF2 Poppins 200/400/600 + font-display: swap + preload.
  • Vite (build performant, purge console/debugger, assets fingerprintés)
  • Netlify (headers de sécurité & cache via netlify.toml)

🧭 Démarche & choix d’implémentation

Structure HTML

  • Un <h1> masqué visuellement sert de titre de page et labellise la section via aria-labelledby.
  • Chaque carte est un <article> avec son propre h3.
  • Les icônes sont strictement décorativesalt="" + aria-hidden="true" et dimensions explicites (width/height) pour éviter le CLS.

Extrait :

<h1 class="sr-only" id="page-title">Four card feature section</h1>
<section class="mx-auto" aria-labelledby="page-title">
  <article
    class="card top-border accent-cyan area-super shadow-mix rounded-md p-8"
    aria-labelledby="feat-supervisor"
  >
    <header>
      <h3 class="text-grey-500 mb-1 text-lg font-semibold" id="feat-supervisor">
        Supervisor
      </h3>
    </header>
    <p class="text-sm/relaxed">
      Monitors activity to identify project roadblocks
    </p>
    <footer>
      <img
        class="mt-8 mb-4 ml-auto"
        src="/assets/icon-supervisor.svg"
        width="64"
        height="64"
        aria-hidden="true"
        alt=""
      />
    </footer>
  </article>
</section>

Design System (Tailwind v4)

Tokens de couleurs & fonts via @theme ; accent bleu exposé en HSL (composantes séparées) pour réutiliser facilement la teinte dans les ombres.

Extrait :

@theme {
  --color-red: hsl(0, 78%, 62%);
  --color-cyan: hsl(180, 62%, 55%);
  --color-orange: hsl(34, 97%, 64%);
  --color-blue: hsl(212, 86%, 64%);
  --color-grey-500: hsl(234, 12%, 34%);
  --color-grey-400: hsl(212, 6%, 44%);
  --color-accent: 212 86% 64%; /* HSL parts for alpha mixing */
  --font-sans: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-poppins: "Poppins", var(--font-sans);
}

Grille responsive (mobile → tablette → desktop)

Empilement mobile par défaut ; tablette en 2 colonnes (super/karma puis team/calc) ; desktop en 3 colonnes avec Supervisor et Calculator qui encadrent (span vertical) et Team/Karma au centre.

J'ai d'abord pensé à wrapper ensemble le deuxième et troisième articles, et de les intégrer dans une grille à trois colonnes sur desktop, mais le design manquait de responsivité sur tablette (il fallait une version à deux colonnes). Pour y parvenir, j'ai opté pour la solution offerte par grid-template-areas afin de maîtriser totalement l'emplacement des cards dans la grille.

Extrait :

@layer components {
  .grid-cards {
    display: grid;
    gap: 1.5rem;
    align-items: center;
    justify-items: center;

    /* Tablet */
    @media (min-width: 48rem) {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      grid-template-areas:
        "super karma"
        "team  calc";
    }

    /* Desktop */
    @media (min-width: 68rem) {
      grid-template-columns: repeat(3, minmax(0, 1fr));
      grid-template-areas:
        "super team calc"
        "super karma calc";
    }
  }

  /* Déclarés seulement à partir de 48rem, quand le template en a besoin, pour éviter l'empilement sur mobile */
  .area-super {
    @media (min-width: 48rem) {
      grid-area: super;
    }
  }
  .area-team {
    @media (min-width: 48rem) {
      grid-area: team;
    }
  }
  .area-karma {
    @media (min-width: 48rem) {
      grid-area: karma;
    }
  }
  .area-calc {
    @media (min-width: 48rem) {
      grid-area: calc;
    }
  }
}

Détail UI : bordure supérieure des cards (illusion “dans la card”)

J’ai d’abord testé une border-top: 4px directement sur la card ; mais avec les coins arrondis, la bordure suivait la courbure latérale et ne rendait pas l’effet souhaité. Pour donner l’illusion d’un liseré qui s’insère dans la card, j’ai ajouté un pseudo-élément sur chaque card, avec ses propres rayons.

Extrait :

.top-border {
  position: relative;
}
.top-border::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  border-top-left-radius: 6px;
  border-top-right-radius: 6px;
}

/* Couleurs par accent */
.accent-cyan::before {
  background-color: var(--color-cyan);
}
.accent-red::before {
  background-color: var(--color-red);
}
.accent-orange::before {
  background-color: var(--color-orange);
}
.accent-blue::before {
  background-color: var(--color-blue);
}

Avantage : on garde les coins externes de la card propres, tout en contrôlant exactement la forme et la couleur du liseré supérieur.

Ombres portées “mix” (gris + teinte bleue)

Pour retrouver la légère teinte bleue du design tout en conservant une ombre douce et moderne, j’empile plusieurs couches : deux grises + une bleutée (HSL avec alpha). Cela évite une ombre trop “colorée” tout en restant cohérent avec la palette.

Extrait :

@layer components {
  .shadow-mix {
    box-shadow:
      0 20px 30px -12px rgba(0, 0, 0, 0.1),
      0 12px 18px -10px rgba(0, 0, 0, 0.02),
      0 18px 28px -14px hsl(var(--color-accent) / 0.3);
    transition: box-shadow 0.25s ease-out /* + transform si souhaité */;
  }

  @media (hover: hover) and (pointer: fine) {
    .shadow-mix:hover {
      box-shadow:
        0 26px 36px -13px rgba(0, 0, 0, 0.1),
        0 18px 26px -11px rgba(0, 0, 0, 0.02),
        0 26px 36px -15px hsl(var(--color-accent) / 0.3);
      /* optionnel : transform: translateY(-2px); */
    }
  }
}

Remarque : si vous observez un léger flou du texte au survol sous Windows/Chrome, évitez transform sur l’élément contenant du texte et animez uniquement l’ombre, ou appliquez le transform sur un pseudo-élément.


♿ Accessibilité (A11y)

  • Hiérarchie logique : h1 (SR-only) → h2 introductif → h3 par carte.
  • Association section ↔ titre via aria-labelledby.
  • Icônes décoratives masquées (alt="", aria-hidden="true").
  • Dimensions d’images explicites (width/height) pour la stabilité du layout.

⚡ Performance

  • Tailwind v4 JIT → CSS minimal.
  • Polices WOFF2 + preload ciblé + swap.
  • Vite : sourcemap: false, drop: ["console","debugger"], assetsInlineLimit: 0 pour éviter l’inlining d’assets.
  • Netlify : cache long sur assets fingerprintés/polices, en-têtes de sécurité pour la version en prod publiée pour la preview.

🧪 Ma démarche

Technologies utilisées

  • Semantic HTML5 markup + ARIA minimale.
  • CSS custom properties (via tokens Tailwind v4 dans @theme et @layer components pour des utilitaires maison (grille, accents, ombres, liseré supérieur))
  • Mobile-first workflow
  • Vite (bundler/build)
  • Tailwind CSS v4
  • Netlify (hébergement & headers)

Ressources utiles

👤 Auteur


🙌 Remerciements

Merci à Frontend Mentor pour le design du challenge et à la communauté pour les retours.
Un clin d’œil aux ressources MDN & Tailwind pour la clarté de leur doc.


📁 Structure du projet

└── 📁fem-four-card-feature-section
    └── 📁.vscode
        ├── settings.json
    └── 📁public
        └── 📁assets
            ├── favicon-32x32.png
            ├── icon-calculator.svg
            ├── icon-karma.svg
            ├── icon-supervisor.svg
            ├── icon-team-builder.svg
        └── 📁fonts
            ├── poppins-v23-latin-200.woff2
            ├── poppins-v23-latin-600.woff2
            ├── poppins-v23-latin-regular.woff2
        ├── og.jpg
    └── 📁src
        ├── main.css
    ├── .gitattributes
    ├── .gitignore
    ├── .prettierrc
    ├── index.html
    ├── netlify.toml
    ├── package-lock.json
    ├── package.json
    ├── README.en.md
    ├── README.md
    └── vite.config.js

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •