Aller au contenu
O-UI

Recherche

Trouvez un composant ou une page de documentation.

GlowBorder

BêtaEffet

Une bordure lumineuse qui glisse autour de l'élément et réagit au pointeur et au focus clavier.

Frameworks
ReactVueVanilla
Technologies
CSS
Déclenché par
PointeurContinu
Mis à jour le
Paramètres

GlowBorder entoure un élément d'une lumière colorée qui glisse lentement le long de son bord. La lumière se rassemble vers le pointeur qui approche, et un court balayage signale le focus clavier. Utilisez-le pour l'action principale d'une page, une carte mise en avant ou un champ important. Dans une liste ou une grande grille, réservez-le à un seul élément.

Installation

Installez l'élément et ses dépendances en une commande :

pnpm dlx shadcn@latest add https://o-ui.hd987.dev/r/glow-border.json

GlowBorder n'a aucune dépendance ; ses adaptateurs utilisent seulement cn de @/lib/utils. Les prérequis communs à tous les composants (Tailwind CSS v4, React 19 ou Vue 3.5) sont décrits dans Installation.

Utilisation

GlowBorder s'utilise de deux façons : il enveloppe un élément (un bouton, un lien, un champ), ou il est lui-même la surface (une carte). Le cas le plus courant est l'enveloppe. Un lien (<a>, Link de Next.js, NuxtLink) s'enveloppe exactement comme ce bouton :

import { GlowBorder } from "@/components/o-ui/glow-border/glow-border";

export function GetStarted() {
  return (
    <GlowBorder className="rounded-full">
      <button
        type="button"
        className="rounded-[inherit] border border-border bg-primary px-6 py-3 text-primary-foreground focus-visible:outline-hidden"
      >
        Get started
      </button>
    </GlowBorder>
  );
}

Quelques règles font la différence :

  1. Posez l'arrondi sur GlowBorder, et rounded-[inherit] sur l'élément enveloppé. La lumière suit l'arrondi de GlowBorder ; l'élément le reprend. Un élément intermédiaire, comme un label autour d'un champ, porte lui aussi rounded-[inherit].
  2. Donnez à l'élément un fond opaque. La lueur passe derrière le contenu : le fond la cache à l'intérieur.
  3. Anneaux de focus : tout dépend du nombre d'éléments focalisables.
    • 3a. Un seul : retirez son anneau avec focus-visible:outline-hidden. GlowBorder signale lui-même le focus, et un anneau en plus ferait une double ligne. outline-hidden garde une outline transparente, qui redevient visible en mode contraste élevé.
    • 3b. Plusieurs, comme dans une carte : chacun garde son anneau. La lumière montre que le focus est dans la carte, l'anneau montre où.
  4. Gardez-lui une vraie bordure. En mode contraste élevé, la lumière est masquée : la bordure garde l'élément dessiné.
    • border border-border : l'élément reste dessiné quand la lumière est éteinte.
    • border border-transparent : seule la lumière se voit ; le contraste élevé rend quand même la bordure visible.
  5. Pour un élément pleine largeur, mettez w-full sur GlowBorder. Sa racine est en inline-flex.

Le contour de secours. Les adaptateurs ajoutent à la racine un contour de focus classique, qui ne s'affiche que lorsque la lumière ne peut pas signaler le focus. Les cas sont listés dans Accessibilité. Gardez-le ; vous pouvez changer son style par className, par exemple has-[:focus-visible]:outline-primary pour sa couleur.

GlowBorder comme surface. Pour une carte, GlowBorder peut être l'élément lui-même : choisissez sa balise avec as, et posez sur lui le fond, la bordure et les marges intérieures. Son fond opaque cache la lueur. S'il contient plusieurs contrôles, ils gardent leur anneau (règle 3b).

<GlowBorder as="article" className="flex flex-col rounded-2xl border border-border bg-card p-6">
  <h3>Pro</h3>…
</GlowBorder>

as n'accepte ni a ni button : la racine porte la lumière, l'élément interactif reste à l'intérieur. Pour une carte entièrement cliquable, étirez donc un lien sur toute la carte. Seul élément focalisable, il retire son anneau (règle 3a). Gardez relative sur la carte : GlowBorder le pose aussi, mais seulement une fois le script chargé. Un autre contrôle placé dans la carte doit passer au-dessus du lien, avec relative z-10.

<GlowBorder as="article" className="relative rounded-2xl border border-border bg-card p-6">
  <h3>
    <a href="/pricing/pro" className="after:absolute after:inset-0 focus-visible:outline-hidden">
      Pro
    </a>
  </h3>
  …
</GlowBorder>

Exemples

Appel à l'action

Le cas le plus simple : un bouton principal enveloppé. Le bouton secondaire, sans GlowBorder, garde son anneau de focus habituel.

Champ de recherche

w-full est posé sur GlowBorder, pour que le champ occupe toute la largeur. Un boost plus élevé (0,8) fait mieux ressortir le champ au focus. Un champ de saisie reçoit le focus visible même au clic : la lumière s'avive donc aussi à la souris.

Offre mise en avant

GlowBorder est ici la carte elle-même, avec as="article". Sa bordure est recouverte par la lumière, mais réapparaît en contraste élevé. Les boutons de la carte gardent leur anneau de focus : la lumière montre que le focus est dans la carte, l'anneau montre où.

Choix d'une option

Chaque option a son GlowBorder, mais seule l'option choisie est allumée : les autres reçoivent disabled, et la lumière passe de l'une à l'autre en fondu. Chaque option a sa propre seed, pour que les lumières ne partent pas du même point. Le bouton radio reste visible : la sélection ne repose pas sur la lumière seule.

Traitement en cours

La lumière indique une activité :

  • disabled suit l'état de l'application : la lumière ne s'allume que pendant le traitement ;
  • le mouvement ne dure que le temps du traitement, lancé par la personne elle-même : il ne demande pas de pause ;
  • un tour plus court (duration={5}) donne une impression d'activité ;
  • le message d'état est annoncé aux lecteurs d'écran (role="status").

GlowBorder est ici la carte elle-même : le champ et le bouton gardent leur anneau de focus (règle 3b). Lumière éteinte, c'est le contour de secours qui entoure la carte au focus, en plus de l'anneau du champ.

Personnalisation

Les réglages se combinent. Voici trois points de départ :

  • Discrète : glow={0.4}, spread={20}, reflection={0} et boost={0.5}, pour un élément secondaire.
  • Néon : borderWidth={2}, intensity={1.3}, glow={1}, spread={72} et duration={8}, pour un seul élément sur un fond sombre.
  • Une seule couleur : colors={["#2dd4bf"]} et randomness={0.2}. Une seule lumière fait le tour du bord.

Une lumière qui s'immobilise. Avec loop à false, la lumière dérive quelques secondes après chaque interaction (apparition, pointeur, focus, changement de prop), puis s'immobilise en douceur. Aucune animation automatique ne dure alors plus de cinq secondes : la page n'a pas besoin de contrôle de pause.

Couleurs du thème. colors accepte toute couleur CSS, variables comprises : colors={["var(--primary)", "var(--chart-2)"]}. La lumière suit alors le thème clair ou sombre. Une variable non définie n'émet aucune lumière.

Fond clair ou sombre. Le liseré et la lueur restent visibles sur un fond clair. Le reflet intérieur s'efface sur une surface blanche : reflection compte surtout sur une surface sombre.

Intégration

Next.js et Nuxt

Le rendu serveur produit la racine et son contenu ; la lumière s'allume à l'hydratation.

  • Next.js : GlowBorder est un composant client, utilisable dans un Server Component. Un fichier qui ajoute un état, comme une sélection, doit lui-même commencer par "use client".
  • Nuxt : aucun <ClientOnly> n'est nécessaire. Importez GlowBorder explicitement, comme dans les exemples : l'import automatique de Nuxt préfixe le nom d'un composant par son dossier, alors qu'un import garde le nom GlowBorder.

Avec le Button de shadcn/ui

Avec le Button de shadcn/ui :

  • Retirez son anneau de focus : GlowBorder s'en charge (règle 3a).
  • Choisissez une variante au fond opaque, default ou secondary (règle 2).
  • Gardez-lui une bordure (règle 4). Vérifiez que votre Button a border border-transparent, présent dans les styles récents de shadcn/ui ; sinon, ajoutez-le.
  • En contraste élevé, où la lumière est masquée, GlowBorder rend au Button l'outline que retire son outline-none.
import { GlowBorder } from "@/components/o-ui/glow-border/glow-border";
import { Button } from "@/components/ui/button";

export function GetStarted() {
  return (
    <GlowBorder className="rounded-lg">
      <Button className="rounded-[inherit] focus-visible:border-transparent focus-visible:ring-0">
        Get started
      </Button>
    </GlowBorder>
  );
}

Selon votre version de shadcn/ui ou de shadcn-vue, adaptez les classes focus-visible:ring-* et focus-visible:border-*.

Mettre en pause toutes les animations

La lumière dérive sans fin. Pour la WCAG 2.2.2, la page doit permettre de mettre ce mouvement en pause. La pause commune de FrameLoop, installé avec GlowBorder, met en pause toutes les bordures de la page, celles créées ensuite comprises, et les autres animations continues d'O-UI : un seul interrupteur suffit. Placez-le dans l'en-tête, le pied de page ou les réglages du site.

"use client";

import { useAnimationsPaused } from "@/components/o-ui/frame-loop/frame-loop";
import { setAnimationsPaused } from "@/components/o-ui/frame-loop/frame-loop-core";

export function MotionToggle() {
  const paused = useAnimationsPaused();
  return (
    <button
      type="button"
      aria-pressed={paused}
      onClick={() => {
        setAnimationsPaused(!paused);
      }}
    >
      Pause animations
    </button>
  );
}

Le libellé reste fixe : avec aria-pressed, un lecteur d'écran annonce « Pause animations, enfoncé ». En pause, la lumière réagit encore au pointeur et au focus (éclat, attraction, court balayage). Pour garder le choix du visiteur d'une page à l'autre, suivez la recette de FrameLoop. Pour une seule bordure, utilisez la prop paused.

Un bouton désactivé

Reliez le disabled de GlowBorder à celui du bouton : la lumière s'éteint en 0,3 s, sans démonter le composant.

<GlowBorder disabled={!canSend} className="rounded-lg">
  <button
    type="submit"
    disabled={!canSend}
    className="rounded-[inherit] border border-border bg-primary px-4 py-2 text-primary-foreground focus-visible:outline-hidden"
  >
    Send
  </button>
</GlowBorder>

Plusieurs bordures sur une page

Toutes les bordures partagent une seule boucle d'animation, celle de la page. Dans une liste, n'allumez que l'élément utile et éteignez les autres avec disabled, comme dans « Choix d'une option ». Deux bordures de même seed, de même taille et apparues ensemble bougent à l'unisson : dans une liste, donnez donc à chacune sa seed, par exemple d'après sa position. Pour des bordures éloignées ou de tailles différentes, c'est inutile.

{
  options.map((option, index) => (
    <GlowBorder
      key={option.id}
      disabled={option.id !== selected}
      seed={index + 1}
      className="rounded-xl"
    >
      …
    </GlowBorder>
  ));
}

Accéder à l'élément

La ref donne la racine de GlowBorder, pas l'élément enveloppé.

const glow = useRef<HTMLElement>(null);
// glow.current is the root element.

<GlowBorder ref={glow} className="rounded-full">
  …
</GlowBorder>;

GlowBorder n'a pas d'API impérative : pilotez-le par ses props.

Utiliser sans enveloppe

Pour allumer la bordure d'un élément de votre propre composant, sans l'élément racine que pose GlowBorder, utilisez le hook useGlowBorder en React, ou le composable du même nom en Vue. Les options sont les mêmes.

"use client";

import { type ReactNode, useRef } from "react";
import { useGlowBorder } from "@/components/o-ui/glow-border/glow-border";

export function FeaturedCard({ children }: { children: ReactNode }) {
  const cardRef = useRef<HTMLElement>(null);
  useGlowBorder(cardRef, { intensity: 1.2 });
  return (
    <article ref={cardRef} className="rounded-2xl border bg-card p-6">
      {children}
    </article>
  );
}
  • La lumière est dessinée dans un élément ajouté en dernier enfant. Si votre framework réécrit le contenu de l'élément (un texte remplacé d'un bloc), il revient à la frame suivante.
  • GlowBorder pose isolation: isolate et, si besoin, position: relative sur l'élément ; ses styles d'origine reviennent au démontage.
  • Le contour de secours, visible avant le chargement du script et quand la lumière ne peut pas signaler le focus, vient des classes de la racine du composant : pour l'avoir, reprenez-les sur votre élément.
  • Le hook et le composable ne renvoient rien. Ils acceptent une ref ou un élément (en Vue : un élément, une ref de template ou un getter, et des options en objet, ref ou getter). Une option retirée revient à sa valeur par défaut.

API

PropTypeDéfautDescription
colorsreadonly string[]["#4d6bff", "#a54dff", "#ff9b4a"]De 1 à 6 couleurs CSS, variables comprises. Chaque couleur est une source de lumière, placée dans l'ordre autour du bord. Une seule couleur donne une seule lumière, qui fait le tour.
borderWidthnumber1.5Épaisseur du liseré lumineux, en pixels CSS, de 0,5 à 8. Augmentez-la pour un effet néon.
intensitynumber1Luminosité des lumières, de 0 à 2. Au-delà de 1, leur centre blanchit. Entre 0,75 et 1,5, avec un `boost` d'au moins 0,5, l'éclat suffit à signaler le focus (voir Accessibilité).
glownumber0.8Force de la lueur autour du bord, de 0 à 1. À 0, il ne reste que le liseré.
spreadnumber36Portée de la lueur au-delà du bord, en pixels CSS, de 0 à 160. Augmentez-la sur un grand élément ou un fond sombre.
reflectionnumber0.3Lumière renvoyée vers l'intérieur de l'élément, de 0 à 1. Visible sur une surface sombre, elle s'efface sur une surface claire.
durationnumber14Durée d'un tour complet des lumières, en secondes, de 2 à 120. Plus court, l'effet paraît plus actif. Un changement ne fait pas sauter la lumière.
randomnessnumber0.5Variations de position et d'intensité, de 0 à 1. À 0, la rotation est parfaitement régulière.
seednumber1Graine des variations, entier positif ou nul (une fraction est arrondie vers le bas). Une même graine donne toujours les mêmes variations : dans une liste, donnez-en une différente à chaque bordure.
follownumber0.6Attraction des lumières vers le pointeur, de 0 à 1. À 0, elles ne se déplacent plus vers lui, mais s'avivent encore à son approche.
proximitynumber120Distance d'action du pointeur au-delà du bord, en pixels CSS, de 0 à 400. À 0, le pointeur n'agit qu'au-dessus de l'élément.
boostnumber0.6Éclat supplémentaire près du pointeur et au focus clavier, de 0 à 1. À partir de 0,5, avec une `intensity` entre 0,75 et 1,5, l'éclat suffit à signaler le focus (voir Accessibilité).
focusSweepnumber0.3Distance que parcourt la lumière à l'arrivée du focus clavier, en fraction de tour, de 0 à 1 : elle accélère, puis reprend sa dérive. À 0, pas de balayage.
pausedbooleanfalseBooléen : fige la dérive de cette bordure. La lumière réagit encore au pointeur et au focus (éclat, attraction, court balayage). Pour toute la page, voir `setAnimationsPaused` de FrameLoop.
loopbooleantrueBooléen, vrai par défaut : la lumière dérive sans fin. À `false`, elle s'immobilise moins de cinq secondes après chaque interaction (apparition, pointeur, focus, changement de prop), et la page n'a pas besoin de contrôle de pause.
disabledbooleanfalseBooléen : éteint la lumière en 0,3 s, sans démonter le composant ni désactiver l'élément enveloppé. La réactivation rallume la lumière en douceur.
reducedMotionReducedMotion"user"`"user"` suit le réglage du système, `"always"` garde les lumières fixes, `"never"` conserve le mouvement : par exemple quand la personne a réactivé les animations dans les réglages de votre site.

Les nombres hors plage sont ramenés dans la plage. Une valeur invalide garde la valeur courante, ou la valeur par défaut à la création. Les couleurs invalides sont ignorées, comme celles au-delà de la sixième.

Les adaptateurs ajoutent as, l'élément racine : div (par défaut), span, li, article, section ou aside. Ils transmettent les attributs natifs, ainsi que className en React et class en Vue. Une classe passée remplace celle de la racine en cas de conflit : flex remplace inline-flex.

Pour toute la page, la pause commune setAnimationsPaused(paused) de FrameLoop met en pause toutes les bordures (voir Mettre en pause). Le hook et le composable useGlowBorder prennent les mêmes options (voir « Utiliser sans enveloppe »).

Accessibilité

Ce que fait GlowBorder

  • La lumière est décorative : les lecteurs d'écran l'ignorent, et elle ne capte pas le pointeur. Le rôle, le nom et le focus restent ceux de l'élément enveloppé. GlowBorder n'ajoute ni rôle ni tabindex.
  • La dérive s'arrête hors écran et dans un onglet masqué ; elle reprend sans saut.
  • GlowBorder signale le focus clavier : à l'arrivée du focus, la lumière fait un court balayage du bord, puis reste plus vive tant que le focus est là. Un focus qui passe d'un élément à l'autre dans GlowBorder ne relance pas le balayage.
  • Quand la lumière ne peut pas signaler le focus, le contour de secours entoure GlowBorder. C'est le cas :
    • avant le chargement du script ;
    • avec disabled ;
    • si les réglages rendent l'éclat invisible. Avec intensity entre 0,75 et 1,5 et boost d'au moins 0,5, la lumière signale toujours le focus.
  • Le pointeur n'agit qu'avec une souris ou un pavé tactile ; le toucher l'écarte (sur un appareil hybride, un toucher relâche l'attraction). Sur mobile, la dérive et l'éclat au focus restent.
  • Aucun clignotement : les changements de luminosité sont lissés, et la lumière « respire » lentement (moins de 0,3 Hz).

Ce que vous devez faire

  • Un seul élément focalisable dans GlowBorder : retirer son anneau, avec focus-visible:outline-hidden. Plusieurs : les garder (règle 3).
  • Lui garder une vraie bordure CSS, pour le mode contraste élevé.
  • Permettre de mettre la dérive en pause (WCAG 2.2.2) : un seul interrupteur pour toute la page, avec setAnimationsPaused de FrameLoop (voir Mettre en pause).
    • Le mouvement réduit du système ne remplace pas cet interrupteur : ce réglage ne concerne que les personnes qui l'ont activé.
    • Avec loop à false, aucun contrôle n'est nécessaire : la lumière s'immobilise en moins de cinq secondes.
    • Un mouvement lancé par la personne et qui s'arrête avec son action, comme un indicateur de traitement, n'en demande pas non plus.
  • Choisir des couleurs qui contrastent avec le fond de la page : l'éclat signale le focus, encore faut-il qu'il se voie.
  • Ne pas confier un état à la lumière seule : une sélection, par exemple, doit rester visible sans elle.

Mouvement réduit. Les lumières restent fixes : ni dérive, ni attraction, ni balayage. La prop reducedMotion permet de forcer ce comportement ou de l'ignorer. Le survol et le focus gardent un court fondu de luminosité (0,2 s). Un changement du réglage système s'applique en direct.

Contraste élevé. En mode forced-colors, la lumière est masquée et le système dessine les éléments. La bordure de l'élément enveloppé reste dessinée. Au focus, un seul indicateur s'affiche : l'outline de l'élément focalisé, que GlowBorder garantit même si l'élément la supprime (outline-none). Le contour de secours s'efface. Dans une carte, chaque contrôle montre ainsi la sienne.

Performances

  • Le cœur et ses fichiers annexes pèsent environ 6 kB minifiés et compressés (13,9 kB minifiés), sans dépendance.
  • L'animation ne change que la position et l'opacité des lumières : le navigateur la confie au GPU, sans recalcul de mise en page ni repeinture.
  • Toutes les bordures d'une page partagent une seule boucle d'animation et un seul jeu d'écouteurs de pointeur, ceux de FrameLoop et de PointerTracker, avec les autres éléments d'O-UI. Une bordure en pause, immobilisée (loop à false), hors écran ou dans un onglet masqué ne fait aucun travail, et la boucle s'arrête quand plus rien ne bouge.
  • Chaque bordure en mouvement anime plusieurs calques par couleur. Dans une grande grille, réservez l'effet aux éléments mis en avant.

Problèmes fréquents

  • La lumière ne bouge pas. La page a appelé setAnimationsPaused(true), la bordure a paused ou loop à false, ou le système demande de réduire les animations.
  • La lueur est coupée. GlowBorder ou un ancêtre a overflow: hidden. Placez cette règle sur un enfant, ou laissez de la marge autour de GlowBorder.
  • La lueur apparaît à l'intérieur de l'élément. L'élément enveloppé est transparent. Donnez-lui un fond opaque.
  • Deux indicateurs de focus apparaissent autour d'un élément enveloppé. Il garde son anneau. Ajoutez-lui focus-visible:outline-hidden et retirez ses classes ring. Dans une carte qui contient plusieurs contrôles, l'anneau du contrôle et la lumière (ou le contour de secours) sont voulus : voir la règle 3b.
  • Le contour de secours s'affiche au focus, au lieu de la lumière. intensity ou boost sortent de la plage où l'éclat se voit. Gardez intensity entre 0,75 et 1,5 et boost d'au moins 0,5.
  • GlowBorder ne prend pas toute la largeur. Sa racine est en inline-flex. Ajoutez w-full à GlowBorder, pas seulement à l'élément enveloppé.
  • Plusieurs bordures bougent à l'unisson. Elles ont la même seed. Donnez-en une à chacune.
  • Un menu déroulant placé dans GlowBorder passe sous les éléments voisins. GlowBorder crée un contexte d'empilement (isolation: isolate). Affichez le menu dans un portail, comme le font la plupart des bibliothèques.
  • Le dernier enfant d'une carte reçoit une marge ou une bordure en trop. La lumière est dessinée dans un élément ajouté en dernier enfant de GlowBorder : space-y-*, divide-y et les sélecteurs :last-child ou last: ne visent plus le bon élément. Préférez flex flex-col gap-*.

Notes et limites

  • Le rendu utilise mask-composite, mix-blend-mode: plus-lighter et color-mix(). Il demande Chrome 120, Safari 16.2 ou Firefox 113, ou une version plus récente.
  • GlowBorder pose isolation: isolate sur sa racine, et position: relative si elle est statique : un enfant en position: absolute se place donc par rapport à elle. Les styles d'origine reviennent à la destruction.
  • Une bordure CSS sur la racine est recouverte par la lumière. Elle redevient visible en contraste élevé.
  • Un fond translucide sur la racine est peint deux fois à l'intérieur, ce qui le densifie : préférez un fond opaque.
  • Le trajet des lumières suit l'arrondi du coin supérieur gauche. Avec des arrondis différents par coin, le liseré suit la forme CSS, mais le trajet l'approxime. Un arrondi elliptique est approché par un arrondi circulaire.
  • Une racine display: inline est mesurée par son cadre englobant : un retour à la ligne la déforme. Les adaptateurs utilisent inline-flex ; en vanilla, préférez inline-flex ou inline-block.
  • Un élément sans taille ou masqué ne dessine rien. La lumière apparaît dès qu'il a une taille.
  • Quand le nombre de couleurs change, les lumières se répartissent de nouveau régulièrement, en glissant vers leur place en une demi-seconde environ. Les nouvelles apparaissent en fondu.