Rotating Border
Un rectangle en dégradé conique indigo → fuchsia tourne sous une face sombre et n'apparaît que dans un anneau de 1 px — un tour en 3 s, sans survol, en CSS seul.
Vous aurez accès à la démo interactive avec un compte gratuit.
Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Boutons comprend 27 effets, dont 2 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
Trois calques empilés. Le bouton .btn-rotate-border est transparent (background: 0 0), avec position: relative; overflow: hidden; border-radius: 12px hérités de .demo-btn et z-index: 1 qui crée un contexte d'empilement — indispensable pour que les pseudo-éléments à z-index négatif restent dans le bouton au lieu de passer derrière la page.
::before (z-index: -2) est le dégradé : inset: -3px, soit un rectangle débordant de 3 px de chaque côté, rempli par conic-gradient(#6366f1, #d946ef, #6366f1) — indigo à 0°, fuchsia à 180°, retour à l'indigo, sans raccord visible. Il est animé par rotateBorder : transform: rotate(360deg) en 3s linear infinite. ::after (z-index: -1) est la face : inset: 1px; background: #0a0a0f; border-radius: 11px. Entre le bord du bouton (rogné à 12 px par overflow: hidden) et cette face, il reste un anneau de 1 px où le dégradé se voit.
Ce qui tourne n'est pas le dégradé mais le rectangle qui le porte, et c'est là que le code montre sa limite. Le bouton de démonstration mesure 190 × 54 px ; son ::before fait 196 × 60 px et pivote autour de son centre. À 0° et 180° il recouvre tout l'anneau ; dès qu'il tourne, son petit côté (60 px) ne couvre plus la largeur du bouton et l'anneau n'est plus alimenté sur les flancs. Mesuré dans Chromium à 45°, 90° et 135° : les pixels du bord gauche et du coin valent la couleur de page, seuls deux courts arcs restent visibles (haut-gauche et bas-droit), qui glissent le long du bord.
L'effet réel est donc un anneau qui s'allume en entier deux fois par tour (toutes les 1,5 s) et se réduit entre-temps à deux traits mobiles — plus proche d'un « scanner » que d'un dégradé qui tourne. Il n'y a aucune règle :hover ni :active : l'animation tourne dès le chargement, en permanence. La FAQ donne les deux façons de rendre l'anneau continu.
Accessibilité
- prefers-reduced-motion absent du code : une rotation permanente dans le champ de vision. Ajoutez
@media (prefers-reduced-motion: reduce) { .btn-rotate-border::before { animation: none } }— il reste un anneau dégradé fixe, complet (à 0° le rectangle couvre tout), parfaitement lisible. - Contraste du texte : blanc sur la face
#0a0a0f= 19,8:1, niveau AAA. La face est opaque : la couleur de la page derrière ne change rien à la lisibilité. - Contraste de l'anneau (WCAG 1.4.11, composants d'interface, seuil 3:1) :
#6366f1sur#0a0a0f= 4,4:1 et#d946ef= 5,7:1 — le contour est perceptible, mais il ne fait que 1 px et disparaît sur les flancs pendant la plus grande partie du cycle. Ne comptez pas sur lui pour délimiter le bouton : la face sombre sur un fond sombre (#0a0a0fsur#0a0a0f) n'a aucun bord propre. Sur un fond de page identique, ajoutez::after { background: #12121a }ou une bordure fixe de 1 px. - Lecteurs d'écran : les deux pseudo-éléments ont
content: ""— rien n'est annoncé, le nom accessible est le libellé. Le bouton reste un<button>natif, focusable et activable au clavier ; l'animation n'a aucune dépendance au survol, elle est identique pour tous. - Focus : le code ne touche pas à
outline, l'anneau de focus natif reste visible et n'est pas rogné paroverflow: hidden(l'outline est dessiné hors de la boîte). Cible ≈ 54 px de haut (padding 18 px + texte), au-dessus du minimum de 44 px.
Compatibilité navigateur
Requiert la propriété raccourcie inset, conic-gradient() et une animation CSS de transform sur un pseudo-élément. Aucun JavaScript, zéro dépendance.
Sans inset (Safari avant 14.1, Firefox avant 66), les pseudo-éléments n'ont ni taille ni position : le bouton est un simple texte blanc sans fond ni contour. Remplacez inset par top/right/bottom/left explicites pour ces navigateurs. Sans conic-gradient (Firefox avant 83), le ::before n'a pas de fond : anneau invisible, face intacte.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
| animation-duration (CSS — ::before, rotateBorder) | 3s | Durée d'un tour. L'anneau complet réapparaît deux fois par tour : 2s = un flash toutes les secondes ; 6s = balayage lent. |
| conic-gradient(#6366f1, #d946ef, #6366f1) (CSS — ::before) | indigo → fuchsia → indigo | Couleurs de l'anneau. Répétez la première couleur en dernier pour éviter un raccord net à 0°. Ajoutez des arrêts (#6366f1, #22d3ee, #d946ef, #6366f1) pour un anneau multicolore. |
| inset (CSS — ::after) | 1px | Épaisseur de l'anneau visible : c'est l'écart entre le bord du bouton et la face. 2px double l'anneau ; ajustez alors border-radius de la face (12 − inset). |
| background (CSS — ::after) | #0a0a0f | Couleur de la face. Identique au fond du catalogue ; sur une autre page, alignez-la sur votre surface ou éclaircissez-la (#12121a) pour que le bouton ait un bord propre. |
| inset (CSS — ::before) | -3px | Débord du rectangle qui tourne. Sans effet sur l'épaisseur (le bouton rogne à son bord) ; c'est la forme du ::before qui décide de la couverture de l'anneau — voir la FAQ pour un anneau continu. |
| border-radius (CSS — .demo-btn 12px / ::before 14px / ::after 11px) | 12 / 14 / 11 px | Arrondi des trois calques. Gardez face = bouton − inset de la face. 999px partout donne une pilule ; l'anneau y est couvert moins longtemps encore. |
| animation-play-state (CSS — à ajouter) | running | Mettez paused au repos et running sous :hover pour un anneau immobile qui se met à tourner au survol, sans repartir de zéro. |
FAQ
::before a la taille du bouton + 3 px ; une fois pivoté de 90°, son petit côté (≈ 60 px) ne couvre plus les 190 px de largeur, et l'anneau n'a plus rien à montrer sur les flancs. Deux remèdes. Soit un pseudo-élément carré plus grand que la diagonale : .btn-rotate-border::before { inset: auto; left: 50%; top: 50%; width: 200%; aspect-ratio: 1; translate: -50% -50% } — l'anneau est alors alimenté à tout angle. Soit ne plus tourner la boîte mais l'angle du dégradé : @property --a { syntax: '<angle>'; inherits: false; initial-value: 0deg }, conic-gradient(from var(--a), …) et @keyframes { to { --a: 360deg } } — plus propre, mais Firefox 128+ et Safari 16.4+ seulement.animation et ajoutez .btn-rotate-border::before { animation-play-state: paused } puis .btn-rotate-border:hover::before { animation-play-state: running }. L'anneau reste complet au repos (à 0° le rectangle couvre tout) et repart de là où il s'était arrêté. Si vous préférez qu'il reparte toujours de 0°, ne déclarez animation que sous :hover::before.z-index négatifs (−2 et −1). Sans contexte d'empilement sur le bouton, ils se placeraient derrière le fond de la page et deviendraient invisibles. z-index: 1 avec position: relative crée ce contexte : les calques négatifs se peignent au-dessus du fond du bouton mais sous son texte. Si vous retirez z-index, anneau et face disparaissent, seul le texte blanc reste.