30 CSS Text Hover Effects18 / 30

CSS onlyMIT licensed

Animated Border / Encased Text Hover

Two grades of 'the text draws a box around itself': the TRACE — two corner pseudo-elements whose width and height tween in sequence, so a hairline visibly runs around the CTA clockwise, corner meeting corner — and the ORBIT — a conic-gradient ring rotating behind a padded inner panel, its angle driven by a registered @property so a comet of light laps the button under the cursor. Trace for editorial ghost buttons, orbit for the glowing tech CTA; both are markup you already have plus pseudo-elements.

Published

Live Demo
Try it

The code

<section class="cth-18" aria-label="Animated border encased text demo">
  <div class="cth-18__stage">
    <div class="cth-18__scene">
      <p class="cth-18__eyebrow">Helio deploy &mdash; private beta</p>
      <h1 class="cth-18__title">Ship the edge build</h1>
      <p class="cth-18__sub">Zero-config previews on every push. Two seats left in the winter cohort.</p>
      <div class="cth-18__row">
        <a class="cth-18__trace" href="#cth18-top" id="cth18-top"><span>Read the runbook</span></a>
        <a class="cth-18__orbit" href="#cth18-top"><span>Request access</span></a>
      </div>
      <p class="cth-18__hint">Left: two corner pseudo-elements trace the frame (width, then height &mdash; watch the corners meet). Right: a <code>@property</code> angle spins a conic comet around the text.</p>
    </div>
  </div>
</section>
@property --cth18-a {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.cth-18,
.cth-18 *,
.cth-18 *::before,
.cth-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-18 {
  --bg: oklch(0.17 0.012 260);
  --ink: oklch(0.95 0.008 250);
  --mut: oklch(0.62 0.02 255);
  --acc: oklch(0.85 0.17 130);
  --line: oklch(0.32 0.02 258);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cth-18__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: radial-gradient(90% 70% at 50% 25%,oklch(0.21 0.018 262),var(--bg) 72%);
  padding: clamp(20px,4vw,56px);
  container: cth18/inline-size;
}

.cth-18__scene {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: clamp(14px,2.6cqi,20px);
}

.cth-18__eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--acc);
}

.cth-18__title {
  font-size: clamp(34px,6.6cqi,62px);
  font-weight: 850;
  letter-spacing: -.025em;
  line-height: 1.06;
}

.cth-18__sub {
  font-size: 15px;
  line-height: 1.65;
  color: var(--mut);
  max-width: 46ch;
}

.cth-18__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(14px,3cqi,24px);
  margin-top: 10px;
}

.cth-18__trace {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 56px;
  padding: 0 30px;
  font-size: 15.5px;
  font-weight: 750;
  letter-spacing: .02em;
  color: var(--ink);
  text-decoration: none;
  background: oklch(0.2 0.014 260);
}

.cth-18__trace::before,
.cth-18__trace::after {
  content: '';
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}

.cth-18__trace::before {
  top: 0;
  left: 0;
  border-top: 1.5px solid var(--acc);
  border-right: 1.5px solid var(--acc);
  transition: width .2s ease-out,height .2s ease-out .2s;
}

.cth-18__trace::after {
  bottom: 0;
  right: 0;
  border-bottom: 1.5px solid var(--acc);
  border-left: 1.5px solid var(--acc);
  transition: width .2s ease-out,height .2s ease-out .2s;
}

.cth-18__trace:hover::before,
.cth-18__trace:hover::after,
.cth-18__trace:focus-visible::before,
.cth-18__trace:focus-visible::after {
  width: 100%;
  height: 100%;
}

.cth-18__trace:hover span,
.cth-18__trace:focus-visible span {
  color: var(--acc);
}

.cth-18__trace span {
  transition: color .3s ease .25s;
}

.cth-18__trace:focus-visible {
  outline: 1.5px solid var(--acc);
  outline-offset: 5px;
}

.cth-18__orbit {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 56px;
  padding: 0 2px;
  border-radius: 14px;
  text-decoration: none;
  background: conic-gradient(from var(--cth18-a),transparent 0 78%,var(--acc) 92%,transparent 100%);
  --cth18-a: 40deg;
}

.cth-18__orbit span {
  display: grid;
  place-items: center;
  min-height: calc(56px - 3px);
  margin: 1.5px;
  padding: 0 30px;
  border-radius: 12.5px;
  background: oklch(0.22 0.016 260);
  color: var(--ink);
  font-size: 15.5px;
  font-weight: 750;
  letter-spacing: .02em;
  transition: background .3s,color .3s;
}

.cth-18__orbit:hover,
.cth-18__orbit:focus-visible {
  animation: cth18-spin 1.6s linear infinite;
}

.cth-18__orbit:hover span,
.cth-18__orbit:focus-visible span {
  color: var(--acc);
  background: oklch(0.19 0.014 260);
}

.cth-18__orbit:focus-visible {
  outline: 1.5px solid var(--acc);
  outline-offset: 4px;
}

@keyframes cth18-spin {
  to {
    --cth18-a: 400deg;
  }
}

.cth-18__hint {
  margin-top: 8px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--mut);
  max-width: 60ch;
}

.cth-18__hint code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .9em;
  background: oklch(0.25 0.016 260);
  padding: 1px 6px;
  border-radius: 4px;
  color: var(--acc);
}

@media (prefers-reduced-motion: reduce) {
  .cth-18 *,
    .cth-18 *::before,
    .cth-18 *::after {
    transition-duration: .01ms !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 Text 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: Animated Border / Encased Text Hover
Source: https://codefronts.com/motion/css-text-hover-effects/animated-border-encased-text-hover/

Two grades of 'the text draws a box around itself': the TRACE — two corner pseudo-elements whose width and height tween in sequence, so a hairline visibly runs around the CTA clockwise, corner meeting corner — and the ORBIT — a conic-gradient ring rotating behind a padded inner panel, its angle driven by a registered @property so a comet of light laps the button under the cursor. Trace for editorial ghost buttons, orbit for the glowing tech CTA; both are markup you already have plus pseudo-elements.
## HTML
```html
<section class="cth-18" aria-label="Animated border encased text demo">
  <div class="cth-18__stage">
    <div class="cth-18__scene">
      <p class="cth-18__eyebrow">Helio deploy &mdash; private beta</p>
      <h1 class="cth-18__title">Ship the edge build</h1>
      <p class="cth-18__sub">Zero-config previews on every push. Two seats left in the winter cohort.</p>
      <div class="cth-18__row">
        <a class="cth-18__trace" href="#cth18-top" id="cth18-top"><span>Read the runbook</span></a>
        <a class="cth-18__orbit" href="#cth18-top"><span>Request access</span></a>
      </div>
      <p class="cth-18__hint">Left: two corner pseudo-elements trace the frame (width, then height &mdash; watch the corners meet). Right: a <code>@property</code> angle spins a conic comet around the text.</p>
    </div>
  </div>
</section>
```
## CSS
```css
@property --cth18-a {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.cth-18,
.cth-18 *,
.cth-18 *::before,
.cth-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-18 {
  --bg: oklch(0.17 0.012 260);
  --ink: oklch(0.95 0.008 250);
  --mut: oklch(0.62 0.02 255);
  --acc: oklch(0.85 0.17 130);
  --line: oklch(0.32 0.02 258);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cth-18__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: radial-gradient(90% 70% at 50% 25%,oklch(0.21 0.018 262),var(--bg) 72%);
  padding: clamp(20px,4vw,56px);
  container: cth18/inline-size;
}

.cth-18__scene {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: clamp(14px,2.6cqi,20px);
}

.cth-18__eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--acc);
}

.cth-18__title {
  font-size: clamp(34px,6.6cqi,62px);
  font-weight: 850;
  letter-spacing: -.025em;
  line-height: 1.06;
}

.cth-18__sub {
  font-size: 15px;
  line-height: 1.65;
  color: var(--mut);
  max-width: 46ch;
}

.cth-18__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(14px,3cqi,24px);
  margin-top: 10px;
}

.cth-18__trace {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 56px;
  padding: 0 30px;
  font-size: 15.5px;
  font-weight: 750;
  letter-spacing: .02em;
  color: var(--ink);
  text-decoration: none;
  background: oklch(0.2 0.014 260);
}

.cth-18__trace::before,
.cth-18__trace::after {
  content: '';
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}

.cth-18__trace::before {
  top: 0;
  left: 0;
  border-top: 1.5px solid var(--acc);
  border-right: 1.5px solid var(--acc);
  transition: width .2s ease-out,height .2s ease-out .2s;
}

.cth-18__trace::after {
  bottom: 0;
  right: 0;
  border-bottom: 1.5px solid var(--acc);
  border-left: 1.5px solid var(--acc);
  transition: width .2s ease-out,height .2s ease-out .2s;
}

.cth-18__trace:hover::before,
.cth-18__trace:hover::after,
.cth-18__trace:focus-visible::before,
.cth-18__trace:focus-visible::after {
  width: 100%;
  height: 100%;
}

.cth-18__trace:hover span,
.cth-18__trace:focus-visible span {
  color: var(--acc);
}

.cth-18__trace span {
  transition: color .3s ease .25s;
}

.cth-18__trace:focus-visible {
  outline: 1.5px solid var(--acc);
  outline-offset: 5px;
}

.cth-18__orbit {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 56px;
  padding: 0 2px;
  border-radius: 14px;
  text-decoration: none;
  background: conic-gradient(from var(--cth18-a),transparent 0 78%,var(--acc) 92%,transparent 100%);
  --cth18-a: 40deg;
}

.cth-18__orbit span {
  display: grid;
  place-items: center;
  min-height: calc(56px - 3px);
  margin: 1.5px;
  padding: 0 30px;
  border-radius: 12.5px;
  background: oklch(0.22 0.016 260);
  color: var(--ink);
  font-size: 15.5px;
  font-weight: 750;
  letter-spacing: .02em;
  transition: background .3s,color .3s;
}

.cth-18__orbit:hover,
.cth-18__orbit:focus-visible {
  animation: cth18-spin 1.6s linear infinite;
}

.cth-18__orbit:hover span,
.cth-18__orbit:focus-visible span {
  color: var(--acc);
  background: oklch(0.19 0.014 260);
}

.cth-18__orbit:focus-visible {
  outline: 1.5px solid var(--acc);
  outline-offset: 4px;
}

@keyframes cth18-spin {
  to {
    --cth18-a: 400deg;
  }
}

.cth-18__hint {
  margin-top: 8px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--mut);
  max-width: 60ch;
}

.cth-18__hint code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .9em;
  background: oklch(0.25 0.016 260);
  padding: 1px 6px;
  border-radius: 4px;
  color: var(--acc);
}

@media (prefers-reduced-motion: reduce) {
  .cth-18 *,
    .cth-18 *::before,
    .cth-18 *::after {
    transition-duration: .01ms !important;
    animation: none !important;
  }
}
```

How this works

THE TRACE. Each pseudo-element owns two adjacent sides and one corner. Sequencing widths before heights makes the line appear to travel:

.trace::before{ content:''; position:absolute; top:0; left:0;
  width:0; height:0; border-top:1.5px solid var(--acc);
  border-right:1.5px solid var(--acc);
  transition:width .2s ease-out, height .2s ease-out .2s; }
.trace::after{ /* mirrored: bottom-left, border-bottom+left */ }
.trace:hover::before,
.trace:hover::after{ width:100%; height:100%; }

::before starts at the top-left, grows its width along the top edge, THEN (the .2s delay) grows height down the right side; ::after mirrors from the bottom-right — the two meet at opposite corners and the box closes. Un-hover runs it backward: the delays make the line retreat the way it came, a detail transitions give you free and keyframes never would.

THE ORBIT. A conic gradient can't rotate — but its angle input can, once it's a typed property:

@property --cth18-a{ syntax:'<angle>';
  inherits:false; initial-value:0deg; }
.orbit{ background:conic-gradient(from var(--cth18-a),
  transparent 0 78%, var(--acc) 92%, transparent 100%); }
.orbit:hover{ animation:cth18-spin 1.6s linear infinite; }
@keyframes cth18-spin{ to{ --cth18-a:360deg; } }

The ring is the element's own background; an inner span with inset:1.5px and the page's surface color masks the middle, leaving a 1.5px annulus where the comet runs. Because only a custom property animates, there's no transform on the box — the glow orbits while the button and its text hold perfectly still.

Make it yours

  • Trace speed: .2s per side ≈ .4s to encase — doubling to .4s/.4s feels like a fountain pen; keep both pseudo-elements symmetric or the corners visibly miss.
  • Orbit comet length: the gap between 78% and 92% in the conic stops — widen for a soft halo lap, tighten to 96%+ for a scanner blip.
  • Two comets: add a second bright stop opposite (at 28%–42%) for a radar pair; it halves the perceived lap time without changing the animation.
  • Freeze the orbit as a static gradient ring on :focus-visible if the infinite spin fights your brand — the @property makes the angle a design token you can set anywhere.

Gotchas — read before shipping

  • The trace's un-hover reversal only mirrors correctly if the transition delays are declared on the BASE state (they re-apply in reverse) — moving them into :hover breaks the retreat.
  • Pointer-events: the pseudo-elements overlay the button's edge; they're decoration, so they inherit pointer-events from the parent — never give them their own hit-testing.
  • @property animations tick on the main thread (custom properties aren't compositable) — one orbiting CTA is nothing, twenty is jank. Budget like you would box-shadow animation.
  • Without @property support (Firefox <128) the conic angle can't tween — the orbit degrades to a static ring; the trace variant is your universal fallback (it's 2015-era CSS).

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Trace variant runs in any 2015+ engine (floor is oklch cosmetics). Orbit needs @property: Chrome 85+/Safari 16.4+/Firefox 128+, degrading to a static gradient ring below.

Techniques used in this demo

Search CodeFronts

Loading…