On parle de quoi au juste ? De ça.
Lorem title
Texte fictif : Aliquam consequat adipiscing diam a ipsum placerat enim nec lectus in ornare varius molestie erat maecenas.
Texte fictif : Enim sodales in luctus nulla nulla ut amet duis ullamcorper semper egestas semper dignissim iaculis mauris volutpat erat aenean non a ante metus ipsum pulvinar praesent gravida mollis.
Texte fictif : Consequat metus at nulla poda in dolor aliquam aliquet fermentum curabitur fermentum duis sed aliquam feugiat.
Texte fictif : Egestas adipiscing non tellus fusce maecenas cubilia aenean velit nonummy dolor sapien elit enim egestas euismod aliquam orci eu mi pharetra.
Texte fictif : Adipiscing sollicitudin at justo eu feugiat ultricies molestie luctus pellentesque nisl eros vitae fermentum orci tincidunt ligula mollis felis id non.
Soyons honnêtes : personne ne lit tout sur Internet. PERSONNE.
Et pourtant, le client veut mettre 47 paragraphes sur la page d'accueil parce que "c'est important pour le SEO".
CollapsibleText est là pour sauver l'interface (et notre santé mentale) en proposant un petit bouton "Lire la suite" tout mignon qui cache la misère.
Caractéristiques
Accessibilité
Oui oui, on y a pensé.
Contrairement à 98% des scripts trouvés sur Stack Overflow à 3h du matin, celui-ci respecte VRAIMENT l'accessibilité :
- Conformité WCAG : Tous les petits attributs ARIA sont là (
aria-expanded,aria-controls,aria-live). La checklist d'audit sera superbe ; - Navigation clavier : Tab, Entrée, Espace... tout fonctionne. Les gens qui n'utilisent pas de souris pourront aussi tout lire. Cerise sur le gâteau, le focus est toujours au bon endroit… ;
- Lecteurs d'écran : Ils annoncent les changements d'état. Magie ;
- Respect de
prefers-reduced-motion: Parce que certaines personnes ont le mal de mer avec les animations.
Configuration flexible
Ou "fais comme tu veux, on te juge pas".
Le composant s'adapte au niveau de flemme du jour :
<!-- Mode minimaliste : "J'ai la flemme" -->
<div data-collapse-lines="3">
<div class="collapsible-content">...</div>
</div>
<!-- Mode personnalisé : "Je veux mes textes à moi" -->
<div data-collapse-lines='[2, "Lire plus...", "Masquer"]'>
<div class="collapsible-content">...</div>
</div>
<!-- Mode j'ai-lu-la-doc : "Regarde maman, je maîtrise JSON" -->
<div data-collapse-lines='{"lines": 4, "expandText": "Voir plus", "collapseText": "Voir moins"}'>
<div class="collapsible-content">...</div>
</div>
Internationalisation
Parce que oui, il existe des gens qui ne parlent pas français. Incroyable, non ? Le système supporte l'i18n via window.i18n, et si on a oublié de configurer les traductions (on s'comprend…), il a des fallbacks.
Architecture technique
Vanilla JavaScript pur (oui, SANS jQuery)
Zero. Zéro. 0 dépendance externe. Juste du bon vieux JavaScript natif qui fonctionne partout :
- ResizeObserver : Pour gérer le responsive sans se prendre la tête ;
- WeakMap : Parce qu'on est des développeurs sérieux qui gèrent la mémoire ;
- Classes ES6 : On n'est plus en 2010, les
functionavec desthispartout c'est fini (ou presque…).
Gestion du DOM
Gestion intelligente du DOM (enfin, on espère).
Le composant calcule tout proprement :
- La
line-height(oui oui, même si tu as mis une valeur bizarre enem) ; - Les marges entre paragraphes (celles qu'on oublie toujours dans les calculs) ;
- Le redimensionnement de la fenêtre (parce que les gens adorent resize leur navigateur en pleine lecture…).
Performance
Pas de lag, promis !
- Debouncing des resize (sinon le CPU va pleurer) ;
- Cache des éléments focusables (parcourir le DOM 50 fois par seconde, très peu pour nous) ;
- ResizeObserver au lieu de
window.resize, on n'est pas des sauvages ; - Le DOM n'est modifié que quand c'est vraiment nécessaire (économie d'énergie, tout ça tout ça).
En pratique
Structure HTML
Non, vraiment, c'est simple.
<!-- HTML initial -->
<div class="collapsible-block" data-collapse-lines="2">
<div class="collapsible-content">
<p>Un pavé de texte ici...</p>
<p>Qui continue encore et encore...</p>
<p>Et encore un peu plus loin...</p>
<p>Finalement, a-t-on vraiment dit des choses intéressantes ?</p>
</div>
</div>
Ce qui donne un machin comme ça.
Un pavé de texte ici, limité à une ligne
Qui continue encore et encore...
Et encore un peu plus loin...
Finalement, a-t-on vraiment dit des choses intéressantes ?
Si vous arrivez à vous planter avec ça, on ne peut plus rien pour vous.
Une fois le code interprété, on obtient ceci.
<div class="collapsible-block" data-collapse-lines="1">
<div class="collapsible-content collapsed" id="collapsible-j182za56p" tabindex="-1" style="max-height: 1.913rem; -webkit-line-clamp: 1;">
<p>Un pavé de texte ici, limité à une ligne</p>
<p>Qui continue encore et encore...</p>
<p>Et encore un peu plus loin...</p>
<p>Finalement, a-t-on vraiment dit des choses intéressantes ?</p>
</div>
<span class="sr-only" aria-live="polite" aria-atomic="true">Contenu partiellement affiché</span>
<button class="collapsing-button" type="button" aria-expanded="false" aria-controls="collapsible-j182za56p" aria-label="Afficher la suite du contenu partiellement affiché">Afficher la suite</button>
</div>
Javascript
Taille brute : 17,65 kilo-octets (~4,76 kilo-octets gzip)
/**
* CollapsibleText - Système de troncature de texte accessible
*
* Permet de tronquer du texte sur un nombre de lignes configurable avec un bouton
* "Lire la suite" optionnel. Conforme WCAG, SEO-friendly et performant.
*
* Licence : Creative commons CC BY-NC-SA 4.0
* https://creativecommons.org/licenses/by-nc-sa/4.0/deed.fr
*
*/
(() => {
'use strict';
/**
* Détection du support CSS Nesting (exécuté une seule fois)
*/
const supportsNesting = window.supportsNesting ??= CSS?.supports?.('selector(&)') ?? false;
if (!supportsNesting) {
// Pas de support CSS : on arrête là, les contenus seront tous affichés par défaut
document.documentElement.classList.add('no-nesting-css');
console.warn(`CollapsibleText n'est pas appliqué car le navigateur ne supporte pas les Nesting CSS modernes`);
return;
} else {
document.documentElement.classList.add('use-nesting-css');
}
/**
* Stockage des instances pour gestion du cycle de vie
* @type {WeakMap<Element, CollapsibleText>}
*/
const instances = new WeakMap();
/**
* Récupère un texte internationalisé ou utilise le fallback
*
* @param {string} key - Clé de traduction
* @param {string} fallback - Texte par défaut si traduction absente
* @returns {string} Texte traduit ou fallback
* @private
*/
function getI18nText(key, fallback) {
if (typeof window.i18n === 'object' && window.i18n !== null && key in window.i18n) {
return window.i18n[key];
}
return fallback;
}
/**
* Classe gérant la troncature et l'expansion de contenu textuel
*
* @class CollapsibleText
* @example
* // HTML markup
* <div class="collapsible-block" data-collapse-lines="3">
* <div class="collapsible-content">
* <p>Votre contenu ici...</p>
* </div>
* </div>
*
* @example
* // Déploiement définitif (pas de bouton pour replier)
* <div class="collapsible-block" data-collapse-lines="3" data-collapse-once>
*/
class CollapsibleText {
/**
* Crée une instance de CollapsibleText
*
* @constructor
* @param {HTMLElement} block - L'élément conteneur avec data-collapse-lines
*/
constructor(block) {
this.block = block;
this.content = block.querySelector('.collapsible-content');
this.uniqueId = 'collapsible-' + Math.random().toString(36).substr(2, 9);
this.config = this.parseConfig(block.dataset.collapseLines);
this.maxLines = this.config.lines;
this.expandText = this.config.expandText;
this.collapseText = this.config.collapseText;
this.showButton = this.parseShowButton(block.dataset.collapseButton);
// Déploiement définitif : la présence de l'attribut suffit à activer l'option
this.expandOnce = block.dataset.collapseOnce !== undefined && this.parseShowButton(block.dataset.collapseOnce);
this.button = null;
this.isCollapsed = true;
this.originalHeight = 0;
this.resizeObserver = null;
this.resizeTimeout = null;
this.lastWidth = 0;
this.ariaLiveRegion = null;
this.focusableElements = [];
this.init();
this.setupResizeObserver();
}
/**
* Parse l'attribut data-collapse-button pour déterminer si le bouton doit être affiché
* Également utilisé pour data-collapse-once (attribut booléen)
*
* @param {string|undefined} buttonValue - Valeur de data-collapse-button
* @returns {boolean} True si le bouton doit être affiché
*/
parseShowButton(buttonValue) {
if (buttonValue === undefined || buttonValue === null) return true;
const falseValues = ['false', '0', 'non', 'no'];
return !falseValues.includes(buttonValue.toLowerCase().trim());
}
/**
* Parse la configuration depuis data-collapse-lines
*
* @param {string|undefined} configString - Valeur de data-collapse-lines
* @returns {{lines: number, expandText: string, collapseText: string}} Configuration parsée
*
* @example
* // Format simple (nombre)
* data-collapse-lines="3"
*
* @example
* // Format tableau JSON
* data-collapse-lines='[2, "suite…", "Masquer"]'
*
* @example
* // Format objet JSON
* data-collapse-lines='{"lines": 3, "expandText": "Voir plus", "collapseText": "Voir moins"}'
*/
parseConfig(configString) {
const defaults = {
lines: 3,
expandText: getI18nText('collapsible.expandText', 'Lire la suite'),
collapseText: getI18nText('collapsible.collapseText', 'Réduire')
};
if (!configString) return defaults;
// Format nombre simple
if (/^\d+$/.test(configString.trim())) {
return {
...defaults,
lines: parseInt(configString)
};
}
// Format JSON
try {
const parsed = JSON.parse(configString);
// Format objet
if (typeof parsed === 'object' && !Array.isArray(parsed)) {
return {
lines: parsed.lines || defaults.lines,
expandText: parsed.expandText || defaults.expandText,
collapseText: parsed.collapseText || defaults.collapseText
};
}
// Format tableau
if (Array.isArray(parsed)) {
return {
lines: parsed[0] || defaults.lines,
expandText: parsed[1] || defaults.expandText,
collapseText: parsed[2] || defaults.collapseText
};
}
} catch (e) {
console.warn(`Format data-collapse-lines invalide sur ${this.uniqueId}, utilisation des valeurs par défaut`, e);
}
return defaults;
}
/**
* Initialise le composant
* Calcule les hauteurs, détecte le dépassement et crée le bouton si nécessaire
*
* @private
*/
init() {
// Setup des attributs accessibilité
this.content.id = this.uniqueId;
this.content.setAttribute('tabindex', '-1');
if (this.showButton) {
this.createAriaLiveRegion();
}
this.cacheFocusableElements();
this.originalHeight = this.content.scrollHeight;
// Calcul de la hauteur maximale avec marges
const computedStyle = getComputedStyle(this.content);
const lineHeight = parseFloat(computedStyle.lineHeight);
const firstP = this.content.querySelector('p');
let marginBottom = 0;
if (firstP) {
const pStyle = getComputedStyle(firstP);
marginBottom = parseFloat(pStyle.marginBlockEnd);
}
const maxHeight = (lineHeight * this.maxLines) + (marginBottom * (this.maxLines - 1));
// Créer le bouton et tronquer si nécessaire
if (this.originalHeight > maxHeight + 1) {
if (this.showButton) {
this.createButton();
}
this.collapse();
}
}
/**
* Met en cache tous les éléments focusables du contenu
* Stocke leur tabindex original pour restauration ultérieure
*
* @private
*/
cacheFocusableElements() {
const selector = 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])';
this.focusableElements = Array.from(this.content.querySelectorAll(selector));
this.focusableElements.forEach(el => {
el.dataset.originalTabindex = el.getAttribute('tabindex') || 'none';
});
}
/**
* Désactive tous les éléments focusables (mode collapsed)
* Empêche la navigation clavier dans le contenu masqué
*
* @private
*/
disableFocusableElements() {
this.focusableElements.forEach(el => {
el.setAttribute('tabindex', '-1');
el.setAttribute('aria-hidden', 'true');
});
}
/**
* Réactive tous les éléments focusables (mode expanded)
* Restaure les tabindex originaux
*
* @private
*/
enableFocusableElements() {
this.focusableElements.forEach(el => {
const originalTabindex = el.dataset.originalTabindex;
if (originalTabindex === 'none') {
el.removeAttribute('tabindex');
} else {
el.setAttribute('tabindex', originalTabindex);
}
el.removeAttribute('aria-hidden');
});
}
/**
* Crée la région aria-live pour annoncer les changements d'état
* Utilisée uniquement si le bouton est affiché
*
* @private
*/
createAriaLiveRegion() {
this.ariaLiveRegion = document.createElement('span');
this.ariaLiveRegion.className = 'sr-only';
// live et atomic couvre les combinaisons AT/navigateur qui n'appliquent pas les valeurs implicites
this.ariaLiveRegion.setAttribute('aria-live', 'polite');
this.ariaLiveRegion.setAttribute('aria-atomic', 'true');
this.ariaLiveRegion.setAttribute('role', 'status');
this.block.appendChild(this.ariaLiveRegion);
}
/**
* Annonce un changement d'état via aria-live
*
* @param {string} message - Message à annoncer aux lecteurs d'écran
* @private
*/
announceChange(message) {
if (this.ariaLiveRegion) {
this.ariaLiveRegion.textContent = '';
setTimeout(() => {
this.ariaLiveRegion.textContent = message;
}, 100);
}
}
/**
* Récupère un label descriptif pour les attributs aria
* Cherche un heading ou un élément class="collapsible-title" dans le bloc ou retourne un texte par défaut
*
* @returns {string} Label descriptif du contenu
* @private
*/
getContentLabel() {
const heading = this.block.querySelector('h1, h2, h3, h4, h5, h6, .collapsible-title');
if (heading && heading.textContent.trim()) {
return heading.textContent.trim();
}
return getI18nText('collapsible.contentLabel', 'du contenu partiellement affiché');
}
/**
* Configure le ResizeObserver pour gérer le responsive
* Ne réagit qu'aux changements de largeur (pas de hauteur)
*
* @private
*/
setupResizeObserver() {
this.lastWidth = this.block.offsetWidth;
this.resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const newWidth = entry.contentRect.width;
if (Math.abs(newWidth - this.lastWidth) > 1) {
this.lastWidth = newWidth;
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.reInit();
}, 150);
}
}
});
this.resizeObserver.observe(this.block);
}
/**
* Réinitialise complètement le composant
* Utilisé lors du redimensionnement de la fenêtre
* Sans effet si le contenu a déjà été déployé en mode data-collapse-once
*
* @private
*/
reInit() {
if (this.expandOnce && !this.isCollapsed) return;
this.enableFocusableElements();
if (this.button) {
this.button.remove();
this.button = null;
}
if (this.ariaLiveRegion) {
this.ariaLiveRegion.remove();
this.ariaLiveRegion = null;
}
this.content.style.maxHeight = '';
this.content.style.webkitLineClamp = 'unset';
this.content.classList.remove('collapsed');
this.isCollapsed = true;
this.init();
}
/**
* Crée le bouton de toggle avec tous les attributs d'accessibilité
*
* @private
*/
createButton() {
this.button = document.createElement('button');
this.button.className = 'collapsing-button';
this.button.setAttribute('type', 'button');
this.button.setAttribute('aria-expanded', 'false');
this.button.setAttribute('aria-controls', this.uniqueId);
this.button.setAttribute('aria-label', [this.expandText, this.getContentLabel()].join(' '));
this.button.textContent = this.expandText;
this.button.addEventListener('click', () => this.toggle());
this.block.appendChild(this.button);
}
/**
* Réduit le contenu au nombre de lignes configuré
* Désactive les éléments focusables et met à jour l'accessibilité
*
* @public
*/
collapse() {
const computedStyle = getComputedStyle(this.content);
const lineHeight = parseFloat(computedStyle.lineHeight);
const firstP = this.content.querySelector('p');
let marginBottom = 0;
if (firstP) {
const pStyle = getComputedStyle(firstP);
marginBottom = parseFloat(pStyle.marginBlockEnd);
}
const maxHeight = (lineHeight * this.maxLines) + (marginBottom * (this.maxLines - 1));
this.content.style.maxHeight = ((maxHeight * .0625) + 1).toFixed(3) + 'rem';
this.content.style.webkitLineClamp = this.maxLines;
this.content.classList.add('collapsed');
this.isCollapsed = true;
this.disableFocusableElements();
if (this.button) {
this.button.setAttribute('aria-expanded', 'false');
this.updateButtonText();
this.announceChange(getI18nText('collapsible.contentHidden', 'Contenu partiellement affiché'));
}
}
/**
* Affiche le contenu complet
* Réactive les éléments focusables et déplace le focus
* En mode data-collapse-once, le bouton est supprimé (pas de retour à l'état réduit)
*
* @public
*/
expand() {
this.content.style.maxHeight = (this.originalHeight * .0625 +1).toFixed(3) + 'rem';
this.content.style.webkitLineClamp = 'unset';
this.content.classList.remove('collapsed');
this.isCollapsed = false;
this.enableFocusableElements();
if (this.button) {
if (this.expandOnce) {
this.button.remove();
this.button = null;
} else {
this.button.setAttribute('aria-expanded', 'true');
this.updateButtonText();
}
this.announceChange(getI18nText('collapsible.contentShown', 'Contenu affiché'));
}
this.content.focus();
}
/**
* Bascule entre l'état collapsed et expanded
*
* @public
*/
toggle() {
if (this.isCollapsed) {
this.expand();
} else {
this.collapse();
this.block.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}
/**
* Met à jour le texte et l'aria-label du bouton
*
* @private
*/
updateButtonText() {
const contentLabel = this.getContentLabel();
if (this.isCollapsed) {
this.button.textContent = this.expandText;
this.button.setAttribute('aria-label', [this.expandText, contentLabel].join(' '));
} else {
this.button.textContent = this.collapseText;
this.button.setAttribute('aria-label', [this.collapseText, contentLabel].join(' '));
}
}
/**
* Nettoie l'instance et libère les ressources
* Déconnecte les observers, supprime les listeners et restaure l'état
*
* @public
*/
destroy() {
this.resizeObserver && this.resizeObserver.disconnect();
this.ariaLiveRegion && this.ariaLiveRegion.remove();
this.button && this.button.remove();
this.enableFocusableElements();
clearTimeout(this.resizeTimeout);
// Retirer des instances
instances.delete(this.block);
}
}
/**
* Initialise tous les blocs collapsibles dans le contexte donné
*
* @param {string} [context=document] - Sélecteur CSS du contexte de recherche
* @private
*/
function init(context = document) {
const root = typeof context === 'string' ? document.querySelector(context) : context;
if (!root) return;
const blocks = root.querySelectorAll('[data-collapse-lines]');
blocks.forEach(block => {
// Éviter les doublons
if (!instances.has(block)) {
const instance = new CollapsibleText(block);
instances.set(block, instance);
}
});
}
// Auto-initialisation au chargement du DOM
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', () => init())
: init();
/**
* Initialise manuellement les blocs collapsibles
* Utile pour du contenu chargé en asynchrone
*
* @function initCollapsibleText
* @global
* @param {string} [selector='[data-collapse-lines]'] - Sélecteur CSS des blocs à initialiser
*
* @example
* // Réinitialiser tous les blocs
* window.initCollapsibleText();
*
* @example
* // Initialiser uniquement dans un conteneur
* window.initCollapsibleText('#ajax-container [data-collapse-lines]');
*/
window.initCollapsibleText = function (selector) {
const blocks = document.querySelectorAll(selector || '[data-collapse-lines]');
blocks.forEach(block => {
if (!instances.has(block)) {
const instance = new CollapsibleText(block);
instances.set(block, instance);
}
});
};
/**
* Détruit les instances de CollapsibleText
* Libère les ressources et nettoie les listeners
*
* @function destroyCollapsibleText
* @global
* @param {string} [selector='[data-collapse-lines]'] - Sélecteur CSS des blocs à détruire
*
* @example
* // Détruire tous les blocs avant de recharger du contenu
* window.destroyCollapsibleText('#ajax-container [data-collapse-lines]');
*/
window.destroyCollapsibleText = function (selector) {
const blocks = document.querySelectorAll(selector || '[data-collapse-lines]');
blocks.forEach(block => {
const instance = instances.get(block);
if (instance) {
instance.destroy();
}
});
};
})();
Télécharger kortic-truncate.js
Styles
CSS inclus. Et c'est joli, car simple.
Le fichier CSS c'est le bonus :
- Transitions fluides ;
- Boutons personnalisables (change les couleurs, fais-toi plaisir) ;
- Support responsive… même pas, il n'y en a pas besoin, justement.
Feuille de styles
Taille brute : 1,19 kilo-octets (~568,00 octets gzip)
/*
* Attention : nesting CSS
* Le script associé ne s'appliquera que si le navigateur est compatible.
*/
:root:not(.no-nesting-css) .collapsible-block {
/* Styles du content */
& .collapsible-content {
overflow: hidden;
@media (prefers-reduced-motion: no-preference) {
transition: max-height 0.3s ease;
}
&.collapsed {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
& > * {
margin-block: 0 !important;
display: block;
& + * {
margin-block-start: .5em !important;
}
}
}
/* Styles du bouton */
& .collapsing-button {
margin-top: 0.3125rem;
line-height: 1.5em !important;
font-size: inherit;
color: var(--link-color);
background-color: transparent;
display: flex;
border-radius: 0;
&:after {
color: currentColor;
font-weight: bold;
margin-left: 0.3125rem;
}
&[aria-expanded="false"]:after {
content: "+" / "";
}
&[aria-expanded="true"]:after {
content: "–" / "";
}
@media (any-hover: hover) {
&:hover {
color: var(--link-color-alternate, blue);
}
}
}
}
Télécharger kortic-truncate.css
Initialisation
C'est automatique (magie noire inside).
Le script démarre tout seul comme un grand au chargement de la page. Mais si vous avez besoin de faire le malin avec de l'AJAX :
// Après avoir chargé du contenu dynamique
window.initCollapsibleText('#ajax-container [data-collapse-lines]');
// Avant de tout péter (lire la doc dans le script…)
window.destroyCollapsibleText('#ajax-container [data-collapse-lines]');
Cas d'usage
-
Blogs et articles
Tronque les aperçus d'articles sur la page d'accueil. Les gens qui veulent vraiment lire cliqueront, les autres scrolleront de toute façon. C'est la vie ; -
FAQ et accordéons légers
On n'a pas besoin d'un framework JavaScript de 250 Ko pour gérer une FAQ. Vraiment. En plus, avec des balisesdetails, c'est encore plus simple ; -
Descriptions produits
Affiche un résumé court sur mobile (parce que lire 3 pages sur un écran de 6 pouces, c'est l'enfer). Sur desktop, tout le monde est content ; -
Commentaires utilisateurs
Pour les gens qui écrivent des commentaires de 47 paragraphes. Vous savez, ceux qui racontent leur vie complète avant de dire "produit correct, 4/5".
Les bonus
-
SEO-friendly (Google dira merci)
Le contenu complet reste dans le DOM, juste masqué visuellement. Les robots de Google ne sont pas dupes, ils indexent tout. Le client sera content, l'agence SEO aussi ; -
Bouton optionnel (mode ninja)
Si vous voulez juste un clamp CSS sans le bouton "Lire la suite" (on ne juge pas les choix de vie), utilisez l'attributdata-collapse-button="false". Ça marche aussi avec0au lieu defalse, c'est plus court à taper ; -
Gestion du focus (pour les vrais)
Quand le contenu est replié, tous les liens deviennent inaccessibles au clavier. Pas de piège à focus, pas de confusion. Les utilisateurs de navigation clavier sauront où ils se trouvent dans la page.
Conclusion
CollapsibleText c'est le composant que j'aurais dû coder il y a 6 mois.
Performance, accessibilité, flexibilité : tout y est.
Que ce soit un blog de recettes, une appli web corporate ou un site e-commerce qui vend des chaussettes licornes à tailles déparaillées, ce composant s'intègre sans broncher.
Le code est commenté (oui oui, pour de vrai) et on peut l'étendre sans trop de complications.
On peut l'utiliser les yeux fermés (enfin, pas vraiment, il faut lire un peu la doc et la licence).
Compatibilité navigateurs : Tous ceux qui ne datent pas de l'époque de Windows XP
That's all, Folks !