[Lab]AWAIS MALIK

The lab.

Small interface effects from this site. Try them live, then copy the code — free to use in your own projects.

[01]

Scramble text

Random glyphs settle into the real word, left to right. Used on the navigation and footer links.

  • Vanilla JS
  • No dependencies

Live — hover me

View code · 2 files
index.html
<!-- A monospace font keeps the word from changing width while it scrambles -->
<a href="#" data-scramble>Hover me</a>
scramble.js
const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&*+/<>";
const STEP = 70; // ms before each next letter locks in
const FLICKER = 50; // ms between random-glyph shuffles

document.querySelectorAll("[data-scramble]").forEach((el) => {
  const original = el.textContent;
  let raf = 0;

  el.addEventListener("pointerenter", () => {
    cancelAnimationFrame(raf);
    // Time-based, so it runs at the same speed on 60Hz and 144Hz screens.
    const start = performance.now();
    let lastShuffle = -Infinity;

    const tick = (now) => {
      const locked = Math.floor((now - start) / STEP);
      if (locked >= original.length) {
        el.textContent = original;
        return;
      }
      if (now - lastShuffle >= FLICKER) {
        lastShuffle = now;
        el.textContent = [...original]
          .map((ch, i) => (ch === " " || i < locked ? ch : GLYPHS[Math.floor(Math.random() * GLYPHS.length)]))
          .join("");
      }
      raf = requestAnimationFrame(tick);
    };
    tick(start);
  });
});

[02]

Pointer mask

A circle follows the pointer and reveals an inverted copy of the label underneath. Used on the main navigation.

  • CSS clip-path
  • Vanilla JS
View code · 3 files
index.html
<a href="#" class="mask-link">
  Services
  <span class="mask-layer" aria-hidden="true">Services</span>
</a>
mask.css
.mask-link {
  position: relative;
  display: inline-block;
  overflow: hidden;
  padding: 0.65rem 1rem;
  background: #0d0d0c;
  color: #f1f0e8;
  font-family: ui-monospace, monospace;
  text-transform: uppercase;
}
/* Inverted copy of the label, clipped to a circle at the pointer */
.mask-layer {
  position: absolute;
  inset: 0;
  padding: inherit;
  background: #d6ff3b;
  color: #0d0d0c;
  pointer-events: none;
  clip-path: circle(0px at var(--x, 50%) var(--y, 50%));
  transition: clip-path 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.mask-link:hover .mask-layer {
  clip-path: circle(22px at var(--x, 50%) var(--y, 50%));
}
mask.js
document.querySelectorAll(".mask-link").forEach((link) => {
  link.addEventListener("pointermove", (e) => {
    const r = link.getBoundingClientRect();
    link.style.setProperty("--x", `${e.clientX - r.left}px`);
    link.style.setProperty("--y", `${e.clientY - r.top}px`);
  });
});

[03]

Letter roll

Each letter slides up and a coloured copy rolls in behind it, one after another. Used in the full-screen menu and pixel buttons.

  • Pure CSS hover
  • Tiny JS splitter
View code · 3 files
index.html
<a href="#" data-roll>Start a project</a>
roll.css
.roll-char {
  position: relative;
  display: inline-block;
  overflow: clip;
  padding-block: 0.04em; /* keeps glyph tops and bottoms out of the clip */
}
.roll-char > span {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i) * 22ms);
}
.roll-char > span + span {
  position: absolute;
  inset: 0.04em 0;
  transform: translateY(110%);
  color: var(--roll-color, #d6ff3b);
}
a:hover .roll-char > span:first-child,
a:focus-visible .roll-char > span:first-child { transform: translateY(-110%); }
a:hover .roll-char > span + span,
a:focus-visible .roll-char > span + span { transform: translateY(0); }
roll.js
// Wraps every letter in two stacked copies; screen readers get the plain text.
document.querySelectorAll("[data-roll]").forEach((el) => {
  const text = el.textContent;
  el.setAttribute("aria-label", text);
  el.innerHTML = [...text]
    .map((ch, i) => {
      const c = ch === " " ? "&nbsp;" : ch;
      return `<span class="roll-char" aria-hidden="true" style="--i:${i}"><span>${c}</span><span>${c}</span></span>`;
    })
    .join("");
});