Aller au contenu

HTML⚓︎

Un gabarit minimaliste⚓︎

🐣 2022-08

Parce qu’il faut bien copier-coller une source pour démarrer .

template.html
<!doctype html><!-- (1)! -->
<html lang="fr"><!-- (2)! -->
<head>
  <meta charset="utf-8"><!-- (3)! -->
  <!-- (4)! -->
  <meta name="viewport"
        content="width=device-width,initial-scale=1"><!-- (5)! -->
  <meta name="referrer" content="origin-when-cross-origin"><!-- (6)! -->
  <title>Title</title><!-- (1)! -->
  <link rel="icon" href="data:;base64,iVBORw0KGgo="><!-- (7)! -->
  <link rel="stylesheet" href="style.css" />
  <style></style>
</head>
<body>
  <header>
    <!-- logo etc -->
    <h1>Title</h1>
    <nav></nav>
    <form role="search"></form> <!-- if it exists -->
  </header>

  <main><!-- (8)! -->
    <!-- what your page is all about -->
  </main>

  <aside>
    <!-- complementary information -->
  </aside>

  <footer>
    <!-- copyright notice etc -->
    <nav></nav> <!-- if you want to -->
  </footer>
  <script type="module"></script>
</body>
  1. Pour en faire un document valide HTML5.
  2. Pour les lecteurs d’écran, le référencement, les extensions, etc.
  3. Doit être dans les premiers 1024 bytes, plutôt à mettre avant le <title>, référence à ce sujet.
  4. Pourquoi pas de balise meta avec une valeur X-UA-Compatible ? Voir cette réponse sur StackOverflow.
  5. Beaucoup de monde dans la balise meta relative au viewport, et nous sommes responsables de cela…
  6. Avec cette valeur, le site lié ne saura pas de quelle page provient le lien qui a été cliqué mais aura uniquement l’information du domaine. Cela me semble être un bon compromis pour préserver l’intimité des personnes.
  7. Gif transparent le plus petit qui soit, évite une requête inutile vers le serveur (et une erreur dans la console).
  8. Ne doit pas être contenu dans une <section>

🤳 Capturer une image/vidéo de la webcam depuis le navigateur⚓︎

🐣 2022-09

Exploration de Austin Gil.

C’est uniquement possible sur certains navigateurs mobiles mais c’est bon à savoir.

capture.html
<input type="file" accept="image/*" capture="environment">

Styler des colonnes de tableaux⚓︎

🐣 2022-10

J’ai toujours pensé qu’il fallait le faire à la main cellule par cellule mais en fait avec colgroup/col c’est tout à fait possible et en jouant avec :target on peut même le rendre interactif :

table-columns.html
<table>
  <caption>
    Frequency and average use of heading elements.
  </caption>
  <colgroup>
    <col id="table1-heading" />
    <col id="table1-occurrences" />
    <col id="table1-average" />
  </colgroup>
  <thead>
    <tr>
      <th><a href="#table1-heading">Heading</a></th>
      <th><a href="#table1-occurrences">Occurrences</a></th>
      <th><a href="#table1-average">Average per page</a></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>h1</code></td>
      <td>10,524,810</td>
      <td>1.66</td>
    </tr>
    <tr>
      <td><code>h2</code></td>
      <td>37,312,338</td>
      <td>5.88</td>
    </tr>
    <tr>
      <td><code>h3</code></td>
      <td>44,135,313</td>
      <td>6.96</td>
    </tr>
    <tr>
      <td><code>h4</code></td>
      <td>20,473,598</td>
      <td>3.23</td>
    </tr>
    <tr>
      <td><code>h5</code></td>
      <td>8,594,500</td>
      <td>1.36</td>
    </tr>
    <tr>
      <td><code>h6</code></td>
      <td>3,527,470</td>
      <td>0.56</td>
    </tr>
    <tr>
      <td><code>h7</code></td>
      <td>30,073</td>
      <td>0.005</td>
    </tr>
    <tr>
      <td><code>h8</code></td>
      <td>9,266</td>
      <td>0.0015</td>
    </tr>
  </tbody>
</table>

Pour la partie CSS :

1
2
3
col:target {
  background: #dedede;
}

Trouvé chez Manuel Matuzovic.

Faire grandir automatiquement une textarea⚓︎

🐣 2022-10

On commence avec le HTML suivant qui confère un parent (grow-wrap) au textarea ciblé :

1
2
3
4
5
<div class="grow-wrap">
  <textarea name="text" id="text" 
    onInput="this.parentNode.dataset.replicatedValue = this.value"
    ></textarea>
</div>

Info

La partie onInput pourrait être faite en JavaScript hors du HTML pour une réutilisation/séparation plus propre.

Un exemple possible :

textarea-grow.js
1
2
3
4
5
6
7
8
const growers = document.querySelectorAll('.grow-wrap')

growers.forEach((grower) => {
  const textarea = grower.querySelector('textarea')
  textarea.addEventListener('input', () => {
    grower.dataset.replicatedValue = textarea.value
  })
})

La suite se passe en CSS, une fois la valeur de la textarea dans le parent, on se sert de celui-ci pour obtenir la taille désirée du :after en fonction de l’espace utilisé.

textarea-grow.css
.grow-wrap {
  /* easy way to plop the elements on top of each other
  and have them both sized based on the tallest one's height */
  display: grid;
}
.grow-wrap::after {
  /* Note the weird space! Needed to preventy jumpy behavior */
  content: attr(data-replicated-value) ' ';

  /* This is how textarea text behaves */
  white-space: pre-wrap;

  /* Hidden from view, clicks, and screen readers */
  visibility: hidden;
}
.grow-wrap > textarea {
  /* You could leave this, but after a user resizes,
  then it ruins the auto sizing */
  resize: none;

  /* Firefox shows scrollbar on growth, you can hide like this. */
  overflow: hidden;
}
.grow-wrap > textarea,
.grow-wrap::after {
  /* Identical styling required!! */
  border: 1px solid black;
  padding: 0.5rem;
  font: inherit;

  /* Place on top of each other */
  grid-area: 1 / 1 / 2 / 2;
}

Il y a un article complet sur CSS-Tricks qui liste aussi des alternatives comportant davantage de JavaScript comme celle de Jim Nielsen.

Prendre en compte les préférences utilisateur·ice pour lancer une animation⚓︎

🐣 2022-10

J’ai découvert qu’il était possible d’afficher une image statique plutôt qu’un gif animé en fonction de la propriété prefers-reduced-motion directement dans le HTML :

1
2
3
4
<picture>
  <source srcset="moonwalk.png" media="(prefers-reduced-motion: reduce)" />
  <img src="moonwalk.gif" alt="Someone doing the moonwalk" />
</picture>

Voir aussi ce qu’il est possible de faire en CSS et en JS.

Info

On peut aussi se servir des media-queries dans le HTML pour adapter les images d’un thème clair/sombre :

1
2
3
4
<picture>
  <source srcset="dark_logo.jpg" media="(prefers-color-scheme: dark)" />
  <img src="light_logo.jpg" alt="Homepage" />
</picture>

Adapter le favicon au thème clair/sombre⚓︎

🐣 2022-10

Il est possible d’utiliser les media-queries pour adapter les couleurs du favicon en fonction du thème clair/foncé choisi.

favicon.svg
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
  <style>
    circle {
      fill: yellow;
      stroke: black;
      stroke-width: 3px;
    }
    @media (prefers-color-scheme: dark) {
      circle {
        fill: black;
        stroke: yellow;
      }
    }
  </style>
  <circle cx="50" cy="50" r="47" />
</svg>

Pour ensuite une inclusion classique :

<link rel="icon" href="/favicon.ico"> <!-- (1)! -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  1. Ça n’est pas trop mal supporté mais ça mérite tout de même une valeur par défaut.

Ils en parlent ici et .

Les favicons essentiels⚓︎

🐣 2022-10

Je me servais du RealFaviconGenerator mais il semblerait que ce ne soit plus nécessaire d’après un article de Andrey Sitnik qui réduit cela à 4 lignes :

favicons.html
1
2
3
4
<link rel="icon" href="/favicon.ico" sizes="any"><!-- 32×32 -->
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png"><!-- 180×180 -->
<link rel="manifest" href="/manifest.webmanifest">

Auxquelles il faut ajouter le manifest.webmanifest suivant :

1
2
3
4
5
6
{
  "icons": [
    { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
    { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
  ]
}

Moins de fichiers… moins de maintenance 🙌 !

Faire une redirection en HTML⚓︎

🐣 2022-11

Sans avoir la main sur le serveur, il est possible de faire des redirections depuis une page HTML. Par exemple dans des environnements comme Github Pages ou certains générateurs de sites statiques :

redirect.html
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="refresh" content="0;url=https://example.com"/>
    <link rel="canonical" href="https://example.com"/>
    <title>Redirecting to https://example.com</title>
    <style>
        body {
            font-family: sans-serif;
            max-width: 40em;
            margin: 1em auto;
        }
    </style>
</head>
<body>
    <h1>Redirecting to https://example.com</h1>
    <p>This document has moved!</p>
    <p>Redirecting to <a href="https://example.com">https://example.com</a> in 0 seconds.</p>
</body>
</html>

Un bouton pour fermer une modale⚓︎

🐣 2022-12

Il y a d’autres façons de le faire, toute une liste est disponible sur HTMLHell.

close-button.html
1
2
3
4
<button type="button">
  Close
  <span aria-hidden="true">×</span>
</button>

J’ai choisi celle-ci car elle est explicite et qu’elle évite d’annoncer le signe de multiplication (✕).

Générer des hashs SubResource Integrity (SRI)⚓︎

🐣 2022-12

Il s’agit d’un moyen de s’assurer que le fichier distant (CDN par exemple) téléchargé dans la page est celui escompté. Cela donne une garantie supplémentaire face aux proxys douteux, aux attaques intermédiaires ou aux CDN qui se sont fait hackés. Ce n’est pas magique non plus et pour la personne qui a également accès au HTML envoyé/reçu il est possible de modifier aussi la valeur de l’attribut integrity

Il existe des sites pour ça mais il est possible de le générer à la main dans le terminal grâce à openssl :

 openssl dgst -sha384 -binary FILENAME.js | openssl base64 -A 

Le résultat doit ensuite être préfixé par l’algorithme (sha384- dans cet exemple) :

1
2
3
<script src="https://unpkg.com/react@18/umd/react.production.min.js" 
  integrity="sha384-tMH8h3BGESGckSAVGZ82T9n90ztNXxvdwvdM6UoR56cYcf+0iGXBliJ29D+wZ/x8"
  crossorigin="anonymous"></script>

Warning

L’attribut crossorigin="anonymous" est nécessaire lorsqu’il s’agit d’un fichier sur un autre domaine sinon le navigateur ne fait pas la vérification !

Sémantique des sous-titres⚓︎

🐣 2023-01

Il ne s’agit pas de sous-titrer des vidéos mais de gérer un sous-titre à un titre principal sur une page web.

J’ai souvent utilisé le titre <hx> suivant en fonction de l’arborescence en cours mais Steve Faulkner propose une autre sémantique que je trouve intéressante :

subheadings.html
1
2
3
4
5
<hgroup role="group" aria-roledescription="Heading group">
  <p aria-roledescription="subtitle">Magazine of the Decade</p>
  <h1>THE MONTH</h1>
  <p aria-roledescription="subtitle">The Best of UK and Foreign Media</p>
</hgroup>

En utilisant un <hgroup>, cela permet d’avoir des sous-titres supérieurs ou inférieurs sans altérer une arborescence de titres dans la page. Les balises ARIA viennent décrire explicitement la sémantique des différentes parties et de leurs relations.

Afficher une option par défaut pour un select requis⚓︎

🐣 2023-04

Pour un élément de formulaire de type <select> requis, on veut tout de même afficher un premier choix sans qu’il soit pour autant possible de le soumettre.

Avec la combinaison des attributs disabled et selected on a le premier choix qui est sélectionné par défaut mais qui ne peut pas être soumis et qui n’est pas sélectionnable à nouveau lorsqu’on a choisi l’une des options possibles.

select-required.html
1
2
3
4
5
6
<select required>
  <option disabled selected value>Choisir une option</option>
  <option value="choix-1">Choix 1</option>
  <option value="choix-2">Choix 2</option>
  <option value="choix-3">Choix 3</option>
</select>

Vous pouvez jouer avec ici (nouvel onglet)

Onglets accessibles⚓︎

🐣 2023-11

Info

Il y a beaucoup de façons d’implémenter cela qui sont plus ou moins valides sémantiquement et en matière d’accessibilité. Si la vôtre fonctionne, tant mieux ! Si vous pensez qu’il y a un défaut à celle proposée ci-dessous, n’hésitez pas à proposer des améliorations ou à engager la discussion.

Je suis parti de cet article en ajoutant une couche de sémantique/accessibilité que l’on peut trouver notamment par là. Voir aussi la documentation du W3C à ce sujet.

On commence par le HTML :

tabs.html
<nav role="tablist"><!-- (1)! -->
  <!-- (2)! -->
  <button role="tab"
    id="tab-1"
    aria-selected="true"
    aria-controls="tabpanel-1"
    ><label for="input-1">Tab 1</label></button><!-- (3)! -->
  <button role="tab"
    id="tab-2"
    aria-selected="false"
    aria-controls="tabpanel-2"
    ><label for="input-2">Tab 2</label></button>
</nav>
<div role="tabpanels">
  <section>
    <input hidden="hidden" type="radio" name="tabs"
      id="input-1" checked="checked" />
    <div role="tabpanel"
      id="tabpanel-1"
      aria-labelledby="tab-1">
      <p>Content 1</p>
    </div>
    <input hidden="hidden" type="radio" name="tabs"
      id="input-2" />
    <div role="tabpanel"
      id="tabpanel-2"
      aria-labelledby="tab-2">
      <p>Content 2</p>
    </div>
  </section>
</div>
  1. Les rôles tablist, tab et tabpanel sont un moyen de rendre sémantiquement correcte cette implémentation.
  2. On a un id et aria-controls sur le tab qui répondent à id et aria-labelledby sur le tabpanel.
  3. J’ai souvent besoin de mettre le <label> dans un <button> pour aider à l’intégration dans une CSS existante mais ce n’est pas obligatoire.

Sans CSS ni JS, on a quand même le contenu qui s’affiche par défaut.

Un parti pris de ma part est de laisser tranquille les tabindex car j’ai peur de faire plus de mal qu’autre chose et l’argument de passer de tab en tab en bougeant les flèches du clavier ne me semble pas être très naturel. Ça va aussi dépendre de s’il y a des éléments focusables au sein des tabpanel.

Au lieu du combo <button> + <label>, on pourrait mettre une simple liste avec des liens qui font des ancres vers les sections. D’expérience, ça demande de prendre davantage la main niveau styles, à vous de voir.

On ajoute ensuite une couche de CSS pour que le <label> active l’<input> de type checkbox :

tabs.css
[role="tabpanels"] {
  display: flex;
}

[role="tabpanels"] section {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
}

[role="tabpanels"] [role="tabpanel"] {
  flex-grow: 1;
  width: 100%;
  height: 100%;
  display: none;
  margin-top: 2rem;
}

[role="tabpanels"] [type="radio"]:checked + [role="tabpanel"] {
  display: block; /* (1)! */
}
  1. La « magie » se fait ici, c’est le tabpanel suivant l’input checké qui est affiché.

À cette étape-là, on a déjà quelque chose de fonctionnel, on pourrait ajouter une couleur de fond pour distinguer l’onglet actif (button:is([aria-selected="true"], :hover, :active, :focus) par exemple).

On ajoute du JavaScript pour mettre à jour l’attribut aria-selected sur les boutons :

tabs.js
const tabs = document.querySelectorAll('[role="tablist"] [role="tab"]')
const eventListenerCallback = setActiveState.bind(null, tabs)

tabs.forEach((tab) => {
  tab.addEventListener('click', eventListenerCallback)
  tab.addEventListener('keyup', (event) => {
    // (1)!
    if (event.code === 'Enter' || event.code === 'Space') {
      tab.querySelector('label').click()
    }
  })
})

function setActiveState(tabs, event) {
  // (2)!
  tabs.forEach((tab) => tab.setAttribute('aria-selected', false))
  event.target.closest('[role="tab"]').setAttribute('aria-selected', true)
}
  1. On simule un clic lorsqu’on tabule jusqu’à un onglet avant d’appuyer sur Entrée ou Espace.
  2. La seule chose qui est finalement faite en JS c’est de modifier un attribut.

Notez le soin qui est apporté à essayer de se servir des attributs sémantiques plutôt que d’ajouter des classes ou de dépendre de la structure HTML.

Vous pouvez jouer avec ici (nouvel onglet)

Pour une version utilisant un Web Component.

Forcer un élément parmi une datalist⚓︎

🐣 2024-02

On peut ajouter des suggestions choisies sur un <input> à l’aide de <datalist> mais par défaut il est possible de soumettre un élément qui n’est pas proposé dans la liste initiale.

Pour forcer ce comportement, il faut ajouter un attribut pattern qui liste lui aussi l’intégralité des options disponibles.

datalist-forced.html
1
2
3
4
5
6
7
8
<datalist id="places">
  <option value="Québec">
  <option value="Montréal">
  <option value="Ahuntsic (Montréal)">
</datalist>

<input list="places" type="text" name="city"
  pattern="Québec|Montréal|Ahuntsic \(Montréal\)"> <!-- (1)! -->
  1. Notez qu’il est nécessaire d’échapper tout ce qui ressemble à une expression régulière !

Si vous avez besoin de vérifier le comportement, vous pouvez ajouter ces lignes de CSS :

1
2
3
4
input[list]:user-invalid {
  background-color: lightpink;
  border: 2px solid red;
}