Button with Slanted Reveal

A CSS-only button hover effect where a slanted background sweeps in from below and duplicate label text slides in on a diagonal path using CSS custom properties for rotation and movement. Supports both hover and focus-visible states.

csshoverbuttontransformanimationaccessibility

Code

index.html
html
<a href="#" class="btn-slanted">
  <div class="btn-slanted-label__wrap">
    <span class="btn-slanted-label">Slanted Button</span>
    <span aria-hidden="true" class="btn-slanted-label">Slanted Button</span>
  </div>
  <div class="btn-slanted-bg"></div>
</a>
styles.css
css
.btn-slanted {
  color: #fff;
  background-color: #146ef5;
  border-radius: .125em;
  padding: .625em 1em;
  text-decoration: none;
  position: relative;
  overflow: hidden;
  --rotate: 20deg;
  --move: 150%;
}

.btn-slanted-label__wrap {
  z-index: 1;
  place-items: center;
  display: grid;
  position: relative;
}

.btn-slanted-bg {
  z-index: 0;
  transform-origin: 50% 0;
  border-radius: inherit;
  background-color: #002f79;
  width: 120%;
  height: 100%;
  position: absolute;
  top: auto;
  bottom: 0%;
  right: 0;
  transform: rotate(-30deg)translate(0%, 200%);
  transition: transform 0.45s cubic-bezier(0.625, 0.05, 0, 1);
}

.btn-slanted-label {
  transform-origin: 50% 1000%;
  transition: transform 0.45s cubic-bezier(0.625, 0.05, 0, 1);
}

.btn-slanted-label:not(:nth-of-type(1)) {
  position: absolute;
}

.btn-slanted-label:nth-of-type(1) {
  transform: translate(calc(var(--move) * 0), calc(var(--move) * 0)) rotate(calc(var(--rotate) * 0));
  transition-delay: 0.075s;
}

.btn-slanted-label:nth-of-type(2) {
  transform: translate(calc(var(--move) * -0.2), calc(var(--move) * 2.5)) rotate(calc(var(--rotate) * -2));
  transition-delay: 0s;
}

/* Hover styles */
@media (hover: hover) {
  .btn-slanted:hover .btn-slanted-label:nth-of-type(1) {
    transform: translate(calc(var(--move) * 0.2), calc(var(--move) * -2.5)) rotate(calc(var(--rotate) * 2));
    transition-delay: 0s;
  }
  .btn-slanted:hover .btn-slanted-label:nth-of-type(2) {
    transform: translate(calc(var(--move) * 0), calc(var(--move) * 0)) rotate(calc(var(--rotate) * 0));
    transition-delay: 0.075s;
  }
  .btn-slanted:hover .btn-slanted-bg {
    transform: translate(0%, 0%) rotate(0deg);
  }
}

/* Focus styles */
.btn-slanted:focus-visible .btn-slanted-label:nth-of-type(1) {
  transform: translate(calc(var(--move) * 0.2), calc(var(--move) * -2.5)) rotate(calc(var(--rotate) * 2));
  transition-delay: 0s;
}
.btn-slanted:focus-visible .btn-slanted-label:nth-of-type(2) {
  transform: translate(calc(var(--move) * 0), calc(var(--move) * 0)) rotate(calc(var(--rotate) * 0));
  transition-delay: 0.075s;
}
.btn-slanted:focus-visible .btn-slanted-bg {
  transform: translate(0%, 0%) rotate(0deg);
}

Guide

Overview

The button stacks two identical label spans using CSS grid's place-items: center. On hover the first span exits on a diagonal path while the second enters from below. A rotated background div sweeps up simultaneously.

CSS Variables

--rotate controls the rotation multiplier and --move controls the X/Y distance multiplier. Both are set on .btn-slanted and referenced in the transform calculations. To share values across all buttons, move them to :root instead.

Tweaking Values

Temporarily set overflow: visible on .btn-slanted while adjusting --rotate and --move so you can see exactly how the labels travel outside the button bounds before clipping them.

Accessibility

The second label span has aria-hidden="true" so screen readers only announce the text once. The hover styles are also applied on :focus-visible so keyboard users get the same effect.