30 CSS Hover Effects 05 / 30

CSS Split Text Hover Effect

Four split-text hover effects where words or characters divide and separate on hover — vertical halve, top-bottom reveal, left-right push-apart, and diagonal slash — all achieved with CSS clip-path on layered pseudo-elements.

Pure CSS MIT licensed
Live Demo Open in tab

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

Open in playground

The code

<div class="hv-05">
  <div class="hv-05__grid">
    <div class="hv-05__cell">
      <span class="hv-05__split hv-05__split--vert" data-text="DIVIDE">DIVIDE</span>
      <span class="hv-05__label">top / bottom split</span>
    </div>
    <div class="hv-05__cell">
      <span class="hv-05__split hv-05__split--lr" data-text="SCHISM">SCHISM</span>
      <span class="hv-05__label">left / right push</span>
    </div>
    <div class="hv-05__cell">
      <span class="hv-05__split hv-05__split--diag" data-text="SLASH">SLASH</span>
      <span class="hv-05__label">diagonal polygon</span>
    </div>
    <div class="hv-05__cell">
      <span class="hv-05__split hv-05__split--fade" data-text="RIFT">RIFT</span>
      <span class="hv-05__label">split + fade</span>
    </div>
  </div>
</div>
.hv-05,.hv-05 *,.hv-05 *::before,.hv-05 *::after{box-sizing:border-box;margin:0;padding:0}
.hv-05 ::selection{background:#0891b2;color:#fff}
.hv-05{
  --bg:#020917;
  --text:#e0f2fe;
  --dim:#475569;
  --cyan:#06b6d4;
  --rose:#f43f5e;
  --violet:#8b5cf6;
  --amber:#f59e0b;
  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-05__grid{
  display:grid;grid-template-columns:repeat(2,1fr);gap:48px;
  max-width:800px;width:100%;
}
.hv-05__cell{
  display:flex;flex-direction:column;align-items:center;gap:24px;
  padding:60px 32px;
  background:rgba(255,255,255,.025);
  border:1px solid rgba(255,255,255,.07);
  border-radius:14px;
}
.hv-05__label{font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:var(--dim)}

/* shared split base */
.hv-05__split{
  font-size:clamp(2rem,6vw,3rem);font-weight:900;letter-spacing:.1em;
  position:relative;cursor:default;display:inline-block;
  color:transparent;overflow:hidden;
}
.hv-05__split::before,
.hv-05__split::after{
  content:attr(data-text);
  position:absolute;top:0;left:0;width:100%;height:100%;
  transition:transform .45s cubic-bezier(.4,0,.2,1),opacity .45s;
  pointer-events:none;
}

/* 1 — vertical top/bottom */
.hv-05__split--vert::before{color:var(--cyan);clip-path:inset(0 0 50% 0)}
.hv-05__split--vert::after{color:var(--cyan);clip-path:inset(50% 0 0 0)}
.hv-05__split--vert:hover::before{transform:translateY(-.55em)}
.hv-05__split--vert:hover::after{transform:translateY(.55em)}

/* 2 — left/right */
.hv-05__split--lr::before{color:var(--rose);clip-path:inset(0 50% 0 0)}
.hv-05__split--lr::after{color:var(--amber);clip-path:inset(0 0 0 50%)}
.hv-05__split--lr:hover::before{transform:translateX(-.4em)}
.hv-05__split--lr:hover::after{transform:translateX(.4em)}

/* 3 — diagonal */
.hv-05__split--diag::before{
  color:var(--violet);
  clip-path:polygon(0 0,100% 0,60% 100%,0 100%);
}
.hv-05__split--diag::after{
  color:var(--cyan);
  clip-path:polygon(60% 0,100% 0,100% 100%,0 100%);
  /* small intentional gap at clip edge */
}
.hv-05__split--diag:hover::before{transform:translate(-.3em,-.3em)}
.hv-05__split--diag:hover::after{transform:translate(.3em,.3em)}

/* 4 — split + fade */
.hv-05__split--fade::before{color:var(--amber);clip-path:inset(0 0 50% 0)}
.hv-05__split--fade::after{color:var(--rose);clip-path:inset(50% 0 0 0)}
.hv-05__split--fade:hover::before{transform:translateY(-.6em) rotate(-3deg);opacity:0}
.hv-05__split--fade:hover::after{transform:translateY(.6em) rotate(3deg);opacity:0}

@media(max-width:520px){.hv-05__grid{grid-template-columns:1fr}}
@media(prefers-reduced-motion:reduce){
  .hv-05__split::before,.hv-05__split::after{transition:none!important}
}

How this works

Each variant layers the same text twice using ::before and ::after pseudo-elements with content: attr(data-text). The two pseudo-elements are clipped to complementary halves using clip-path: inset() — the top half uses inset(0 0 50% 0) and the bottom uses inset(50% 0 0 0). On hover, transform: translateY() pushes each half in the opposite direction, revealing the background beneath.

The diagonal slash replaces rectangular inset() with polygon() coordinates so the split follows a diagonal line. The left-right push uses inset(0 50% 0 0) / inset(0 0 0 50%) combined with translateX. All movements are smooth via transition: transform .4s cubic-bezier(.4,0,.2,1) and each half can be tinted a different colour to emphasise the split.

Customize

  • Adjust split depth by increasing the translateY value — translateY(-1.2em) gives a more dramatic separation than the default -.5em.
  • Tint each half a distinct colour by setting different color values on ::before and ::after for a duochrome effect.
  • Change the diagonal angle by adjusting the polygon() coordinates — polygon(0 0, 55% 0, 45% 100%, 0 100%) steepens the slash.
  • Add a letter-spacing expansion on hover to combine the spatial split with a typographic breathe-out.
  • Apply the effect inside a button element by replacing span with a button and adjusting position: relative; overflow: hidden on the button.

Watch out for

  • The parent element must have overflow: hidden if you don't want the departing halves to remain visible outside the element bounds.
  • clip-path interacts with will-change: clip-path — avoid setting will-change: transform on the same element or the browser may create separate layers that prevent clipping.
  • Setting color: transparent on the base element is required so the actual text doesn't show through beneath the two pseudo-element halves.

Browser support

ChromeSafariFirefoxEdge
55+ 13.1+ 54+ 55+

clip-path polygon is fully supported in all modern browsers; no prefixes needed.

Search CodeFronts

Loading…