Formation Créer un Framework CSS | Les Composants

Boutons, boîtes d'alerte, cartes... Découvrez comment créer des composants d'interface, essentiels pour un design system cohérent.

Icône de calendrier MAJ en
Débutant
6 chapitres

Qu’est-ce qu’un composant ?

Un composant est une unité autonome et réutilisable qui permet de structurer et de styliser des éléments de manière modulaire dans une application ou un site web.

Les composants facilitent la maintenance et l’évolution d’un projet en permettant la réutilisation de styles et de comportements identiques à différents endroits d’une interface.

Composants pour notre framework

Boutons

Incontournables sur la plupart des interfaces, les boutons contribuent à ajouter de l’interactivité en permettant aux utilisateurs de déclencher des actions en les cliquant ou touchant (sur mobile).

Parmi ces actions, on retrouve généralement :

  • La navigation
  • La soumission d’un formulaire
  • L’affichage d’une popup
  • Etc.

Essentiels pour l’expérience utilisateur, ils peuvent être déclinés en plusieurs styles (type, couleurs…) pour s’adapter à différentes utilisations et offrir une hiérarchie visuelle dans les actions proposées.

Par défaut, un bouton aura un style « plein », autrement dit seul son fond sera coloré. ce style sera défini au sein de la classe .btn.

copié !
<a class="btn" href="#">.btn</a>

On souhaiterait obtenir le résultat suivant :

.btn

Afin d’optimiser le contraste des boutons sur l’interface, nous définirons la couleur par défaut comme étant la couleur dark.

components/_button.css
copié !
:root {
	--btn-color: var(--dark-light);
	--btn-color-contrast: var(--light-dark);
}
  • --btn-color est la couleur du bouton
  • --btn-color-contrast est la couleur de texte assurant un bon contraste pour les boutons pleins

Vient le moment de définir la classe de base .btn :

components/_button.css
copié !
/* VARIABLES ... */

.btn {
	border-radius: calc(var(--base-unit) / 4);
	background-color: var(--btn-color);
	color: var(--btn-color-contrast);
	cursor: pointer;
	display: inline-block;
	padding: calc(var(--base-unit) * 0.625) calc(var(--base-unit) * 1);
	border-style: solid;
	border-width: 1px;
	border-color: var(--btn-color);
	outline: none;
}

Dans notre framework nous créerons des boutons déclinés selon 2 caractéristiques :

  • Le type (plein ou en contour)
  • La couleur (primary, success, warning et danger)

Il est donc temps de créer quelques « modificateurs d’état ».

Type

La class .is-outline rendra notre bouton plus discret, dont seuls le texte et la bordure seront de couleur.

Voici le code de la classe de modificateur d’état .is-outline :

components/_button.css
copié !
/* VARIABLES ... */

/* .btn ... */

.btn.is-outline {
	color: var(--btn-color);
	border-color: var(--btn-color);
	background-color: transparent;
}

.btn.is-outline:hover {
	color: var(--btn-color-contrast);
	background-color: var(--btn-color);
}

La pseudo classe :hover nous permet ici de transformer visuellement notre bouton de contour en bouton plein lorsqu’il est survolé.

Il est désormais possible de créer des boutons dans le HTML de la manière suivante :

copié !
<a class="btn is-outline" href="#">.btn .is-outline</a>

On obtient le résultat suivant :

.btn .is-outline

Couleur

Sur cette base de style, plein ou en contour, il serait intéressant d’avoir des classes permettant de modifier la couleur du bouton.

  • .is-primary pour appliquer la couleur principale
  • .is-success, .is-warning et .is-danger pour appliquer les couleurs d’état

Plutôt que de redéfinir pour chaque classe de couleur les propriétés CSS, nous allons procéder de manière plus propre, en redéfinissant simplement les variables --btn-color et --btn-color-contrast lorsque la class .btn est utilisée conjointement avec les classes .is-primary, .is-success, .is-warning et .is-danger.

components/_button.css
copié !
/* VARIABLES */

/* .btn */

/* .is-outline */

.btn.is-primary {
	--btn-color: var(--primary);
	--btn-color-contrast: var(--primary-contrast);
}

.btn.is-success {
	--btn-color: var(--success);
	--btn-color-contrast: var(--success-contrast);
}

.btn.is-warning {
	--btn-color: var(--warning);
	--btn-color-contrast: var(--warning-contrast);
}

.btn.is-danger {
	--btn-color: var(--danger);
	--btn-color-contrast: var(--danger-contrast);
}

Il est désormais possible de créer des boutons de couleur dans le HTML de la manière suivante :

copié !
<!-- 🌓 DEFAULT -->
<a class="btn" href="#">.btn</a>
<!-- ✨ PRIMARY -->
<a class="btn is-primary" href="#">.btn .is-primary</a>
<!-- ✅ SUCCESS -->
<a class="btn is-success" href="#">.btn .is-success</a>
<!-- ⚠️ WARNING -->
<a class="btn is-warning" href="#">.btn .is-warning</a>
<!-- ❌ DANGER -->
<a class="btn is-danger" href="#">.btn .is-danger</a>

On obtient le résultat suivant :

Cartes

Les cartes sont des conteneurs visuels permettant de présenter un contenu de manière structurée et attrayante dans un format compact et cohérent, facilement lisible pour l’utilisateur.

Les cartes regroupent divers éléments comme des :

  1. Médias (images, icônes, vidéos…)
  2. Textes (titre, description…)
  3. Eléménts d’action (boutons, liens…)

Elles sont généralement utilisées pour afficher des « previews » de contenus tels que des :

  • Article de blog
  • Produit ecommerce
  • Profils utilisateurs
  • Publications sur un réseau social
  • Etc.

Chaque carte peut ainsi avoir plusieurs configurations pour s’adapter au contexte et aux données qu’elle contient.

Dans notre framework nous créerons des cartes structurées en 2 parties :

  • L’en-tête : contenant une image
  • Le corps : contenant un titre, un texte descriptif, des boutons, des liens…

La présentation de nos cartes pourra varier selon 2 formats : vertical (par défaut) et horizontal.

Carte verticale

Notre carte de base inclura les éléments principaux et utilisera des classes pour leur stylisation :

  • .card : la classe principale de la carte, définissant sa structure et ses marges
    • .card-header : pour l’en-tête de la carte (contenant une image illustrative)
    • .card-body : pour le corps de la carte
      • .card-title : pour le titre de la carte
      • .card-description : pour du texte descriptif
      • .card-actions : pour les actions (boutons, liens…), souvent situées en bas de la carte
copié !
<div class="card">
  <header class="card-header">
    <img
      src="..."
      alt="Description de l'image"
    />
  </header>
  <div class="card-body">
    <h3 class="card-title">Titre</h3>
    <p class="card-description">Lorem ipsum dolor sit amet...</p>
    <div class="card-actions">
      ...
    </div>
  </div>
</div>

On souhaite obtenir le résultat suivant :

Illustration d'exemple

Titre

Lorem ipsum dolor sit amet…

On souhaite que nos cartes puissent se détacher visuellement de la couleur de fond de l’interface. Pour cela on définira deux variables --card-color-background et --card-color-text.

components/_card.css
copié !
:root {
	--card-color-background: var(--light-3);
	--card-color-text: var(--dark-2);
}

Le body étant défini par notre fichier 📄 _reset.css à la valeur de notre couleur --light-2, --card-color-background: var(--light-3) permet ici de faire ressortir les cartes en étant légèrement plus claires.

Card container

Créons désormais la class .card, jouant le rôle de conteneur de notre carte.

copié !
<div class="card">
  ...
</div>
components/_card.css
copié !
/* VARIABLES ... */

.card {
  background-color: var(--card-color-background);
  color: var(--card-color-text);
  border-radius: calc(var(--base-unit) / 2);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
  • Les couleurs définies préalablement sont appliquées au conteneur.
  • Pour maximiser la cohérence globale, les arrondis sont calculés à partir de notre variable de référence --base-unit.
  • On empile .card-header et .card-body avec flex.
  • On empêche le contenu de la carte de sortir du conteneur avec overflow: hidden (cela s’avèrera utile pour empêcher l’image de l’en-tête de la carte de sortir des angles arrondis).
Card header

Notre carte intègrera en en-tête (.card-header) la possibilité d’insérer une image.

copié !
<div class="card">
  <header class="card-header">
    <img
      src="..."
      alt="Description de l'image"
    />
  </header>
  ...
</div>
components/_card.css
copié !
/* VARIABLES ... */

/* .card ... */

.card-header {
	overflow: hidden;
}

.card-header > img {
	display: block;
	width: 100%;
}

.card:hover .card-header > img {
	transform: scale(1.1);
}
  • On demande à l’image d’occuper 100% de l’en-tête de la carte.
  • Le display: block est une astuce permettant de supprimer des espaces blancs potentiels en bas de l’image causés par le display: inline par défaut de la balise <img>, surtout si on est dans un contexte flex ou grid.
  • Au survol de la carte toute entière, l’image sera zoomée. On empêche donc l’image de sortir de l’en-tête et d’empiéter sur .card-body lors du zoom avec overflow: hidden.
Card body

Notre carte intègrera dans son corps (.card-body) la possibilité d’insérer un titre, un texte descriptif et des éléments d’actions (boutons, liens…).

copié !
<div class="card">
  <header class="card-header">
    <img
      src="..."
      alt="Description de l'image"
    />
  </header>
  <div class="card-body">
    <h3 class="card-title">Titre</h3>
    <p class="card-description">Lorem ipsum dolor sit amet...</p>
    <div class="card-actions">
      ...
    </div>
  </div>
</div>
components/_card.css
copié !
/* VARIABLES ... */

/* .card ... */

/* .card-header ... */

.card-body {
	padding: calc(var(--base-unit) * 1.5);
	display: flex;
	flex-direction: column;
	gap: var(--base-unit);
	align-items: flex-start;
}

.card-title {
	font-size: calc(var(--base-unit) * 1.375);
	line-height: calc(calc(var(--base-unit) * 1.375) * var(--ratio-line-height));
	font-weight: 700;
}

.card-description {
	font-size: var(--base-unit);
	line-height: calc(var(--base-unit) * var(--ratio-line-height));
  font-weight: 400;
}

.card-actions {
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--base-unit) / 2);
	align-items: center;
}
  • Le corps de la carte se détachera des bordures du conteneur avec des marges internes calculées à partir de notre variable de référence --base-unit.
  • On empile .card-title, .card-description et .card-actions avec flex.
  • .card-title et .card-description ont une taille et une hauteur de ligne calculés à partir de nos variables de référence --base-unit et --ratio-line-height ainsi qu’une graisse spécifique.
  • .card-actions pourra accueillir des éléments alignés proprement en ligne.

Carte horizontale

Il pourrait être intéressant de décliner nos cartes au format horizontal. Concrètement, notre image d’en-tête serait située sur la gauche et non en haut du corps de la carte.

Pour créer cette déclinaison nous créerons une classe de modificateur d’état nommée .is-inline.

copié !
<div class="card is-inline">
  ...
</div>

Ce modificateur contribuera à adopter plusieurs modifications.

D’abord, on conditionne la flex-direction au type de carte :

components/_card.css
copié !
.card {
  /* ... */
  flex-direction: column;
}

.card:not(.is-inline) {
	flex-direction: column;
}

.card.is-inline {
	flex-direction: row;
}

Ensuite, on fait en sorte que si la carte est horizontale, l’image d’en-tête occupe 25% de la largeur et toute la hauteur de la carte :

components/_card.css
copié !
.card.is-inline .card-header {
  max-width: 25%;
}

.card.is-inline .card-header > img {
	height: 100%;
	object-fit: cover;
}

.card.is-inline .card-body {
	align-self: center;
}

align-self: center permet de centrer .card-body, dans le cas où l’image est plus haute.

Enfin, on ajoute un modificateur, applicable sur .card-header afin de modifier le ratio (par défaut 25%) que doit occuper l’image.

copié !
<div class="card is-inline">
  <header class="card-header is-1/3">...</header>
  <div class="card-body">...</div>
</div>
components/_card.css
copié !
.card.is-inline .card-header.is-1\/2 {
	max-width: calc(100% / 2);
}

.card.is-inline .card-header.is-1\/3 {
	max-width: calc(100% / 3);
}

.card.is-inline .card-header.is-1\/4 {
	max-width: calc(100% / 4);
}

.card.is-inline .card-header.is-1\/5 {
	max-width: calc(100% / 5);
}

Le fait de travailler en divisant 100% permet d’obtenir des valeurs exactes. C’est par exemple utile pour éviter d’arrondir 1/3 à 33%.

On obtient alors le résultat suivant :

Illustration d'exemple

Titre

Lorem ipsum dolor sit amet…

Alertes

Les boîtes d’alerte jouent un rôle clé pour donner du feedback aux utilisateurs.

Elles informent les utilisateurs, généralement à la suite d’interactions avec l’interface. Ces interactions se traduisent par exemple par :

  • Le signalement d’erreurs lors de la saisie de données dans un formulaire
  • La confirmation du succès d’une action (création, modification ou suppression d’une ressource…)
  • Etc.

La présence de ces boîtes n’est pas systématiquement le fruit d’interactions utilisateur et peuvent être présentes dès le chargement de la page pour par exemple :

  • La fourniture d’informations contextuelles pour guider l’utilisateur
  • Avertir/mettre en garde l’utilisateur (mises à jour nécessaires, valider son inscription par email, changement de politique tarifaire…)

En communiquant ces informations, les boîtes d’alerte contribuent à améliorer l’expérience globale de l’utilisateur.

Essentielles pour l’expérience utilisateur, elles peuvent être déclinées en plusieurs couleurs, selon le type de message à transmettre (erreur, attention, succès… cf. les couleurs d’état).

Par défaut, une boîte d’alerte sera définie au sein de la classe .alert.

copié !
<div class="alert">.alert</div>

On souhaiterait obtenir le résultat suivant :

.alert

Afin d’optimiser le contraste des boîtes d’alerte sur l’interface, nous définirons la couleur par défaut comme étant la couleur dark.

components/_alert_.css
copié !
:root {
	--alert-color: var(--dark-2);
	--alert-color-contrast: var(--light-2);
}
  • --alert-color est la couleur de la boîte d’alerte
  • --alert-color-contrast est la couleur de texte assurant un bon contraste

Ensuite, vient le moment de définir la classe de base .alert :

components/_alert.css
copié !
/* VARIABLES ... */

.alert {
	padding: calc(var(--base-unit) * 1.5);
	border-radius: calc(var(--base-unit) / 2);
	background-color: var(--alert-color);
	color: var(--alert-color-contrast);
}

Comme pour les boutons, nous souhaitons décliner nos boîtes selon les couleurs de notre identité graphique : primary, success, warning et danger.

Il est donc temps de créer quelques « modificateurs d’état ».

Couleur

Sur cette base de style, il serait intéressant d’avoir des classes permettant de modifier la couleur des boîtes d’alerte.

  • .is-primary pour appliquer la couleur principale
  • .is-success, .is-warning et .is-danger pour appliquer les couleurs d’état

Plutôt que de redéfinir pour chaque classe de couleur les propriétés CSS, nous allons procéder de manière plus propre, en redéfinissant simplement les variables --alert-color et --alert-color-contrast lorsque la classe .alert est utilisée conjointement avec les classes .is-primary, .is-success, .is-warning et .is-danger.

components/_card.css
copié !
/* VARIABLES */

/* .alert */

.alert.is-primary {
	--alert-color: var(--primary);
	--alert-color-contrast: var(--primary-contrast);
}

.alert.is-success {
	--alert-color: var(--success);
	--alert-color-contrast: var(--success-contrast);
}

.alert.is-warning {
	--alert-color: var(--warning);
	--alert-color-contrast: var(--warning-contrast);
}

.alert.is-danger {
	--alert-color: var(--danger);
	--alert-color-contrast: var(--danger-contrast);
}

Il est désormais possible de créer des boîtes d’alerte de couleur dans le HTML de la manière suivante :

copié !
<!-- 🌓 DEFAULT -->
<div class="alert">.alert</div>
<!-- ✨ PRIMARY -->
<div class="alert is-primary">.alert .is-primary</div>
<!-- ✅ SUCCESS -->
<div class="alert is-success">.alert .is-success</div>
<!-- ⚠️ WARNING -->
<div class="alert is-warning">.alert .is-warning</div>
<!-- ❌ DANGER -->
<div class="alert is-danger">.alert .is-danger</div>

On obtient le résultat suivant :

.alert
.alert .is-primary
.alert .is-success
.alert .is-warning
.alert .is-danger

Code complet

Voici le code complet des fichiers 📄 _button.css, 📄 _alert.css et 📄 _card.css ainsi que du fichier global 📄 all.css :

css/components/_button.css
copié !
:root {
	--btn-color: var(--dark-2);
	--btn-color-contrast: var(--light-2);
}

.btn {
	border-radius: calc(var(--base-unit) / 4);
	background-color: var(--btn-color);
	color: var(--btn-color-contrast);
	cursor: pointer;
	display: inline-block;
	padding: calc(var(--base-unit) * 0.625) calc(var(--base-unit) * 1);
	border-style: solid;
	border-width: 1px;
	border-color: var(--btn-color);
	outline: none;
}

.btn.is-outline {
	color: var(--btn-color);
	border-color: var(--btn-color);
	background-color: transparent;
}

.btn.is-outline:hover {
	color: var(--btn-color-contrast);
	background-color: var(--btn-color);
}

.btn.is-primary {
	--btn-color: var(--primary);
	--btn-color-contrast: var(--primary-contrast);
}

.btn.is-success {
	--btn-color: var(--success);
	--btn-color-contrast: var(--success-contrast);
}

.btn.is-warning {
	--btn-color: var(--warning);
	--btn-color-contrast: var(--warning-contrast);
}

.btn.is-danger {
	--btn-color: var(--danger);
	--btn-color-contrast: var(--danger-contrast);
}
css/components/_alert.css
copié !
:root {
	--alert-color: var(--dark-2);
	--alert-color-contrast: var(--light-2);
}

.alert {
	padding: calc(var(--base-unit) * 1.5);
	border-radius: calc(var(--base-unit) / 2);
	background-color: var(--alert-color);
	color: var(--alert-color-contrast);
}

.alert.is-primary {
	--alert-color: var(--primary);
	--alert-color-contrast: var(--primary-contrast);
}

.alert.is-success {
	--alert-color: var(--success);
	--alert-color-contrast: var(--success-contrast);
}

.alert.is-warning {
	--alert-color: var(--warning);
	--alert-color-contrast: var(--warning-contrast);
}

.alert.is-danger {
	--alert-color: var(--danger);
	--alert-color-contrast: var(--danger-contrast);
}
css/components/_card.css
copié !
:root {
	--card-color-background: var(--light-3);
	--card-color-text: var(--dark-2);
}

.card {
	background-color: var(--card-color-background);
	color: var(--card-color-text);
	border-radius: calc(var(--base-unit) / 2);
	display: flex;
	overflow: hidden;
}

.card:not(.is-inline) {
	flex-direction: column;
}

.card-header {
	overflow: hidden;
}

.card-header > img {
	display: block;
	width: 100%;
}

.card:hover .card-header > img {
	transform: scale(1.1);
}

.card-body {
	padding: calc(var(--base-unit) * 1.5);
	display: flex;
	flex-direction: column;
	gap: var(--base-unit);
	align-items: flex-start;
}

.card-title {
	font-size: calc(var(--base-unit) * 1.375);
	line-height: calc(calc(var(--base-unit) * 1.375) * var(--ratio-line-height));
	font-weight: 700;
}

.card-description {
	font-size: var(--base-unit);
	line-height: calc(var(--base-unit) * var(--ratio-line-height));
	font-weight: 400;
}

.card-actions {
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--base-unit) / 2);
	align-items: center;
}

.card.is-inline {
	flex-direction: row;
}

.card.is-inline .card-header {
  max-width: 25%;
}

.card.is-inline .card-header > img {
	height: 100%;
	object-fit: cover;
}

.card.is-inline .card-header.is-1\/2 {
	max-width: calc(100% / 2);
}

.card.is-inline .card-header.is-1\/3 {
	max-width: calc(100% / 3);
}

.card.is-inline .card-header.is-1\/4 {
	max-width: calc(100% / 4);
}

.card.is-inline .card-header.is-1\/5 {
	max-width: calc(100% / 5);
}

.card.is-inline .card-body {
	align-self: center;
}
css/components/all.css
copié !
@import '_card.css';
@import '_button.css';
@import '_alert.css';

Les composants du framework sont importés dans 📄 app.css :

css/app.css
copié !
@import 'config.css';
@import 'base/all.css';
@import 'layout/all.css';
@import 'components/all.css';
@import 'utilities/all.css';