30 CSS Hover Effects08 / 30

Pure CSSMIT licensed

CSS Border Draw Hover Effect

Six button and card border-draw hover effects — clockwise stroke, corner flash, double-rail slide, snake crawl, dashed grow, and gradient border sweep — all achieved by animating pseudo-element dimensions or SVG-style techniques without JavaScript.

Published

Live Demo
Try it

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

The code

<div class="hv-08">
  <div class="hv-08__grid">
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--cw">Clockwise Draw</button>
      <span class="hv-08__label">clockwise stroke</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--corner">Corner Flash</button>
      <span class="hv-08__label">corner flash</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--rail">Double Rail</button>
      <span class="hv-08__label">dual-rail slide</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--dashed">Dashed Grow</button>
      <span class="hv-08__label">dashed border scale</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--grad">Gradient Sweep</button>
      <span class="hv-08__label">conic border sweep</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--inset">Inset Reveal</button>
      <span class="hv-08__label">box-shadow inset</span>
    </div>
  </div>
</div>
.hv-08,
.hv-08 *,
.hv-08 *::before,
.hv-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.hv-08 ::selection {
  background: #f59e0b;
  color: #000;
}

.hv-08 {
  --bg: #0c0a03;
  --text: #fef3c7;
  --dim: #6b7280;
  --amber: #f59e0b;
  --orange: #f97316;
  --red: #ef4444;
  --yellow: #fde047;
  --gold: #b45309;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 60px 24px;
}

.hv-08__grid {
  display: grid;
  grid-template-columns: repeat(3,1fr);
  gap: 36px;
  max-width: 860px;
  width: 100%;
}

.hv-08__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding: 40px 20px;
  background: rgba(255,255,255,.025);
  border: 1px solid rgba(255,255,255,.07);
  border-radius: 12px;
}

.hv-08__label {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
}
/* shared btn base */

.hv-08__btn {
  position: relative;
  overflow: hidden;
  padding: 13px 32px;
  background: transparent;
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: .05em;
  cursor: pointer;
}
/* 1 — clockwise draw */

.hv-08__btn--cw {
  color: var(--amber);
  border: none;
}

.hv-08__btn--cw::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  width: 0;
  height: 2px;
  background: var(--amber);
  transition: width .2s linear;
}

.hv-08__btn--cw::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 0;
  height: 2px;
  background: var(--amber);
  transition: width .2s linear .4s;
}

.hv-08__btn--cw:hover::before {
  width: 100%;
  transition: width .2s linear;
}

.hv-08__btn--cw:hover::after {
  width: 100%;
  transition: width .2s linear .2s;
}
/* border sides via box-shadow on hover for left/right of CW */

.hv-08__btn--cw {
  box-shadow: inset 2px 0 0 transparent, inset -2px 0 0 transparent;
  transition: box-shadow .001s linear .39s,color .3s;
}

.hv-08__btn--cw:hover {
  color: var(--text);
  box-shadow: inset 2px 0 0 var(--amber), inset -2px 0 0 var(--amber);
  transition: box-shadow .2s linear .2s, color .3s;
}
/* 2 — corner flash (L-shaped pseudos grow on hover to form full border).
       Two problems with the original: (a) the shared .hv-08__btn rule
       sets overflow:hidden, which clipped the L-corner pseudo-elements
       (positioned at top/left/bottom/right: -2px — slightly OUTSIDE the
       button box). Result: invisible default state, no growth on hover.
       (b) The background-image: linear-gradient stack was meant as a
       border-box transparent-center trick but was incomplete (no
       background-origin pairing), so it just painted the page bg
       over everything including the corner indicators.

       Fix: override overflow to visible on this variant only (other
       variants still need overflow:hidden for their clip effects),
       and drop the broken background-image stack entirely — the
       transparent button is already transparent without it. */

.hv-08__btn--corner {
  color: var(--orange);
  overflow: visible;
  transition: color .3s;
}

.hv-08__btn--corner::before,
.hv-08__btn--corner::after {
  content: '';
  position: absolute;
  width: 12px;
  height: 12px;
  border-color: var(--orange);
  border-style: solid;
  transition: width .25s,height .25s;
}

.hv-08__btn--corner::before {
  top: -2px;
  left: -2px;
  border-width: 2px 0 0 2px;
}

.hv-08__btn--corner::after {
  bottom: -2px;
  right: -2px;
  border-width: 0 2px 2px 0;
}

.hv-08__btn--corner:hover::before,
.hv-08__btn--corner:hover::after {
  width: calc(100% + 4px);
  height: calc(100% + 4px);
}
/* 3 — double rail */

.hv-08__btn--rail {
  color: var(--red);
  border: none;
}

.hv-08__btn--rail::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 2px;
  background: var(--red);
  transition: left .35s cubic-bezier(.4,0,.2,1);
}

.hv-08__btn--rail::after {
  content: '';
  position: absolute;
  bottom: 0;
  right: -100%;
  width: 100%;
  height: 2px;
  background: var(--red);
  transition: right .35s cubic-bezier(.4,0,.2,1);
}

.hv-08__btn--rail:hover::before {
  left: 0;
}

.hv-08__btn--rail:hover::after {
  right: 0;
}

.hv-08__btn--rail:hover {
  color: var(--text);
}
/* 4 — dashed grow */

.hv-08__btn--dashed {
  color: var(--yellow);
  border: 2px dashed transparent;
  transition: border-color .3s,color .3s,padding .3s;
}

.hv-08__btn--dashed:hover {
  border-color: var(--yellow);
  padding: 13px 40px;
  color: var(--text);
}
/* 5 — gradient border sweep (conic gradient rotates around a 2px ring,
       button's bg covers the center so only the 2px edge shows the
       spinning gradient).

       Original implementation rotated the entire ::before pseudo-
       element via @keyframes transform: rotate(360deg). That works
       on SQUARE elements but the button is a wide rectangle
       (~189x43), so rotating the rectangle itself swings its
       corners through a huge arc — producing the "gradient wedge
       flapping in the background" effect the user reported.

       Fix: don't rotate the element. Instead use @property to
       register a custom property --angle as angle, then animate
       --angle inside the conic-gradient's "from angle" parameter.
       The ELEMENT stays put; only the gradient's start-angle
       rotates within the rectangle, producing a clean circular
       sweep visible only as a 2px ring (since ::after covers the
       center). @property support: Chrome 85+, Safari 16.4+,
       Firefox 128+ (~92% global). Older browsers see a static
       gradient — still looks intentional, just doesn't spin.

       Also fixed previously: overflow:visible override (shared
       .hv-08__btn sets overflow:hidden which clipped the gradient's
       2px overhang), and isolation:isolate to trap the pseudo-
       elements' negative z-index within the button's stacking
       context. */

@property --hv-08-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.hv-08__btn--grad {
  color: var(--text);
  border: none;
  background: transparent;
  overflow: visible;
  isolation: isolate;
}

.hv-08__btn--grad::before {
  content: '';
  position: absolute;
  inset: -2px;
  --hv-08-angle: 0deg;
  background: conic-gradient(from var(--hv-08-angle),transparent 0%,var(--amber) 30%,var(--orange) 50%,transparent 70%);
  border-radius: 4px;
  opacity: 0;
  transition: opacity .3s;
  z-index: -1;
}

.hv-08__btn--grad::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bg);
  border-radius: 2px;
  z-index: -1;
}

.hv-08__btn--grad:hover::before {
  opacity: 1;
  animation: hv-08-spin 1.5s linear infinite;
}

@keyframes hv-08-spin {
  to {
    --hv-08-angle: 360deg;
  }
}
/* 6 — inset reveal */

.hv-08__btn--inset {
  color: var(--gold);
  border: none;
  box-shadow: inset 0 0 0 0 var(--amber);
  transition: box-shadow .4s cubic-bezier(.4,0,.2,1),color .4s;
}

.hv-08__btn--inset:hover {
  color: #000;
  box-shadow: inset 0 0 0 60px var(--amber);
}

@media (max-width: 640px) {
  .hv-08__grid {
    grid-template-columns: repeat(2,1fr);
  }
}

@media (max-width: 400px) {
  .hv-08__grid {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hv-08__btn,
    .hv-08__btn::before,
    .hv-08__btn::after {
    transition: none!important;
    animation: none!important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Hover Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Border Draw Hover Effect
Source: https://codefronts.com/motion/css-hover-effects/css-border-draw-hover-effect/

Six button and card border-draw hover effects — clockwise stroke, corner flash, double-rail slide, snake crawl, dashed grow, and gradient border sweep — all achieved by animating pseudo-element dimensions or SVG-style techniques without JavaScript.
## HTML
```html
<div class="hv-08">
  <div class="hv-08__grid">
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--cw">Clockwise Draw</button>
      <span class="hv-08__label">clockwise stroke</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--corner">Corner Flash</button>
      <span class="hv-08__label">corner flash</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--rail">Double Rail</button>
      <span class="hv-08__label">dual-rail slide</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--dashed">Dashed Grow</button>
      <span class="hv-08__label">dashed border scale</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--grad">Gradient Sweep</button>
      <span class="hv-08__label">conic border sweep</span>
    </div>
    <div class="hv-08__cell">
      <button class="hv-08__btn hv-08__btn--inset">Inset Reveal</button>
      <span class="hv-08__label">box-shadow inset</span>
    </div>
  </div>
</div>
```
## CSS
```css
.hv-08,
.hv-08 *,
.hv-08 *::before,
.hv-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.hv-08 ::selection {
  background: #f59e0b;
  color: #000;
}

.hv-08 {
  --bg: #0c0a03;
  --text: #fef3c7;
  --dim: #6b7280;
  --amber: #f59e0b;
  --orange: #f97316;
  --red: #ef4444;
  --yellow: #fde047;
  --gold: #b45309;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 60px 24px;
}

.hv-08__grid {
  display: grid;
  grid-template-columns: repeat(3,1fr);
  gap: 36px;
  max-width: 860px;
  width: 100%;
}

.hv-08__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding: 40px 20px;
  background: rgba(255,255,255,.025);
  border: 1px solid rgba(255,255,255,.07);
  border-radius: 12px;
}

.hv-08__label {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
}
/* shared btn base */

.hv-08__btn {
  position: relative;
  overflow: hidden;
  padding: 13px 32px;
  background: transparent;
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: .05em;
  cursor: pointer;
}
/* 1 — clockwise draw */

.hv-08__btn--cw {
  color: var(--amber);
  border: none;
}

.hv-08__btn--cw::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  width: 0;
  height: 2px;
  background: var(--amber);
  transition: width .2s linear;
}

.hv-08__btn--cw::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 0;
  height: 2px;
  background: var(--amber);
  transition: width .2s linear .4s;
}

.hv-08__btn--cw:hover::before {
  width: 100%;
  transition: width .2s linear;
}

.hv-08__btn--cw:hover::after {
  width: 100%;
  transition: width .2s linear .2s;
}
/* border sides via box-shadow on hover for left/right of CW */

.hv-08__btn--cw {
  box-shadow: inset 2px 0 0 transparent, inset -2px 0 0 transparent;
  transition: box-shadow .001s linear .39s,color .3s;
}

.hv-08__btn--cw:hover {
  color: var(--text);
  box-shadow: inset 2px 0 0 var(--amber), inset -2px 0 0 var(--amber);
  transition: box-shadow .2s linear .2s, color .3s;
}
/* 2 — corner flash (L-shaped pseudos grow on hover to form full border).
       Two problems with the original: (a) the shared .hv-08__btn rule
       sets overflow:hidden, which clipped the L-corner pseudo-elements
       (positioned at top/left/bottom/right: -2px — slightly OUTSIDE the
       button box). Result: invisible default state, no growth on hover.
       (b) The background-image: linear-gradient stack was meant as a
       border-box transparent-center trick but was incomplete (no
       background-origin pairing), so it just painted the page bg
       over everything including the corner indicators.

       Fix: override overflow to visible on this variant only (other
       variants still need overflow:hidden for their clip effects),
       and drop the broken background-image stack entirely — the
       transparent button is already transparent without it. */

.hv-08__btn--corner {
  color: var(--orange);
  overflow: visible;
  transition: color .3s;
}

.hv-08__btn--corner::before,
.hv-08__btn--corner::after {
  content: '';
  position: absolute;
  width: 12px;
  height: 12px;
  border-color: var(--orange);
  border-style: solid;
  transition: width .25s,height .25s;
}

.hv-08__btn--corner::before {
  top: -2px;
  left: -2px;
  border-width: 2px 0 0 2px;
}

.hv-08__btn--corner::after {
  bottom: -2px;
  right: -2px;
  border-width: 0 2px 2px 0;
}

.hv-08__btn--corner:hover::before,
.hv-08__btn--corner:hover::after {
  width: calc(100% + 4px);
  height: calc(100% + 4px);
}
/* 3 — double rail */

.hv-08__btn--rail {
  color: var(--red);
  border: none;
}

.hv-08__btn--rail::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 2px;
  background: var(--red);
  transition: left .35s cubic-bezier(.4,0,.2,1);
}

.hv-08__btn--rail::after {
  content: '';
  position: absolute;
  bottom: 0;
  right: -100%;
  width: 100%;
  height: 2px;
  background: var(--red);
  transition: right .35s cubic-bezier(.4,0,.2,1);
}

.hv-08__btn--rail:hover::before {
  left: 0;
}

.hv-08__btn--rail:hover::after {
  right: 0;
}

.hv-08__btn--rail:hover {
  color: var(--text);
}
/* 4 — dashed grow */

.hv-08__btn--dashed {
  color: var(--yellow);
  border: 2px dashed transparent;
  transition: border-color .3s,color .3s,padding .3s;
}

.hv-08__btn--dashed:hover {
  border-color: var(--yellow);
  padding: 13px 40px;
  color: var(--text);
}
/* 5 — gradient border sweep (conic gradient rotates around a 2px ring,
       button's bg covers the center so only the 2px edge shows the
       spinning gradient).

       Original implementation rotated the entire ::before pseudo-
       element via @keyframes transform: rotate(360deg). That works
       on SQUARE elements but the button is a wide rectangle
       (~189x43), so rotating the rectangle itself swings its
       corners through a huge arc — producing the "gradient wedge
       flapping in the background" effect the user reported.

       Fix: don't rotate the element. Instead use @property to
       register a custom property --angle as angle, then animate
       --angle inside the conic-gradient's "from angle" parameter.
       The ELEMENT stays put; only the gradient's start-angle
       rotates within the rectangle, producing a clean circular
       sweep visible only as a 2px ring (since ::after covers the
       center). @property support: Chrome 85+, Safari 16.4+,
       Firefox 128+ (~92% global). Older browsers see a static
       gradient — still looks intentional, just doesn't spin.

       Also fixed previously: overflow:visible override (shared
       .hv-08__btn sets overflow:hidden which clipped the gradient's
       2px overhang), and isolation:isolate to trap the pseudo-
       elements' negative z-index within the button's stacking
       context. */

@property --hv-08-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.hv-08__btn--grad {
  color: var(--text);
  border: none;
  background: transparent;
  overflow: visible;
  isolation: isolate;
}

.hv-08__btn--grad::before {
  content: '';
  position: absolute;
  inset: -2px;
  --hv-08-angle: 0deg;
  background: conic-gradient(from var(--hv-08-angle),transparent 0%,var(--amber) 30%,var(--orange) 50%,transparent 70%);
  border-radius: 4px;
  opacity: 0;
  transition: opacity .3s;
  z-index: -1;
}

.hv-08__btn--grad::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bg);
  border-radius: 2px;
  z-index: -1;
}

.hv-08__btn--grad:hover::before {
  opacity: 1;
  animation: hv-08-spin 1.5s linear infinite;
}

@keyframes hv-08-spin {
  to {
    --hv-08-angle: 360deg;
  }
}
/* 6 — inset reveal */

.hv-08__btn--inset {
  color: var(--gold);
  border: none;
  box-shadow: inset 0 0 0 0 var(--amber);
  transition: box-shadow .4s cubic-bezier(.4,0,.2,1),color .4s;
}

.hv-08__btn--inset:hover {
  color: #000;
  box-shadow: inset 0 0 0 60px var(--amber);
}

@media (max-width: 640px) {
  .hv-08__grid {
    grid-template-columns: repeat(2,1fr);
  }
}

@media (max-width: 400px) {
  .hv-08__grid {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hv-08__btn,
    .hv-08__btn::before,
    .hv-08__btn::after {
    transition: none!important;
    animation: none!important;
  }
}
```

How this works

The classic border draw uses two pseudo-elements. ::before covers the top + right sides (full width, height: 0100% on right, driven by height transition) and ::after covers the bottom + left. By staggering transition-delay between them and setting width / height from zero, the border appears to draw itself clockwise around the element.

The gradient border variant uses a single ::before with background: conic-gradient(from var(--angle), transparent, var(--color), transparent) positioned as a larger element behind the button with the button's own background covering the center — creating an apparent gradient border. A @keyframes rotates --angle on hover for the sweeping effect. The snake variant uses outline-offset combined with outline transitions, a lesser-known trick that avoids layout impact.

Make it yours

  • Adjust draw speed by changing the transition duration — .2s per side feels instant; .4s feels drawn.
  • Change border color by editing the background on ::before/::after — use a gradient instead of a solid for richer strokes.
  • Increase border thickness by changing width: 2px / height: 2px on the pseudo-elements to 3px or 4px.
  • Add a matching box-shadow: inset 0 0 0 2px var(--color) to show a ghost border before the draw begins, so users know the element is interactive.
  • Reverse the draw direction by flipping transform-origin or starting at right: 0 instead of left: 0 on the pseudo-element.

Gotchas — read before shipping

  • Pseudo-element borders on elements with border-radius don't follow the curve — the drawn line will cut across corners instead of arcing. Use outline or SVG for rounded draws.
  • Stacking ::before and ::after for the four sides means you only get two pseudo-elements per element — drawing all four sides independently requires a wrapper.
  • The gradient border trick requires the button background to exactly match the page background; on gradient or image backgrounds the center fill will mismatch.

Browser support

ChromeSafariFirefoxEdge
85+16.4+128+85+

Floor set by @property as this demo is written. The core technique itself goes back further — Chrome 60+.

All techniques are baseline CSS; the conic-gradient border variant needs Chrome 69+ and Safari 12.1+.

Techniques used in this demo

Search CodeFronts

Loading…