Swish UI
All componentsButtons and controls Free

Lift and press buttons

Buttons rise 2px with a soft shadow on hover, sink to 97 percent on press, and the arrow nudges where it points.

It imports two small hooks from @/lib/motion and the colour tokens; the zip includes both.

Demo.tsx

"use client";
import styles from "./demo.module.css";

function Arrow({ diagonal = false }: { diagonal?: boolean }) {
  return (
    <svg className={styles.icon} viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
      {diagonal ? <path d="M5 11 11 5M6 5h5v5" /> : <path d="M3 8h10M9 4l4 4-4 4" />}
    </svg>
  );
}

export default function Demo() {
  return (
    <div className={styles.root}>
      <div className={styles.row}>
        <button type="button" className={styles.button}>
          Find a gap night
          <Arrow />
        </button>
        <button type="button" className={`${styles.button} ${styles.ghost} ${styles.diagonal}`}>
          Read the host guide
          <Arrow diagonal />
        </button>
      </div>
    </div>
  );
}

demo.module.css

.root { width: 100%; height: 100%; display: grid; place-items: center; padding: 16px; }
.row { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; }
.button {
  min-height: 48px; display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  padding: 0 24px; border-radius: 999px; border: 1.5px solid var(--ink);
  background: var(--ink); color: var(--white); font-size: 15px; font-weight: 600; white-space: nowrap; cursor: pointer;
  transition: background-color .18s, border-color .18s, box-shadow .18s, transform .18s var(--out);
}
.icon { flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; transition: transform .18s var(--out); }
.ghost { background: var(--white); color: var(--ink); border-color: var(--line); }
@media (hover: hover) {
  .button:hover { background: var(--ink-hover); border-color: var(--ink-hover); transform: translateY(-2px); box-shadow: 0 12px 24px -14px color-mix(in srgb, var(--ink) 60%, transparent); }
  .button:hover .icon { transform: translateX(3px); }
  .ghost:hover { background: var(--white); border-color: var(--ink); }
  .diagonal:hover .icon { transform: translate(2px, -2px); }
}
.button:active { transform: scale(.97); box-shadow: none; transition-duration: .08s; }
.ghost:active { background: var(--surface); }
@media (prefers-reduced-motion: reduce) {
  .button:hover, .button:active, .button:hover .icon { transform: none; }
}

Use it for

  • Primary and secondary calls to action on landing pages
  • Any button that should feel physical without drawing attention
  • A default button style for a whole product

Avoid

  • Dense toolbars, where lifting icons crowd each other
  • Buttons inside cards that already lift on hover

How it works

Plain CSS transitions on transform, box-shadow and colours. Hover translates the button up 2px and adds a blurred shadow with a negative spread so it stays tight; :active swaps to scale(.97) with a shorter duration so the press feels instant. The inline SVG arrow translates 3px right, or 2px up and right for the diagonal arrow, so the motion points where the link goes.

Accessibility

Hover rules sit inside a hover: hover media query so touch devices do not get stuck in the lifted state. Reduced motion keeps the colour change and drops all movement. Focus uses the global visible outline.

Found on