/ webflow magic

150 Attribute Tricks
for Webflow

Drop any of these into Webflow via Custom Attributes or an Embed block. No plugins. No page builders. Just pure CSS + JS.

CSS Only JS Only CSS + JS

No tricks found. Try a different search.

#01 CSS Only

Fade In on Scroll

Elements fade in as they enter the viewport. No JS needed — pure CSS scroll-driven animation.

Attribute
data-animate="fade-in"
CSS
[data-animate="fade-in"] {
  opacity: 0;
  animation: fadeIn linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 25%;
}
@keyframes fadeIn { to { opacity: 1; } }
Animations
#02 CSS Only

Slide Up on Scroll

Elements slide up into view on scroll entry. Great for cards, headings, and sections.

Attribute
data-animate="slide-up"
CSS
[data-animate="slide-up"] {
  opacity: 0;
  transform: translateY(40px);
  animation: slideUp linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}
@keyframes slideUp { to { opacity: 1; transform: none; } }
Animations
#03 CSS + JS

Stagger Children

Automatically staggers the animation delay for each direct child element.

Attribute
data-stagger="children"
CSS
[data-stagger="children"] > * {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
[data-stagger="children"].is-visible > * { opacity: 1; transform: none; }
[data-stagger="children"].is-visible > *:nth-child(1) { transition-delay: 0ms; }
[data-stagger="children"].is-visible > *:nth-child(2) { transition-delay: 80ms; }
[data-stagger="children"].is-visible > *:nth-child(3) { transition-delay: 160ms; }
[data-stagger="children"].is-visible > *:nth-child(4) { transition-delay: 240ms; }
[data-stagger="children"].is-visible > *:nth-child(5) { transition-delay: 320ms; }
JS JavaScript
document.querySelectorAll('[data-stagger="children"]').forEach(el => {
  new IntersectionObserver(([e]) => {
    if (e.isIntersecting) el.classList.add('is-visible');
  }, { threshold: 0.1 }).observe(el);
});
Animations
#04 JS Only

Parallax Scroll

Moves an element at a different speed than the scroll. Set speed with data-parallax='0.3'.

Attribute
data-parallax="0.3"
JS JavaScript
document.querySelectorAll('[data-parallax]').forEach(el => {
  const speed = parseFloat(el.dataset.parallax) || 0.3;
  window.addEventListener('scroll', () => {
    el.style.transform = `translateY(${window.scrollY * speed}px)`;
  }, { passive: true });
});
Animations
#05 JS Only

Count Up Number

Animates a number from 0 to its text value when it enters the viewport.

Attribute
data-countup
JS JavaScript
document.querySelectorAll('[data-countup]').forEach(el => {
  const target = parseInt(el.textContent, 10);
  const duration = 1500;
  let start = null;
  const observer = new IntersectionObserver(([e]) => {
    if (!e.isIntersecting) return;
    observer.disconnect();
    const step = (ts) => {
      if (!start) start = ts;
      const progress = Math.min((ts - start) / duration, 1);
      el.textContent = Math.floor(progress * target);
      if (progress < 1) requestAnimationFrame(step);
      else el.textContent = target;
    };
    requestAnimationFrame(step);
  });
  observer.observe(el);
});
Animations
#06 CSS + JS

Typewriter Text

Types out text character by character. Set the text in the attribute itself.

Attribute
data-typewriter="Hello, World!"
CSS
[data-typewriter]::after {
  content: '|';
  animation: blink 0.7s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }
JS JavaScript
document.querySelectorAll('[data-typewriter]').forEach(el => {
  const text = el.dataset.typewriter;
  el.textContent = '';
  let i = 0;
  const type = () => {
    if (i < text.length) { el.textContent += text[i++]; setTimeout(type, 55); }
  };
  new IntersectionObserver(([e]) => {
    if (e.isIntersecting) { type(); }
  }, { threshold: 0.5 }).observe(el);
});
Animations
#07 CSS Only

Pulse Glow

Adds a repeating glow pulse. Great for CTAs and live status indicators.

Attribute
data-glow="pulse"
CSS
[data-glow="pulse"] {
  animation: glowPulse 2s ease-in-out infinite;
}
@keyframes glowPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(99,102,241,0.5); }
  50%       { box-shadow: 0 0 0 12px rgba(99,102,241,0); }
}
Animations
#08 CSS Only

Shake on Hover

Shakes an element on hover — useful for error states or attention-grabbing elements.

Attribute
data-hover="shake"
CSS
[data-hover="shake"]:hover {
  animation: shake 0.4s ease;
}
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%       { transform: translateX(-6px); }
  40%       { transform: translateX(6px); }
  60%       { transform: translateX(-4px); }
  80%       { transform: translateX(4px); }
}
Animations
#09 CSS + JS

Sticky Progress Bar

A reading progress bar fixed to the top of the page that fills as you scroll.

Attribute
data-progress-bar
CSS
[data-progress-bar] {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 0%;
  background: var(--accent, #6366f1);
  z-index: 9999;
  transition: width 0.1s linear;
}
JS JavaScript
const bar = document.querySelector('[data-progress-bar]');
if (bar) {
  window.addEventListener('scroll', () => {
    const pct = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
    bar.style.width = pct + '%';
  }, { passive: true });
}
Scroll
#10 CSS + JS

Scroll to Target

Smooth scrolls to the element with the matching ID. Add to any link or button.

Attribute
data-scroll-to="#section-id"
CSS
html { scroll-behavior: smooth; }
JS JavaScript
document.querySelectorAll('[data-scroll-to]').forEach(el => {
  el.addEventListener('click', e => {
    e.preventDefault();
    document.querySelector(el.dataset.scrollTo)?.scrollIntoView({ behavior: 'smooth' });
  });
});
Scroll
#11 CSS Only

Scroll Snap Section

Turns a wrapper into a full-screen scroll-snapping container — each child snaps into view.

Attribute
data-snap="container"
CSS
[data-snap="container"] {
  height: 100vh;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
}
[data-snap="container"] > * {
  scroll-snap-align: start;
  min-height: 100vh;
}
Scroll
#12 CSS + JS

Back to Top Button

Shows a back-to-top button after scrolling 400px, hides it at the top.

Attribute
data-back-to-top
CSS
[data-back-to-top] {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
[data-back-to-top].is-visible {
  opacity: 1;
  pointer-events: auto;
}
JS JavaScript
const btn = document.querySelector('[data-back-to-top]');
if (btn) {
  window.addEventListener('scroll', () => {
    btn.classList.toggle('is-visible', window.scrollY > 400);
  }, { passive: true });
  btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
}
Scroll
#13 CSS Only

Horizontal Scroll Section

Makes a section scroll horizontally. Add to a wide container with child columns.

Attribute
data-scroll="horizontal"
CSS
[data-scroll="horizontal"] {
  overflow-x: auto;
  overflow-y: hidden;
  display: flex;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
[data-scroll="horizontal"]::-webkit-scrollbar { display: none; }
[data-scroll="horizontal"] > * {
  flex: 0 0 auto;
  scroll-snap-align: start;
}
Scroll
#14 CSS + JS

Custom Cursor Follower

A custom circle that follows the cursor with a smooth lag effect.

Attribute
data-cursor="follow"
CSS
[data-cursor="follow"] {
  position: fixed;
  width: 20px; height: 20px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
  pointer-events: none;
  z-index: 9999;
  transition: transform 0.12s ease, opacity 0.3s ease;
}
JS JavaScript
const cursor = document.querySelector('[data-cursor="follow"]');
if (cursor) {
  let mx = 0, my = 0, cx = 0, cy = 0;
  document.addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; });
  (function loop() {
    cx += (mx - cx) * 0.12;
    cy += (my - cy) * 0.12;
    cursor.style.left = (cx - 10) + 'px';
    cursor.style.top  = (cy - 10) + 'px';
    requestAnimationFrame(loop);
  })();
}
Cursor
#15 CSS + JS

Magnetic Button

Button pulls toward the cursor like a magnet on hover. Perfect for CTAs.

Attribute
data-magnetic
CSS
[data-magnetic] { transition: transform 0.3s ease; }
JS JavaScript
document.querySelectorAll('[data-magnetic]').forEach(el => {
  el.addEventListener('mousemove', e => {
    const r = el.getBoundingClientRect();
    const x = (e.clientX - r.left - r.width / 2) * 0.35;
    const y = (e.clientY - r.top - r.height / 2) * 0.35;
    el.style.transform = `translate(${x}px, ${y}px)`;
  });
  el.addEventListener('mouseleave', () => el.style.transform = '');
});
Cursor
#16 CSS + JS

Cursor Blend Mode

Cursor element uses mix-blend-mode: difference for an invert effect over any content.

Attribute
data-cursor="blend"
CSS
[data-cursor="blend"] {
  position: fixed;
  width: 24px; height: 24px;
  background: #fff;
  border-radius: 50%;
  pointer-events: none;
  z-index: 9999;
  mix-blend-mode: difference;
}
JS JavaScript
const cur = document.querySelector('[data-cursor="blend"]');
if (cur) {
  document.addEventListener('mousemove', e => {
    cur.style.left = (e.clientX - 12) + 'px';
    cur.style.top  = (e.clientY - 12) + 'px';
  });
}
Cursor
#17 CSS Only

Gradient Text

Applies an animated gradient to text using background-clip trick.

Attribute
data-text="gradient"
CSS
[data-text="gradient"] {
  background: linear-gradient(90deg, #6366f1, #ec4899, #f59e0b);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
Text
#18 CSS Only

Animated Gradient Text

Same as gradient text but the gradient shifts continuously — eye-catching hero headings.

Attribute
data-text="gradient-animate"
CSS
[data-text="gradient-animate"] {
  background: linear-gradient(90deg, #6366f1, #ec4899, #f59e0b, #6366f1);
  background-size: 200% auto;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  animation: gradShift 3s linear infinite;
}
@keyframes gradShift { to { background-position: 200% center; } }
Text
#19 JS Only

Scramble Text on Hover

Scrambles the text with random characters on hover, then resolves back.

Attribute
data-scramble
JS JavaScript
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%';
document.querySelectorAll('[data-scramble]').forEach(el => {
  const original = el.textContent;
  let frame, iteration = 0;
  el.addEventListener('mouseenter', () => {
    clearInterval(frame);
    iteration = 0;
    frame = setInterval(() => {
      el.textContent = original.split('').map((c, i) => {
        if (c === ' ') return ' ';
        if (i < iteration) return original[i];
        return chars[Math.floor(Math.random() * chars.length)];
      }).join('');
      if (iteration >= original.length) clearInterval(frame);
      iteration += 0.5;
    }, 30);
  });
});
Text
#20 CSS + JS

Split Word Reveal

Splits each word into a span and reveals them with a staggered animation.

Attribute
data-reveal="words"
CSS
[data-reveal="words"] .word {
  display: inline-block;
  opacity: 0;
  transform: translateY(100%);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
[data-reveal="words"].is-visible .word { opacity: 1; transform: none; }
JS JavaScript
document.querySelectorAll('[data-reveal="words"]').forEach(el => {
  el.innerHTML = el.textContent.split(' ')
    .map((w, i) => `<span class="word" style="transition-delay:${i * 60}ms">${w}&nbsp;</span>`)
    .join('');
  new IntersectionObserver(([e]) => {
    if (e.isIntersecting) el.classList.add('is-visible');
  }, { threshold: 0.3 }).observe(el);
});
Text
#21 CSS Only

Text Stroke Outline

Makes text outline-only with no fill — great for large display headings.

Attribute
data-text="outline"
CSS
[data-text="outline"] {
  -webkit-text-stroke: 1.5px currentColor;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
Text
#22 CSS + JS

Highlight on Scroll

Highlights a span of text like a marker pen when it scrolls into view.

Attribute
data-highlight
CSS
[data-highlight] {
  background: linear-gradient(to right, #fde047 0%, #fde047 100%) no-repeat;
  background-size: 0% 40%;
  background-position: 0 80%;
  transition: background-size 0.6s ease;
}
[data-highlight].is-visible { background-size: 100% 40%; }
JS JavaScript
document.querySelectorAll('[data-highlight]').forEach(el => {
  new IntersectionObserver(([e]) => {
    if (e.isIntersecting) el.classList.add('is-visible');
  }, { threshold: 0.6 }).observe(el);
});
Text
#23 CSS + JS

Toggle Visibility

Clicking a trigger toggles the target element's visibility. Accordion-style.

Attribute
data-toggle="#target-id"
CSS
[data-toggle-target] { display: none; }
[data-toggle-target].is-open { display: block; }
JS JavaScript
document.querySelectorAll('[data-toggle]').forEach(trigger => {
  trigger.addEventListener('click', () => {
    document.querySelectorAll(trigger.dataset.toggle).forEach(target => {
      target.classList.toggle('is-open');
    });
  });
});
UI Patterns
#24 CSS + JS

Accordion

Native-feeling accordion with smooth height transition using the details element pattern.

Attribute
data-accordion
CSS
[data-accordion] { overflow: hidden; max-height: 0; transition: max-height 0.4s ease; }
[data-accordion].is-open { max-height: 600px; }
JS JavaScript
document.querySelectorAll('[data-accordion-trigger]').forEach(trigger => {
  trigger.addEventListener('click', () => {
    const target = document.querySelector(trigger.dataset.accordionTrigger);
    target?.classList.toggle('is-open');
    trigger.setAttribute('aria-expanded', target?.classList.contains('is-open') ? 'true' : 'false');
  });
});
UI Patterns
#25 CSS Only

Tooltip on Hover

Pure CSS tooltip — no JS. The content comes from the attribute value.

Attribute
data-tooltip="Your tooltip text"
CSS
[data-tooltip] { position: relative; cursor: default; }
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: #111;
  color: #fff;
  font-size: 0.75rem;
  padding: 0.3rem 0.7rem;
  border-radius: 6px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
[data-tooltip]:hover::after { opacity: 1; }
UI Patterns
#26 CSS + JS

Copy to Clipboard

Copies the element's text content to clipboard on click and shows a confirmation.

Attribute
data-copy
CSS
[data-copy] { cursor: pointer; }
[data-copy].copied::after { content: ' ✓ Copied'; color: #6bcb77; font-size: 0.8em; }
JS JavaScript
document.querySelectorAll('[data-copy]').forEach(el => {
  el.addEventListener('click', () => {
    navigator.clipboard.writeText(el.textContent.trim()).then(() => {
      el.classList.add('copied');
      setTimeout(() => el.classList.remove('copied'), 2000);
    });
  });
});
UI Patterns
#27 CSS + JS

Tab Switcher

Simple tab component. Trigger has data-tab='name', content has data-tab-panel='name'.

Attribute
data-tab="panel-name"
CSS
[data-tab-panel] { display: none; }
[data-tab-panel].is-active { display: block; }
[data-tab].is-active { font-weight: 700; border-bottom: 2px solid currentColor; }
JS JavaScript
document.querySelectorAll('[data-tab]').forEach(tab => {
  tab.addEventListener('click', () => {
    document.querySelectorAll('[data-tab]').forEach(t => t.classList.remove('is-active'));
    document.querySelectorAll('[data-tab-panel]').forEach(p => p.classList.remove('is-active'));
    tab.classList.add('is-active');
    document.querySelector(`[data-tab-panel="${tab.dataset.tab}"]`)?.classList.add('is-active');
  });
});
UI Patterns
#28 CSS + JS

Modal Open / Close

Opens a modal overlay. data-modal-open targets the modal ID, data-modal-close is inside it.

Attribute
data-modal-open="#modal-id"
CSS
[data-modal] {
  display: none;
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.6);
  backdrop-filter: blur(4px);
  z-index: 9999;
  align-items: center;
  justify-content: center;
}
[data-modal].is-open { display: flex; }
JS JavaScript
document.querySelectorAll('[data-modal-open]').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelector(btn.dataset.modalOpen)?.classList.add('is-open');
  });
});
document.querySelectorAll('[data-modal-close]').forEach(btn => {
  btn.closest('[data-modal]')?.addEventListener('click', e => {
    if (e.target === btn || e.target === btn.closest('[data-modal]'))
      btn.closest('[data-modal]').classList.remove('is-open');
  });
});
UI Patterns
#29 CSS + JS

Lazy Load Image

Loads images only when they enter the viewport, using the src from a data attribute.

Attribute
data-src="/image.jpg"
CSS
[data-src] { opacity: 0; transition: opacity 0.4s ease; }
[data-src].loaded { opacity: 1; }
JS JavaScript
document.querySelectorAll('img[data-src]').forEach(img => {
  new IntersectionObserver(([e], obs) => {
    if (!e.isIntersecting) return;
    img.src = img.dataset.src;
    img.onload = () => img.classList.add('loaded');
    obs.disconnect();
  }).observe(img);
});
UI Patterns
#30 CSS + JS

Dark Mode Toggle

Toggles dark/light mode by adding a class to the <html> element and saves the preference.

Attribute
data-theme-toggle
CSS
html.dark { filter: invert(1) hue-rotate(180deg); }
html.dark img, html.dark video { filter: invert(1) hue-rotate(180deg); }
JS JavaScript
const toggle = document.querySelector('[data-theme-toggle]');
if (toggle) {
  const html = document.documentElement;
  if (localStorage.getItem('theme') === 'dark') html.classList.add('dark');
  toggle.addEventListener('click', () => {
    html.classList.toggle('dark');
    localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
  });
}
UI Patterns
#31 CSS Only

Equal Height Grid

Forces all children to equal height regardless of content length.

Attribute
data-grid="equal"
CSS
[data-grid="equal"] {
  display: grid;
  grid-auto-rows: 1fr;
  align-items: stretch;
}
[data-grid="equal"] > * { height: 100%; }
Layout
#32 CSS Only

Auto Masonry Grid

CSS-only masonry-style layout using columns property. No JS, no library.

Attribute
data-grid="masonry"
CSS
[data-grid="masonry"] {
  columns: 3 280px;
  column-gap: 1.5rem;
}
[data-grid="masonry"] > * {
  break-inside: avoid;
  margin-bottom: 1.5rem;
}
Layout
#33 CSS Only

Full Bleed Section

Makes an element inside a constrained container break out to full viewport width.

Attribute
data-bleed="full"
CSS
[data-bleed="full"] {
  width: 100vw;
  margin-left: calc(50% - 50vw);
}
Layout
#34 CSS Only

Aspect Ratio Box

Locks an element to a specific aspect ratio. Value like '16/9', '1/1', '4/3'.

Attribute
data-ratio="16/9"
CSS
[data-ratio] { aspect-ratio: attr(data-ratio); }
[data-ratio="16/9"] { aspect-ratio: 16/9; }
[data-ratio="1/1"]  { aspect-ratio: 1/1; }
[data-ratio="4/3"]  { aspect-ratio: 4/3; }
Layout
#35 CSS Only

Sticky Sidebar

Makes a sidebar column sticky while the main content scrolls past it.

Attribute
data-sticky
CSS
[data-sticky] {
  position: sticky;
  top: 2rem;
  align-self: start;
}
Layout
#36 CSS + JS

Tilt 3D Card

Card tilts in 3D following the mouse position for a depth effect on hover.

Attribute
data-tilt
CSS
[data-tilt] { transform-style: preserve-3d; transition: transform 0.1s ease; }
JS JavaScript
document.querySelectorAll('[data-tilt]').forEach(el => {
  el.addEventListener('mousemove', e => {
    const r = el.getBoundingClientRect();
    const x = (e.clientY - r.top - r.height / 2) / r.height * -20;
    const y = (e.clientX - r.left - r.width / 2) / r.width * 20;
    el.style.transform = `perspective(600px) rotateX(${x}deg) rotateY(${y}deg)`;
  });
  el.addEventListener('mouseleave', () => el.style.transform = '');
});
Visual FX
#37 CSS Only

Glass Morphism

Applies a frosted glass effect using backdrop-filter.

Attribute
data-glass
CSS
[data-glass] {
  background: rgba(255,255,255,0.1);
  backdrop-filter: blur(12px) saturate(180%);
  -webkit-backdrop-filter: blur(12px) saturate(180%);
  border: 1px solid rgba(255,255,255,0.18);
  border-radius: 12px;
}
Visual FX
#38 CSS Only

Noise Texture Overlay

Adds a subtle grain/noise texture over an element using SVG filter.

Attribute
data-noise
CSS
[data-noise] { position: relative; }
[data-noise]::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
  opacity: 0.06;
}
Visual FX
#39 CSS + JS

Spotlight Follow Mouse

Radial gradient spotlight on a card that follows the cursor, like a flashlight.

Attribute
data-spotlight
CSS
[data-spotlight] {
  position: relative;
  overflow: hidden;
}
[data-spotlight]::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(circle at var(--mx, 50%) var(--my, 50%), rgba(255,255,255,0.08) 0%, transparent 60%);
  transition: opacity 0.3s;
  opacity: 0;
}
[data-spotlight]:hover::before { opacity: 1; }
JS JavaScript
document.querySelectorAll('[data-spotlight]').forEach(el => {
  el.addEventListener('mousemove', e => {
    const r = el.getBoundingClientRect();
    el.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%');
    el.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100) + '%');
  });
});
Visual FX
#40 CSS Only

Gradient Border

Animated gradient border around any element using a pseudo-element trick.

Attribute
data-border="gradient"
CSS
[data-border="gradient"] {
  position: relative;
  border-radius: 12px;
  background: var(--bg, #fff);
  z-index: 0;
}
[data-border="gradient"]::before {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  background: linear-gradient(135deg, #6366f1, #ec4899, #f59e0b, #6366f1);
  background-size: 200% 200%;
  z-index: -1;
  animation: borderSpin 3s linear infinite;
}
@keyframes borderSpin { to { background-position: 200% 200%; } }
Visual FX
#41 CSS Only

Floating Label Input

Label floats above the input when focused or filled — Material-style.

Attribute
data-float-label
CSS
[data-float-label] { position: relative; }
[data-float-label] label {
  position: absolute;
  left: 12px; top: 50%;
  transform: translateY(-50%);
  font-size: 1rem;
  color: #888;
  pointer-events: none;
  transition: 0.2s ease;
}
[data-float-label] input:focus + label,
[data-float-label] input:not(:placeholder-shown) + label {
  top: 0; font-size: 0.72rem; color: #6366f1;
  background: white; padding: 0 4px;
}
Forms
#42 CSS + JS

Password Strength Meter

Shows a color-coded strength bar as the user types a password.

Attribute
data-strength-meter
CSS
[data-strength-bar] {
  height: 4px;
  border-radius: 2px;
  background: #e5e7eb;
  overflow: hidden;
}
[data-strength-bar]::after {
  content: '';
  display: block;
  height: 100%;
  width: var(--strength, 0%);
  background: var(--strength-color, #ef4444);
  transition: width 0.3s ease, background 0.3s ease;
  border-radius: 2px;
}
JS JavaScript
const meter = document.querySelector('[data-strength-meter]');
const bar = document.querySelector('[data-strength-bar]');
if (meter && bar) {
  meter.addEventListener('input', () => {
    const v = meter.value;
    let score = 0;
    if (v.length >= 8) score++;
    if (/[A-Z]/.test(v)) score++;
    if (/[0-9]/.test(v)) score++;
    if (/[^A-Za-z0-9]/.test(v)) score++;
    const colors = ['#ef4444','#f97316','#facc15','#22c55e'];
    bar.style.setProperty('--strength', (score * 25) + '%');
    bar.style.setProperty('--strength-color', colors[score - 1] || '#ef4444');
  });
}
Forms
#43 CSS + JS

Character Counter

Live character count below a textarea. Set max via data-max='280'.

Attribute
data-char-count data-max="280"
CSS
[data-char-display] { font-size: 0.75rem; color: #888; text-align: right; }
[data-char-display].over { color: #ef4444; }
JS JavaScript
document.querySelectorAll('[data-char-count]').forEach(el => {
  const max = parseInt(el.dataset.max, 10) || 0;
  const display = document.querySelector(`[data-char-display="${el.id}"]`);
  if (!display) return;
  el.addEventListener('input', () => {
    const len = el.value.length;
    display.textContent = max ? `${len} / ${max}` : len;
    display.classList.toggle('over', max > 0 && len > max);
  });
});
Forms
#44 CSS + JS

Shrink Navbar on Scroll

Navbar shrinks in padding and adds a background when the user scrolls past 80px.

Attribute
data-navbar="shrink"
CSS
[data-navbar="shrink"] {
  transition: padding 0.3s ease, background 0.3s ease, box-shadow 0.3s ease;
}
[data-navbar="shrink"].scrolled {
  padding-top: 0.5rem !important;
  padding-bottom: 0.5rem !important;
  background: rgba(255,255,255,0.95);
  backdrop-filter: blur(8px);
  box-shadow: 0 2px 20px rgba(0,0,0,0.06);
}
JS JavaScript
const nav = document.querySelector('[data-navbar="shrink"]');
if (nav) {
  window.addEventListener('scroll', () => {
    nav.classList.toggle('scrolled', window.scrollY > 80);
  }, { passive: true });
}
Navigation
#45 CSS + JS

Active Nav Link Highlighter

Automatically marks the nav link matching the current URL as active.

Attribute
data-nav-link
CSS
[data-nav-link].active {
  color: var(--accent, #6366f1);
  font-weight: 700;
}
JS JavaScript
document.querySelectorAll('[data-nav-link]').forEach(link => {
  if (link.href === window.location.href ||
      window.location.pathname.startsWith(new URL(link.href).pathname) && link.href !== '/') {
    link.classList.add('active');
  }
});
Navigation
#46 CSS + JS

Disable Right Click

Prevents right-click context menu on images or sections to protect content.

Attribute
data-no-rightclick
CSS
[data-no-rightclick] { user-select: none; -webkit-user-drag: none; }
JS JavaScript
document.querySelectorAll('[data-no-rightclick]').forEach(el => {
  el.addEventListener('contextmenu', e => e.preventDefault());
});
Utility
#47 CSS + JS

Auto External Link Icons

Appends an external arrow icon and target='_blank' to all links inside a container.

Attribute
data-external-links
CSS
[data-external-links] a[target="_blank"]::after {
  content: ' ↗';
  font-size: 0.8em;
  opacity: 0.5;
}
JS JavaScript
document.querySelectorAll('[data-external-links] a').forEach(link => {
  try {
    if (new URL(link.href).hostname !== window.location.hostname) {
      link.setAttribute('target', '_blank');
      link.setAttribute('rel', 'noopener noreferrer');
    }
  } catch(e) {}
});
Utility
#48 CSS Only

Print Friendly Section

Hides all other content and only prints the marked section.

Attribute
data-print-only
CSS
@media print {
  body > *:not([data-print-only]) { display: none !important; }
  [data-print-only] { display: block !important; }
}
Utility
#49 JS Only

Keyboard Shortcut Trigger

Triggers a click on the element when a keyboard key is pressed. Set key in attribute.

Attribute
data-hotkey="k"
JS JavaScript
document.querySelectorAll('[data-hotkey]').forEach(el => {
  document.addEventListener('keydown', e => {
    if ((e.ctrlKey || e.metaKey) && e.key === el.dataset.hotkey) {
      e.preventDefault();
      el.click();
    }
  });
});
Utility
#50 JS Only

Confetti Burst on Click

Fires a confetti animation burst from the clicked button position. Party time.

Attribute
data-confetti
CSS
@keyframes confettiFly {
  to { transform: translateY(-200px) rotate(720deg); opacity: 0; }
}
JS JavaScript
document.querySelectorAll('[data-confetti]').forEach(btn => {
  btn.addEventListener('click', e => {
    const colors = ['#6366f1','#ec4899','#f59e0b','#22c55e','#ef4444','#3b82f6'];
    for (let i = 0; i < 30; i++) {
      const dot = document.createElement('span');
      Object.assign(dot.style, {
        position: 'fixed',
        left: e.clientX + 'px', top: e.clientY + 'px',
        width: Math.random() * 8 + 4 + 'px',
        height: Math.random() * 8 + 4 + 'px',
        background: colors[Math.floor(Math.random() * colors.length)],
        borderRadius: Math.random() > 0.5 ? '50%' : '2px',
        pointerEvents: 'none', zIndex: 9999,
        transform: `translate(${(Math.random()-0.5)*80}px,0)`,
        animation: `confettiFly ${Math.random()*0.5+0.5}s ease-out forwards`,
      });
      document.body.appendChild(dot);
      dot.addEventListener('animationend', () => dot.remove());
    }
  });
});
Utility
#51 CSS Only

Blur In on Scroll

Elements sharpen into focus as they enter the viewport. Great for image reveals.

Attribute
data-animate="blur-in"
CSS
[data-animate="blur-in"] {
  opacity: 0;
  filter: blur(12px);
  animation: blurIn linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}
@keyframes blurIn { to { opacity: 1; filter: blur(0); } }
Animations
#52 CSS Only

Scale Up on Scroll

Elements grow from 80% to full size as they enter view.

Attribute
data-animate="scale-up"
CSS
[data-animate="scale-up"] {
  opacity: 0;
  transform: scale(0.8);
  animation: scaleUp linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}
@keyframes scaleUp { to { opacity: 1; transform: scale(1); } }
Animations
#53 CSS Only

Slide In from Left

Element sweeps in from the left side of the screen on scroll.

Attribute
data-animate="slide-left"
CSS
[data-animate="slide-left"] {
  opacity: 0;
  transform: translateX(-60px);
  animation: slideLeft linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}
@keyframes slideLeft { to { opacity: 1; transform: none; } }
Animations
#54 CSS Only

Slide In from Right

Element sweeps in from the right side on scroll.

Attribute
data-animate="slide-right"
CSS
[data-animate="slide-right"] {
  opacity: 0;
  transform: translateX(60px);
  animation: slideRight linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}
@keyframes slideRight { to { opacity: 1; transform: none; } }
Animations
#55 CSS Only

Flip In on Scroll

Element flips from flat (rotateX 90°) into full view as it enters the viewport.

Attribute
data-animate="flip-in"
CSS
[data-animate="flip-in"] {
  opacity: 0;
  transform: rotateX(90deg);
  transform-origin: top center;
  animation: flipIn linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 35%;
}
@keyframes flipIn { to { opacity: 1; transform: rotateX(0); } }
Animations
#56 CSS Only

Bounce In

Element bounces into view when it enters the viewport.

Attribute
data-animate="bounce-in"
CSS
[data-animate="bounce-in"] {
  opacity: 0;
  animation: bounceIn linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 20%;
}
@keyframes bounceIn {
  0%  { opacity: 0; transform: scale(0.3); }
  50% { opacity: 1; transform: scale(1.05); }
  70% { transform: scale(0.95); }
  100%{ transform: scale(1); }
}
Animations
#57 CSS Only

Infinite Float

Element bobs up and down continuously — good for hero images and icons.

Attribute
data-float
CSS
[data-float] {
  animation: floatLoop 3s ease-in-out infinite;
}
@keyframes floatLoop {
  0%, 100% { transform: translateY(0); }
  50%       { transform: translateY(-12px); }
}
Animations
#58 CSS Only

Spin Loader

Spins the element continuously. Use on icon spinners or loading states.

Attribute
data-spin
CSS
[data-spin] {
  animation: spinLoop 1s linear infinite;
}
@keyframes spinLoop { to { transform: rotate(360deg); } }
Animations
#59 CSS Only

Heartbeat Pulse

Element beats like a heart. Great for like/favourite icons.

Attribute
data-heartbeat
CSS
[data-heartbeat] {
  animation: heartbeat 1.2s ease-in-out infinite;
}
@keyframes heartbeat {
  0%, 100% { transform: scale(1); }
  14%       { transform: scale(1.3); }
  28%       { transform: scale(1); }
  42%       { transform: scale(1.2); }
  70%       { transform: scale(1); }
}
Animations
#60 CSS Only

Blink Attention

Blinks the element to draw attention — like a notification badge.

Attribute
data-blink
CSS
[data-blink] {
  animation: blinkAnim 1s step-end infinite;
}
@keyframes blinkAnim {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0; }
}
Animations
#61 CSS + JS

Reveal on Scroll (IntersectionObserver)

Classic JS-based scroll reveal using IntersectionObserver. Add to any element.

Attribute
data-reveal
CSS
[data-reveal] {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
[data-reveal].is-visible {
  opacity: 1;
  transform: none;
}
JS JavaScript
new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting) e.target.classList.add('is-visible');
  });
}, { threshold: 0.15 }).observe(...document.querySelectorAll('[data-reveal]'));
Scroll
#62 CSS + JS

Scroll-Linked Opacity

Element fades out as you scroll past it — like a hero vanishing into the page.

Attribute
data-scroll-fade
CSS
[data-scroll-fade] { transition: opacity 0.1s linear; }
JS JavaScript
document.querySelectorAll('[data-scroll-fade]').forEach(el => {
  window.addEventListener('scroll', () => {
    const r = el.getBoundingClientRect();
    const pct = Math.max(0, Math.min(1, 1 - r.bottom / window.innerHeight));
    el.style.opacity = 1 - pct;
  }, { passive: true });
});
Scroll
#63 CSS + JS

Section Active Class

Adds 'is-active' to nav links matching the section currently in view.

Attribute
data-section-id="about"
CSS
[data-nav-item].is-active {
  color: var(--accent, #6366f1);
  font-weight: 700;
}
JS JavaScript
const sections = document.querySelectorAll('[data-section-id]');
const navItems = document.querySelectorAll('[data-nav-item]');
new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      navItems.forEach(n => n.classList.toggle('is-active', n.dataset.navItem === e.target.dataset.sectionId));
    }
  });
}, { threshold: 0.5 }).observe(...sections);
Scroll
#64 CSS Only

Infinite Marquee

Scrolls child elements in a continuous horizontal loop — no JS required.

Attribute
data-marquee
CSS
[data-marquee] {
  overflow: hidden;
  display: flex;
}
[data-marquee] > * {
  display: flex;
  flex-shrink: 0;
  animation: marquee 20s linear infinite;
}
@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}
Scroll
#65 CSS + JS

Scroll Velocity Tilt

Element tilts based on scroll velocity — faster scroll = more tilt.

Attribute
data-velocity-tilt
CSS
[data-velocity-tilt] { transition: transform 0.3s ease; }
JS JavaScript
let lastY = 0;
document.querySelectorAll('[data-velocity-tilt]').forEach(el => {
  window.addEventListener('scroll', () => {
    const vel = window.scrollY - lastY;
    el.style.transform = `rotateX(${Math.max(-8, Math.min(8, -vel * 0.3))}deg)`;
    lastY = window.scrollY;
  }, { passive: true });
});
Scroll
#66 CSS + JS

Cursor Scale on Hover

Custom cursor grows when hovering over elements with data-cursor-grow.

Attribute
data-cursor-grow
CSS
[data-cursor="dot"] {
  position: fixed;
  width: 8px; height: 8px;
  background: currentColor;
  border-radius: 50%;
  pointer-events: none;
  z-index: 9999;
  transition: transform 0.2s ease, width 0.2s ease, height 0.2s ease;
}
body:has([data-cursor-grow]:hover) [data-cursor="dot"] {
  transform: scale(4);
}
JS JavaScript
const dot = document.querySelector('[data-cursor="dot"]');
if (dot) {
  document.addEventListener('mousemove', e => {
    dot.style.left = (e.clientX - 4) + 'px';
    dot.style.top  = (e.clientY - 4) + 'px';
  });
}
Cursor
#67 CSS + JS

Cursor Trail Particles

Leaves fading dot particles as the cursor moves across the page.

Attribute
data-cursor-trail
CSS
@keyframes trailFade { to { opacity: 0; transform: scale(0); } }
JS JavaScript
document.addEventListener('mousemove', e => {
  const dot = document.createElement('span');
  Object.assign(dot.style, {
    position: 'fixed', left: e.clientX + 'px', top: e.clientY + 'px',
    width: '6px', height: '6px', borderRadius: '50%',
    background: `hsl(${Math.random()*360},80%,60%)`,
    pointerEvents: 'none', zIndex: 9999,
    animation: 'trailFade 0.6s ease forwards',
  });
  document.body.appendChild(dot);
  dot.addEventListener('animationend', () => dot.remove());
});
Cursor
#68 JS Only

Text Cursor Override

Changes the cursor to a custom SVG icon on hover.

Attribute
data-cursor-icon="🎯"
JS JavaScript
document.querySelectorAll('[data-cursor-icon]').forEach(el => {
  const icon = el.dataset.cursorIcon;
  const svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text y='26' font-size='26'>${icon}</text></svg>`;
  el.style.cursor = `url('${svg}') 16 16, auto`;
});
Cursor
#69 CSS + JS

Glitch Effect

Text glitches with displaced colour channels on hover. Perfect for dark/cyber themes.

Attribute
data-glitch
CSS
[data-glitch] {
  position: relative;
}
[data-glitch]:hover::before,
[data-glitch]:hover::after {
  content: attr(data-glitch);
  position: absolute;
  left: 0; top: 0;
  animation: glitch 0.3s infinite;
}
[data-glitch]:hover::before { color: #f0f; clip-path: polygon(0 30%,100% 30%,100% 50%,0 50%); }
[data-glitch]:hover::after  { color: #0ff; clip-path: polygon(0 55%,100% 55%,100% 75%,0 75%); left: 2px; }
@keyframes glitch {
  0%  { transform: translateX(0); }
  20% { transform: translateX(-3px); }
  40% { transform: translateX(3px); }
  60% { transform: translateX(-2px); }
  80% { transform: translateX(2px); }
  100%{ transform: translateX(0); }
}
JS JavaScript
document.querySelectorAll('[data-glitch]').forEach(el => {
  el.setAttribute('data-glitch', el.textContent ?? '');
});
Text
#70 CSS Only

Letter Spacing Hover

Letters expand on hover for a cinematic title effect.

Attribute
data-text="expand"
CSS
[data-text="expand"] {
  letter-spacing: 0em;
  transition: letter-spacing 0.4s ease;
}
[data-text="expand"]:hover { letter-spacing: 0.3em; }
Text
#71 CSS + JS

Rotating Text Cycle

Cycles through a list of words with a fade-swap animation.

Attribute
data-rotate-text="Design,Build,Ship"
CSS
[data-rotate-text] .rt-word {
  display: inline-block;
  animation: rtFade 0.4s ease;
}
@keyframes rtFade {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
JS JavaScript
document.querySelectorAll('[data-rotate-text]').forEach(el => {
  const words = el.dataset.rotateText.split(',');
  let i = 0;
  el.innerHTML = `<span class="rt-word">${words[0]}</span>`;
  setInterval(() => {
    i = (i + 1) % words.length;
    el.innerHTML = `<span class="rt-word">${words[i]}</span>`;
  }, 2000);
});
Text
#72 CSS + JS

Character Split Hover

Each character flies apart on hover then comes back together.

Attribute
data-char-split
CSS
[data-char-split] span {
  display: inline-block;
  transition: transform 0.3s ease;
}
[data-char-split]:hover span { transform: translateY(-6px) rotate(var(--r, 0deg)); }
JS JavaScript
document.querySelectorAll('[data-char-split]').forEach(el => {
  el.innerHTML = [...el.textContent].map((c, i) =>
    `<span style="--r:${(i%2===0?1:-1)*Math.random()*15}deg">${c === ' ' ? '&nbsp;' : c}</span>`
  ).join('');
});
Text
#73 CSS Only

Balance Headings

Uses CSS text-wrap: balance for perfectly even multi-line headings.

Attribute
data-text="balance"
CSS
[data-text="balance"] {
  text-wrap: balance;
}
Text
#74 CSS Only

Masked Reveal Wipe

Text reveals with a left-to-right clip-path wipe on scroll entry.

Attribute
data-wipe
CSS
[data-wipe] {
  clip-path: inset(0 100% 0 0);
  animation: wipe linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
}
@keyframes wipe { to { clip-path: inset(0 0% 0 0); } }
Text
#75 CSS + JS

Toast Notification

Shows a temporary toast message. Set message in attribute.

Attribute
data-toast="Saved successfully!"
CSS
[data-toast-msg] {
  position: fixed;
  bottom: 2rem; left: 50%;
  transform: translateX(-50%) translateY(60px);
  background: #111;
  color: #fff;
  padding: 0.65rem 1.25rem;
  border-radius: 8px;
  font-size: 0.85rem;
  z-index: 9999;
  opacity: 0;
  transition: transform 0.3s ease, opacity 0.3s ease;
  pointer-events: none;
}
[data-toast-msg].show {
  transform: translateX(-50%) translateY(0);
  opacity: 1;
}
JS JavaScript
function showToast(msg) {
  let el = document.querySelector('[data-toast-msg]');
  if (!el) {
    el = document.createElement('div');
    el.setAttribute('data-toast-msg', '');
    document.body.appendChild(el);
  }
  el.textContent = msg;
  el.classList.add('show');
  setTimeout(() => el.classList.remove('show'), 2500);
}
document.querySelectorAll('[data-toast]').forEach(btn => {
  btn.addEventListener('click', () => showToast(btn.dataset.toast));
});
UI Patterns
#76 CSS Only

Skeleton Loader

Animated shimmer placeholder shown while content loads.

Attribute
data-skeleton
CSS
[data-skeleton] {
  background: linear-gradient(90deg, var(--border) 25%, var(--border-med) 50%, var(--border) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
  border-radius: 6px;
  color: transparent !important;
  pointer-events: none;
}
[data-skeleton] * { visibility: hidden; }
@keyframes shimmer { to { background-position: -200% 0; } }
UI Patterns
#77 CSS + JS

Draggable Element

Makes any element freely draggable with the mouse.

Attribute
data-draggable
CSS
[data-draggable] { cursor: grab; user-select: none; }
[data-draggable].dragging { cursor: grabbing; }
JS JavaScript
document.querySelectorAll('[data-draggable]').forEach(el => {
  let ox = 0, oy = 0, mx = 0, my = 0;
  el.addEventListener('mousedown', e => {
    e.preventDefault();
    ox = e.clientX - el.offsetLeft;
    oy = e.clientY - el.offsetTop;
    el.classList.add('dragging');
    document.addEventListener('mousemove', move);
    document.addEventListener('mouseup', up, { once: true });
  });
  function move(e) {
    el.style.position = 'fixed';
    el.style.left = (e.clientX - ox) + 'px';
    el.style.top  = (e.clientY - oy) + 'px';
  }
  function up() {
    el.classList.remove('dragging');
    document.removeEventListener('mousemove', move);
  }
});
UI Patterns
#78 CSS + JS

Popover on Click

Shows a small popover box anchored to the clicked element.

Attribute
data-popover="Content goes here"
CSS
[data-popover-box] {
  position: fixed;
  background: #111;
  color: #fff;
  padding: 0.5rem 0.9rem;
  border-radius: 8px;
  font-size: 0.8rem;
  z-index: 9999;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.9);
  transition: opacity 0.15s, transform 0.15s;
}
[data-popover-box].show { opacity: 1; transform: scale(1); }
JS JavaScript
let box = document.querySelector('[data-popover-box]');
if (!box) {
  box = document.createElement('div');
  box.setAttribute('data-popover-box', '');
  document.body.appendChild(box);
}
document.querySelectorAll('[data-popover]').forEach(el => {
  el.addEventListener('click', e => {
    const r = el.getBoundingClientRect();
    box.textContent = el.dataset.popover;
    box.style.left = r.left + 'px';
    box.style.top  = (r.bottom + 8) + 'px';
    box.classList.toggle('show');
  });
});
document.addEventListener('click', e => {
  if (!e.target.closest('[data-popover]')) box.classList.remove('show');
});
UI Patterns
#79 CSS + JS

Star Rating

Interactive 1–5 star rating. Stores value in data-rating on the wrapper.

Attribute
data-star-rating
CSS
[data-star-rating] { display: flex; gap: 4px; cursor: pointer; font-size: 1.5rem; }
[data-star-rating] span { color: #d1d5db; transition: color 0.15s; }
[data-star-rating] span.active { color: #facc15; }
JS JavaScript
document.querySelectorAll('[data-star-rating]').forEach(el => {
  const stars = Array.from({ length: 5 }, (_, i) => {
    const s = document.createElement('span');
    s.textContent = '★';
    s.dataset.val = String(i + 1);
    el.appendChild(s);
    return s;
  });
  stars.forEach(s => {
    s.addEventListener('click', () => {
      const v = Number(s.dataset.val);
      el.dataset.rating = String(v);
      stars.forEach((x, i) => x.classList.toggle('active', i < v));
    });
    s.addEventListener('mouseenter', () => {
      stars.forEach((x, i) => x.classList.toggle('active', i <= stars.indexOf(s)));
    });
  });
  el.addEventListener('mouseleave', () => {
    const v = Number(el.dataset.rating) || 0;
    stars.forEach((x, i) => x.classList.toggle('active', i < v));
  });
});
UI Patterns
#80 CSS + JS

Read More Toggle

Truncates text and expands on click. Set max lines with data-lines='3'.

Attribute
data-read-more data-lines="3"
CSS
[data-read-more] {
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--lines, 3);
  transition: -webkit-line-clamp 0s;
}
[data-read-more].expanded { -webkit-line-clamp: unset; }
JS JavaScript
document.querySelectorAll('[data-read-more]').forEach(el => {
  el.style.setProperty('--lines', el.dataset.lines || '3');
  const btn = document.createElement('button');
  btn.textContent = 'Read more';
  btn.style.cssText = 'display:block;margin-top:0.5rem;font-size:0.85rem;color:var(--accent,#6366f1);background:none;border:none;cursor:pointer;padding:0';
  el.after(btn);
  btn.addEventListener('click', () => {
    const open = el.classList.toggle('expanded');
    btn.textContent = open ? 'Read less' : 'Read more';
  });
});
UI Patterns
#81 CSS Only

Image Zoom on Hover

Image zooms smoothly inside its container on hover without changing layout.

Attribute
data-zoom
CSS
[data-zoom] { overflow: hidden; }
[data-zoom] img {
  transition: transform 0.4s ease;
  display: block;
  width: 100%;
}
[data-zoom]:hover img { transform: scale(1.08); }
UI Patterns
#82 CSS + JS

Before / After Slider

Drag to compare two images side by side with a resizable divider.

Attribute
data-before-after
CSS
[data-before-after] {
  position: relative;
  overflow: hidden;
  cursor: col-resize;
  user-select: none;
}
[data-before-after] .ba-after {
  position: absolute; inset: 0;
  clip-path: inset(0 0 0 50%);
  transition: clip-path 0s;
}
[data-before-after] .ba-handle {
  position: absolute; top: 0; bottom: 0;
  width: 3px; background: #fff;
  left: 50%; transform: translateX(-50%);
  cursor: col-resize;
}
JS JavaScript
document.querySelectorAll('[data-before-after]').forEach(el => {
  const after = el.querySelector('.ba-after');
  const handle = el.querySelector('.ba-handle');
  let dragging = false;
  el.addEventListener('mousedown', () => dragging = true);
  document.addEventListener('mouseup', () => dragging = false);
  document.addEventListener('mousemove', e => {
    if (!dragging) return;
    const r = el.getBoundingClientRect();
    const pct = Math.max(0, Math.min(100, ((e.clientX - r.left) / r.width) * 100));
    after.style.clipPath = `inset(0 0 0 ${pct}%)`;
    handle.style.left = pct + '%';
  });
});
UI Patterns
#83 JS Only

Infinite Scroll Trigger

Fires a custom event when a sentinel element enters view — wire to your load logic.

Attribute
data-infinite-trigger
JS JavaScript
const trigger = document.querySelector('[data-infinite-trigger]');
if (trigger) {
  new IntersectionObserver(([e]) => {
    if (e.isIntersecting) trigger.dispatchEvent(new CustomEvent('load-more', { bubbles: true }));
  }, { rootMargin: '200px' }).observe(trigger);
  trigger.addEventListener('load-more', () => {
    console.log('Load next page of content');
  });
}
UI Patterns
#84 CSS + JS

Scroll Lock Body

Prevents body scroll when this element is open — for modals and drawers.

Attribute
data-scroll-lock
CSS
body.scroll-locked { overflow: hidden; }
JS JavaScript
document.querySelectorAll('[data-scroll-lock]').forEach(el => {
  const observer = new MutationObserver(() => {
    document.body.classList.toggle('scroll-locked', el.classList.contains('is-open'));
  });
  observer.observe(el, { attributes: true, attributeFilter: ['class'] });
});
UI Patterns
#85 JS Only

Keyboard Dismiss (Escape)

Closes modals / drawers when the Escape key is pressed.

Attribute
data-escape-close
JS JavaScript
document.addEventListener('keydown', e => {
  if (e.key !== 'Escape') return;
  document.querySelectorAll('[data-escape-close].is-open').forEach(el => {
    el.classList.remove('is-open');
  });
});
UI Patterns
#86 CSS Only

Frosted Card

Blurred glass card with a subtle inner border — clean on gradient backgrounds.

Attribute
data-frosted
CSS
[data-frosted] {
  background: rgba(255,255,255,0.06);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.18);
}
Visual FX
#87 CSS Only

Aurora Background

Animated soft colour blobs in the background — like the aurora borealis.

Attribute
data-aurora
CSS
[data-aurora] {
  position: relative;
  overflow: hidden;
}
[data-aurora]::before,
[data-aurora]::after {
  content: '';
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.35;
  animation: auroraMove 8s ease-in-out infinite alternate;
}
[data-aurora]::before { width: 60%; padding-bottom: 60%; background: #6366f1; top: -20%; left: -10%; }
[data-aurora]::after  { width: 50%; padding-bottom: 50%; background: #ec4899; bottom: -20%; right: -10%; animation-delay: -4s; }
@keyframes auroraMove {
  from { transform: translate(0,0) scale(1); }
  to   { transform: translate(5%,10%) scale(1.1); }
}
Visual FX
#88 CSS Only

Mesh Gradient Background

Generates a colourful mesh gradient from a few colour stops as a background.

Attribute
data-mesh
CSS
[data-mesh] {
  background:
    radial-gradient(at 0% 0%,   #6366f1 0px, transparent 50%),
    radial-gradient(at 100% 0%,  #ec4899 0px, transparent 50%),
    radial-gradient(at 100% 100%,#f59e0b 0px, transparent 50%),
    radial-gradient(at 0% 100%,  #22c55e 0px, transparent 50%);
}
Visual FX
#89 CSS Only

Neumorphism Card

Soft neumorphic card style with inset/raised shadow on light backgrounds.

Attribute
data-neumorphic
CSS
[data-neumorphic] {
  background: #e0e5ec;
  border-radius: 16px;
  box-shadow:
    6px 6px 12px #b8bec7,
    -6px -6px 12px #ffffff;
}
Visual FX
#90 CSS + JS

SVG Filter Duotone

Applies a two-colour filter to images using an SVG feColorMatrix trick.

Attribute
data-duotone
CSS
[data-duotone] {
  filter: url(#duotone);
}
JS JavaScript
if (!document.getElementById('duotone-filter')) {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('id', 'duotone-filter');
  svg.style.cssText = 'position:absolute;width:0;height:0';
  svg.innerHTML = `<defs><filter id="duotone">
    <feColorMatrix type="saturate" values="0"/>
    <feComponentTransfer>
      <feFuncR type="table" tableValues="0.1 0.9"/>
      <feFuncG type="table" tableValues="0.05 0.3"/>
      <feFuncB type="table" tableValues="0.5 0.1"/>
    </feComponentTransfer>
  </filter></defs>`;
  document.body.prepend(svg);
}
Visual FX
#91 CSS + JS

Ripple Effect on Click

Material-style ripple that expands from the click point on any element.

Attribute
data-ripple
CSS
[data-ripple] { position: relative; overflow: hidden; }
.ripple-wave {
  position: absolute;
  border-radius: 50%;
  background: rgba(255,255,255,0.3);
  transform: scale(0);
  animation: rippleExpand 0.6s linear;
  pointer-events: none;
}
@keyframes rippleExpand {
  to { transform: scale(4); opacity: 0; }
}
JS JavaScript
document.querySelectorAll('[data-ripple]').forEach(el => {
  el.addEventListener('click', e => {
    const r = el.getBoundingClientRect();
    const size = Math.max(r.width, r.height);
    const wave = document.createElement('span');
    wave.className = 'ripple-wave';
    Object.assign(wave.style, {
      width: size + 'px', height: size + 'px',
      left: (e.clientX - r.left - size / 2) + 'px',
      top:  (e.clientY - r.top  - size / 2) + 'px',
    });
    el.appendChild(wave);
    wave.addEventListener('animationend', () => wave.remove());
  });
});
Visual FX
#92 CSS Only

Gradient Glow Shadow

Creates a glowing drop shadow that matches the element's background colour.

Attribute
data-glow-shadow
CSS
[data-glow-shadow] {
  box-shadow:
    0 0 20px -5px var(--glow-color, #6366f1),
    0 0 60px -10px var(--glow-color, #6366f1);
}
Visual FX
#93 CSS Only

CSS Neon Text

Glowing neon text using layered text-shadow.

Attribute
data-neon
CSS
[data-neon] {
  color: #fff;
  text-shadow:
    0 0 5px  #fff,
    0 0 10px #fff,
    0 0 20px var(--neon-color, #0ff),
    0 0 40px var(--neon-color, #0ff),
    0 0 80px var(--neon-color, #0ff);
}
Visual FX
#94 CSS Only

CSS Dot Grid Background

Adds a subtle dot grid pattern as a background on any section.

Attribute
data-bg="dots"
CSS
[data-bg="dots"] {
  background-image: radial-gradient(circle, rgba(0,0,0,0.12) 1px, transparent 1px);
  background-size: 24px 24px;
}
Visual FX
#95 CSS Only

CSS Line Grid Background

Adds a subtle line grid (graph paper) pattern as a background.

Attribute
data-bg="grid"
CSS
[data-bg="grid"] {
  background-image:
    linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px);
  background-size: 32px 32px;
}
Visual FX
#96 CSS Only

Auto Fill Grid

Responsive grid that auto-fills columns at a minimum width — no media queries.

Attribute
data-grid="auto"
CSS
[data-grid="auto"] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--min, 240px), 1fr));
  gap: var(--gap, 1.5rem);
}
Layout
#97 CSS Only

Centre Everything

Absolutely centres child content both horizontally and vertically.

Attribute
data-center
CSS
[data-center] {
  display: grid;
  place-items: center;
}
Layout
#98 CSS Only

Fluid Typography Scale

Font size scales fluidly between two viewport sizes with no breakpoints.

Attribute
data-fluid-type
CSS
[data-fluid-type] {
  font-size: clamp(1rem, 2.5vw + 0.5rem, 2.5rem);
  line-height: 1.2;
}
Layout
#99 CSS Only

Container Query Card

Card that changes layout based on its own width, not the viewport.

Attribute
data-cq
CSS
[data-cq] {
  container-type: inline-size;
}
@container (min-width: 400px) {
  [data-cq] .cq-inner {
    display: flex;
    gap: 1rem;
  }
}
Layout
#100 CSS Only

Sidebar + Main Layout

Classic holy grail layout: sidebar fixed-width, main fills remaining space.

Attribute
data-layout="sidebar"
CSS
[data-layout="sidebar"] {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: 2rem;
  align-items: start;
}
@media (max-width: 768px) {
  [data-layout="sidebar"] { grid-template-columns: 1fr; }
}
Layout
#101 CSS Only

Breadcrumb Auto-Separator

Auto-inserts › separators between breadcrumb items using CSS.

Attribute
data-breadcrumb
CSS
[data-breadcrumb] {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.25rem;
  font-size: 0.85rem;
}
[data-breadcrumb] a + a::before {
  content: '›';
  margin-right: 0.25rem;
  color: var(--text-faint);
}
Navigation
#102 JS Only

Mobile Swipe Close Drawer

Detects a left swipe on a drawer element and closes it.

Attribute
data-swipe-close
JS JavaScript
document.querySelectorAll('[data-swipe-close]').forEach(el => {
  let startX = 0;
  el.addEventListener('touchstart', e => { startX = e.touches[0].clientX; }, { passive: true });
  el.addEventListener('touchend', e => {
    if (startX - e.changedTouches[0].clientX > 60) el.classList.remove('is-open');
  });
});
Navigation
#103 CSS + JS

Dot Pagination Indicator

Updates active dot in a pagination strip based on the current slide index.

Attribute
data-dot-nav
CSS
[data-dot-nav] { display: flex; gap: 0.5rem; }
[data-dot-nav] button {
  width: 8px; height: 8px;
  border-radius: 50%; border: none;
  background: var(--border-med);
  transition: background 0.2s, transform 0.2s;
  cursor: pointer; padding: 0;
}
[data-dot-nav] button.active {
  background: var(--text);
  transform: scale(1.3);
}
JS JavaScript
document.querySelectorAll('[data-dot-nav]').forEach(nav => {
  const dots = nav.querySelectorAll('button');
  dots.forEach((dot, i) => {
    dot.addEventListener('click', () => {
      dots.forEach(d => d.classList.remove('active'));
      dot.classList.add('active');
      nav.dispatchEvent(new CustomEvent('dot-change', { detail: i, bubbles: true }));
    });
  });
  dots[0]?.classList.add('active');
});
Navigation
#104 CSS + JS

Input Auto-Resize Textarea

Textarea grows in height as the user types — no scrollbars.

Attribute
data-autoresize
CSS
[data-autoresize] {
  resize: none;
  overflow: hidden;
  min-height: 48px;
  transition: height 0.1s ease;
}
JS JavaScript
document.querySelectorAll('[data-autoresize]').forEach(el => {
  const resize = () => {
    el.style.height = 'auto';
    el.style.height = el.scrollHeight + 'px';
  };
  el.addEventListener('input', resize);
  resize();
});
Forms
#105 CSS Only

Input Validation State

Adds green/red border and icon based on HTML5 validity state.

Attribute
data-validate
CSS
[data-validate]:valid   { border-color: #22c55e; box-shadow: 0 0 0 2px rgba(34,197,94,0.15); }
[data-validate]:invalid { border-color: #ef4444; box-shadow: 0 0 0 2px rgba(239,68,68,0.15); }
[data-validate]:placeholder-shown { border-color: var(--border-med); box-shadow: none; }
Forms
#106 CSS + JS

Toggle Password Visibility

Eye button that toggles password input between text and password type.

Attribute
data-pwd-toggle
CSS
[data-pwd-wrap] { position: relative; }
[data-pwd-toggle] {
  position: absolute; right: 0.75rem; top: 50%;
  transform: translateY(-50%);
  background: none; border: none;
  cursor: pointer; color: var(--text-faint);
  font-size: 0.8rem;
}
JS JavaScript
document.querySelectorAll('[data-pwd-toggle]').forEach(btn => {
  const input = btn.closest('[data-pwd-wrap]')?.querySelector('input');
  if (!input) return;
  btn.addEventListener('click', () => {
    const isText = input.type === 'text';
    input.type = isText ? 'password' : 'text';
    btn.textContent = isText ? '👁' : '🙈';
  });
});
Forms
#107 CSS + JS

Numeric Stepper

Plus/minus buttons that increment or decrement a number input.

Attribute
data-stepper
CSS
[data-stepper] { display: inline-flex; align-items: center; gap: 0; border: 1px solid var(--border-med); border-radius: 8px; overflow: hidden; }
[data-stepper] button { background: var(--border); border: none; padding: 0.4rem 0.7rem; cursor: pointer; font-size: 1rem; color: var(--text); }
[data-stepper] input  { width: 3rem; text-align: center; border: none; background: none; font-size: 0.9rem; color: var(--text); }
JS JavaScript
document.querySelectorAll('[data-stepper]').forEach(el => {
  const input = el.querySelector('input');
  el.querySelectorAll('button').forEach(btn => {
    btn.addEventListener('click', () => {
      const step = Number(input.step) || 1;
      const val  = Number(input.value) || 0;
      input.value = String(btn.dataset.dir === '+' ? val + step : val - step);
    });
  });
});
Forms
#108 CSS + JS

Range Slider Live Value

Shows the current range input value in a live tooltip above the thumb.

Attribute
data-range-live
CSS
[data-range-wrap] { position: relative; }
[data-range-output] {
  position: absolute;
  bottom: calc(100% + 4px);
  left: var(--pct, 50%);
  transform: translateX(-50%);
  background: #111;
  color: #fff;
  font-size: 0.7rem;
  padding: 0.15rem 0.4rem;
  border-radius: 4px;
  pointer-events: none;
}
JS JavaScript
document.querySelectorAll('[data-range-live]').forEach(input => {
  const wrap = input.closest('[data-range-wrap]') || input.parentElement;
  const out = wrap?.querySelector('[data-range-output]');
  const update = () => {
    const pct = ((Number(input.value) - Number(input.min)) / (Number(input.max) - Number(input.min))) * 100;
    if (out) { out.textContent = input.value; out.style.setProperty('--pct', pct + '%'); }
  };
  input.addEventListener('input', update);
  update();
});
Forms
#109 CSS + JS

Debounced Search Filter

Filters a list of items in real time as the user types in a search box.

Attribute
data-search-filter="#list-id"
CSS
[data-filter-item].hidden { display: none; }
JS JavaScript
document.querySelectorAll('[data-search-filter]').forEach(input => {
  const list = document.querySelector(input.dataset.searchFilter);
  if (!list) return;
  let timer;
  input.addEventListener('input', () => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      const q = input.value.toLowerCase();
      list.querySelectorAll('[data-filter-item]').forEach(item => {
        item.classList.toggle('hidden', !item.textContent.toLowerCase().includes(q));
      });
    }, 200);
  });
});
Utility
#110 JS Only

Share via Web Share API

Uses the native share sheet on mobile to share the page URL.

Attribute
data-share
JS JavaScript
document.querySelectorAll('[data-share]').forEach(btn => {
  if (!navigator.share) { btn.style.display = 'none'; return; }
  btn.addEventListener('click', () => {
    navigator.share({
      title: document.title,
      url: location.href,
    }).catch(() => {});
  });
});
Utility
#111 CSS + JS

Auto Anchor Headings

Adds a clickable # link to every heading inside the marked container.

Attribute
data-anchor-headings
CSS
[data-anchor-headings] :is(h1,h2,h3,h4) .anchor {
  opacity: 0;
  margin-left: 0.5rem;
  text-decoration: none;
  color: var(--text-faint);
  font-size: 0.85em;
  transition: opacity 0.2s;
}
[data-anchor-headings] :is(h1,h2,h3,h4):hover .anchor { opacity: 1; }
JS JavaScript
document.querySelectorAll('[data-anchor-headings]').forEach(el => {
  el.querySelectorAll('h1,h2,h3,h4').forEach(h => {
    if (!h.id) h.id = h.textContent.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
    const a = document.createElement('a');
    a.className = 'anchor';
    a.href = '#' + h.id;
    a.textContent = '#';
    h.appendChild(a);
  });
});
Utility
#112 JS Only

Timestamp to Relative Time

Converts ISO date strings to relative text like '3 hours ago'.

Attribute
data-relative-time
JS JavaScript
function relTime(date) {
  const diff = (Date.now() - new Date(date).getTime()) / 1000;
  const units = [['year',31536000],['month',2592000],['day',86400],['hour',3600],['minute',60],['second',1]];
  for (const [name, sec] of units) {
    const n = Math.floor(diff / sec);
    if (n >= 1) return `${n} ${name}${n > 1 ? 's' : ''} ago`;
  }
  return 'just now';
}
document.querySelectorAll('[data-relative-time]').forEach(el => {
  el.textContent = relTime(el.getAttribute('datetime') || el.textContent);
});
Utility
#113 CSS + JS

Prevent Double Submit

Disables a form's submit button after the first click to prevent duplicate submissions.

Attribute
data-once
CSS
[data-once][disabled] { opacity: 0.5; cursor: not-allowed; }
JS JavaScript
document.querySelectorAll('form:has([data-once])').forEach(form => {
  form.addEventListener('submit', () => {
    form.querySelectorAll('[data-once]').forEach(btn => {
      btn.disabled = true;
      btn.textContent = btn.dataset.loadingText || 'Submitting…';
    });
  });
});
Utility
#114 JS Only

Element Resize Observer

Fires a custom event when an element's size changes — useful for responsive logic.

Attribute
data-resize-watch
JS JavaScript
document.querySelectorAll('[data-resize-watch]').forEach(el => {
  new ResizeObserver(([entry]) => {
    el.dispatchEvent(new CustomEvent('resized', {
      detail: { width: entry.contentRect.width, height: entry.contentRect.height },
      bubbles: true,
    }));
  }).observe(el);
  el.addEventListener('resized', e => console.log('Resized:', e.detail));
});
Utility
#115 JS Only

Print Button

Triggers the browser print dialog when clicked.

Attribute
data-print
JS JavaScript
document.querySelectorAll('[data-print]').forEach(btn => {
  btn.addEventListener('click', () => window.print());
});
Utility
#116 JS Only

Idle Timeout Warning

Shows a warning after N seconds of inactivity. Set seconds in attribute.

Attribute
data-idle="30"
JS JavaScript
document.querySelectorAll('[data-idle]').forEach(el => {
  const secs = parseInt(el.dataset.idle, 10) * 1000;
  let timer;
  const reset = () => {
    clearTimeout(timer);
    timer = setTimeout(() => el.dispatchEvent(new CustomEvent('idle', { bubbles: true })), secs);
  };
  ['mousemove','keydown','scroll','touchstart'].forEach(ev => document.addEventListener(ev, reset, { passive: true }));
  el.addEventListener('idle', () => alert('Are you still there?'));
  reset();
});
Utility
#117 JS Only

Redirect After Delay

Redirects to a URL after N seconds. Good for thank-you pages.

Attribute
data-redirect="/home" data-delay="3"
JS JavaScript
document.querySelectorAll('[data-redirect]').forEach(el => {
  const delay = (parseFloat(el.dataset.delay) || 3) * 1000;
  setTimeout(() => location.href = el.dataset.redirect, delay);
});
Utility
#118 CSS + JS

Detect Online / Offline

Adds/removes an 'is-offline' class on the body when connectivity changes.

Attribute
data-connectivity
CSS
body.is-offline [data-connectivity]::after {
  content: 'You are offline';
  display: block;
  background: #ef4444;
  color: #fff;
  text-align: center;
  padding: 0.5rem;
  font-size: 0.85rem;
}
JS JavaScript
window.addEventListener('offline', () => document.body.classList.add('is-offline'));
window.addEventListener('online',  () => document.body.classList.remove('is-offline'));
Utility
#119 JS Only

Locale Number Format

Formats numbers inside elements with locale-aware separators (1,000,000).

Attribute
data-format-number
JS JavaScript
document.querySelectorAll('[data-format-number]').forEach(el => {
  const n = parseFloat(el.textContent.replace(/[^0-9.-]/g, ''));
  if (!isNaN(n)) el.textContent = n.toLocaleString();
});
Utility
#120 JS Only

Focus Trap

Traps keyboard focus inside a modal or drawer when it's open.

Attribute
data-focus-trap
JS JavaScript
document.querySelectorAll('[data-focus-trap]').forEach(el => {
  el.addEventListener('keydown', e => {
    if (e.key !== 'Tab') return;
    const focusable = [...el.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])')].filter(f => !f.disabled);
    const first = focusable[0], last = focusable[focusable.length - 1];
    if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
    else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
  });
});
Utility
#121 JS Only

Preload on Hover

Prefetches a linked page when the user hovers over a link, reducing navigation lag.

Attribute
data-prefetch
JS JavaScript
document.querySelectorAll('a[data-prefetch]').forEach(link => {
  link.addEventListener('mouseenter', () => {
    const rel = document.createElement('link');
    rel.rel = 'prefetch';
    rel.href = link.href;
    document.head.appendChild(rel);
  }, { once: true });
});
Performance
#122 JS Only

Defer Non-Critical Script

Loads a third-party script tag only after the page is idle using requestIdleCallback.

Attribute
data-defer-src="https://example.com/script.js"
JS JavaScript
document.querySelectorAll('[data-defer-src]').forEach(el => {
  const load = () => {
    const s = document.createElement('script');
    s.src = el.dataset.deferSrc;
    document.body.appendChild(s);
  };
  'requestIdleCallback' in window ? requestIdleCallback(load) : setTimeout(load, 2000);
});
Performance
#123 CSS Only

Will-Change Hint

Tells the browser to promote an element to its own layer before animating.

Attribute
data-will-change="transform"
CSS
[data-will-change] { will-change: var(--wc, transform); }
[data-will-change="opacity"]   { --wc: opacity; }
[data-will-change="transform"] { --wc: transform; }
[data-will-change="scroll"]    { --wc: scroll-position; }
Performance
#124 CSS Only

Content Visibility Auto

Skips rendering off-screen sections until they're needed — big perf win for long pages.

Attribute
data-cv-auto
CSS
[data-cv-auto] {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;
}
Performance
#125 JS Only

Resource Hint DNS Prefetch

Injects a DNS-prefetch hint for a third-party domain to speed up connections.

Attribute
data-dns-prefetch="fonts.googleapis.com"
JS JavaScript
document.querySelectorAll('[data-dns-prefetch]').forEach(el => {
  const link = document.createElement('link');
  link.rel = 'dns-prefetch';
  link.href = '//' + el.dataset.dnsPrefetch;
  document.head.appendChild(link);
});
Performance
#126 CSS Only

Skip to Content Link

Hidden link that appears on Tab press, allowing keyboard users to skip navigation.

Attribute
data-skip-link
CSS
[data-skip-link] {
  position: fixed;
  top: -100%;
  left: 1rem;
  background: var(--text);
  color: var(--bg);
  padding: 0.5rem 1rem;
  border-radius: 0 0 6px 6px;
  font-size: 0.85rem;
  z-index: 9999;
  transition: top 0.2s ease;
  text-decoration: none;
}
[data-skip-link]:focus { top: 0; }
Accessibility
#127 CSS + JS

Announce Live Region

Pushes a message to screen readers via an ARIA live region.

Attribute
data-announce
CSS
[data-live-region] {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
}
JS JavaScript
let liveEl = document.querySelector('[data-live-region]');
if (!liveEl) {
  liveEl = document.createElement('div');
  liveEl.setAttribute('data-live-region', '');
  liveEl.setAttribute('aria-live', 'polite');
  liveEl.setAttribute('aria-atomic', 'true');
  document.body.appendChild(liveEl);
}
document.querySelectorAll('[data-announce]').forEach(el => {
  el.addEventListener('click', () => {
    liveEl.textContent = '';
    requestAnimationFrame(() => { liveEl.textContent = el.dataset.announce; });
  });
});
Accessibility
#128 CSS Only

Reduced Motion Aware

Disables all animations for users who prefer reduced motion at the system level.

Attribute
data-motion-safe
CSS
@media (prefers-reduced-motion: reduce) {
  [data-motion-safe] *,
  [data-motion-safe] *::before,
  [data-motion-safe] *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
Accessibility
#129 CSS Only

High Contrast Mode

Switches a section to high contrast colours for better readability.

Attribute
data-high-contrast
CSS
[data-high-contrast] {
  --text: #000;
  --bg: #fff;
  --text-muted: #000;
  --border: #000;
  background: #fff;
  color: #000;
}
html[data-theme="dark"] [data-high-contrast] {
  --text: #fff;
  --bg: #000;
  background: #000;
  color: #fff;
}
Accessibility
#130 JS Only

Tab Index Manager

Removes elements from tab order when a panel is hidden, restores on open.

Attribute
data-tab-hidden
JS JavaScript
document.querySelectorAll('[data-tab-hidden]').forEach(el => {
  const items = el.querySelectorAll('a,button,input,select,textarea,[tabindex]');
  const apply = () => {
    const hidden = !el.classList.contains('is-open');
    items.forEach(item => item.setAttribute('tabindex', hidden ? '-1' : '0'));
  };
  apply();
  new MutationObserver(apply).observe(el, { attributes: true, attributeFilter: ['class'] });
});
Accessibility
#131 JS Only

Autoplay Video on Scroll

Plays a video when it enters the viewport and pauses when it leaves.

Attribute
data-autoplay-scroll
JS JavaScript
document.querySelectorAll('video[data-autoplay-scroll]').forEach(video => {
  new IntersectionObserver(([e]) => {
    e.isIntersecting ? video.play() : video.pause();
  }, { threshold: 0.4 }).observe(video);
});
Media
#132 JS Only

Mute Toggle Button

Toggles mute on the nearest video element.

Attribute
data-mute-toggle
JS JavaScript
document.querySelectorAll('[data-mute-toggle]').forEach(btn => {
  const video = btn.closest('[data-video-wrap]')?.querySelector('video') || document.querySelector('video');
  if (!video) return;
  btn.addEventListener('click', () => {
    video.muted = !video.muted;
    btn.textContent = video.muted ? '🔇' : '🔊';
  });
});
Media
#133 CSS + JS

Image Colour Palette Extractor

Extracts the dominant colour from an image using canvas and sets it as a CSS var.

Attribute
data-extract-color
CSS
[data-extract-color] { --dominant: #888; }
JS JavaScript
document.querySelectorAll('img[data-extract-color]').forEach(img => {
  img.crossOrigin = 'anonymous';
  img.addEventListener('load', () => {
    const c = document.createElement('canvas');
    c.width = c.height = 1;
    c.getContext('2d').drawImage(img, 0, 0, 1, 1);
    const [r,g,b] = c.getContext('2d').getImageData(0,0,1,1).data;
    img.style.setProperty('--dominant', `rgb(${r},${g},${b})`);
  }, { once: true });
  if (img.complete) img.dispatchEvent(new Event('load'));
});
Media
#134 CSS + JS

Lightbox Image Viewer

Opens images in a full-screen overlay when clicked.

Attribute
data-lightbox
CSS
#wm-lightbox {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.9);
  z-index: 9999;
  display: none;
  align-items: center;
  justify-content: center;
  cursor: zoom-out;
}
#wm-lightbox.open { display: flex; }
#wm-lightbox img  { max-width: 90vw; max-height: 90vh; border-radius: 4px; }
JS JavaScript
let lb = document.getElementById('wm-lightbox');
if (!lb) {
  lb = document.createElement('div');
  lb.id = 'wm-lightbox';
  lb.innerHTML = '<img>';
  document.body.appendChild(lb);
  lb.addEventListener('click', () => lb.classList.remove('open'));
}
document.querySelectorAll('img[data-lightbox]').forEach(img => {
  img.style.cursor = 'zoom-in';
  img.addEventListener('click', () => {
    lb.querySelector('img').src = img.src;
    lb.classList.add('open');
  });
});
Media
#135 CSS + JS

Long Press Action

Fires a custom 'longpress' event after 500ms of holding down the mouse or touch.

Attribute
data-long-press
CSS
[data-long-press] { user-select: none; }
JS JavaScript
document.querySelectorAll('[data-long-press]').forEach(el => {
  let timer;
  const start = () => { timer = setTimeout(() => el.dispatchEvent(new CustomEvent('longpress', { bubbles: true })), 500); };
  const cancel = () => clearTimeout(timer);
  el.addEventListener('mousedown', start);
  el.addEventListener('touchstart', start, { passive: true });
  el.addEventListener('mouseup', cancel);
  el.addEventListener('touchend', cancel);
  el.addEventListener('longpress', () => console.log('Long press!'));
});
Interaction
#136 CSS + JS

Double Click Action

Fires a custom event on double click — useful for like/favourite interactions.

Attribute
data-dblclick-like
CSS
[data-dblclick-like] .like-pop {
  position: absolute;
  font-size: 2rem;
  pointer-events: none;
  animation: popUp 0.6s ease forwards;
}
@keyframes popUp {
  from { transform: scale(0) translateY(0); opacity: 1; }
  to   { transform: scale(1.5) translateY(-60px); opacity: 0; }
}
JS JavaScript
document.querySelectorAll('[data-dblclick-like]').forEach(el => {
  el.style.position = 'relative';
  el.addEventListener('dblclick', e => {
    const pop = document.createElement('span');
    pop.className = 'like-pop';
    pop.textContent = '❤️';
    pop.style.cssText = `left:${e.offsetX - 15}px;top:${e.offsetY - 15}px`;
    el.appendChild(pop);
    pop.addEventListener('animationend', () => pop.remove());
  });
});
Interaction
#137 JS Only

Swipe Left / Right Detection

Detects horizontal swipe gestures and fires 'swipe-left' or 'swipe-right' events.

Attribute
data-swipe
JS JavaScript
document.querySelectorAll('[data-swipe]').forEach(el => {
  let sx = 0, sy = 0;
  el.addEventListener('touchstart', e => { sx = e.touches[0].clientX; sy = e.touches[0].clientY; }, { passive: true });
  el.addEventListener('touchend', e => {
    const dx = e.changedTouches[0].clientX - sx;
    const dy = e.changedTouches[0].clientY - sy;
    if (Math.abs(dx) < 30 || Math.abs(dx) < Math.abs(dy)) return;
    el.dispatchEvent(new CustomEvent(dx > 0 ? 'swipe-right' : 'swipe-left', { bubbles: true }));
  });
  el.addEventListener('swipe-left',  () => console.log('Swiped left'));
  el.addEventListener('swipe-right', () => console.log('Swiped right'));
});
Interaction
#138 CSS + JS

Hover Intent (delayed hover)

Only triggers hover state after the mouse has stayed still for 200ms — prevents accidental triggers.

Attribute
data-hover-intent
CSS
[data-hover-intent] .intent-content {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
[data-hover-intent].hovered .intent-content {
  opacity: 1;
  pointer-events: auto;
}
JS JavaScript
document.querySelectorAll('[data-hover-intent]').forEach(el => {
  let timer;
  el.addEventListener('mouseenter', () => { timer = setTimeout(() => el.classList.add('hovered'), 200); });
  el.addEventListener('mouseleave', () => { clearTimeout(timer); el.classList.remove('hovered'); });
});
Interaction
#139 CSS + JS

Pinch Zoom Container

Enables pinch-to-zoom on a container using touch events.

Attribute
data-pinch-zoom
CSS
[data-pinch-zoom] { touch-action: none; overflow: hidden; }
[data-pinch-zoom] > * { transform-origin: center center; transition: transform 0.1s ease; }
JS JavaScript
document.querySelectorAll('[data-pinch-zoom]').forEach(el => {
  const inner = el.firstElementChild;
  let lastDist = 0, scale = 1;
  el.addEventListener('touchmove', e => {
    if (e.touches.length !== 2) return;
    e.preventDefault();
    const dx = e.touches[0].clientX - e.touches[1].clientX;
    const dy = e.touches[0].clientY - e.touches[1].clientY;
    const dist = Math.hypot(dx, dy);
    if (lastDist) scale = Math.max(1, Math.min(4, scale * (dist / lastDist)));
    inner.style.transform = `scale(${scale})`;
    lastDist = dist;
  }, { passive: false });
  el.addEventListener('touchend', () => { lastDist = 0; });
});
Interaction
#140 CSS + JS

Drag to Scroll

Click and drag to scroll a container horizontally — carousel without a library.

Attribute
data-drag-scroll
CSS
[data-drag-scroll] { cursor: grab; overflow-x: auto; scrollbar-width: none; }
[data-drag-scroll]::-webkit-scrollbar { display: none; }
[data-drag-scroll].active { cursor: grabbing; user-select: none; }
JS JavaScript
document.querySelectorAll('[data-drag-scroll]').forEach(el => {
  let down = false, startX, scrollLeft;
  el.addEventListener('mousedown', e => {
    down = true; el.classList.add('active');
    startX = e.pageX - el.offsetLeft;
    scrollLeft = el.scrollLeft;
  });
  document.addEventListener('mouseup', () => { down = false; el.classList.remove('active'); });
  el.addEventListener('mousemove', e => {
    if (!down) return;
    el.scrollLeft = scrollLeft - (e.pageX - el.offsetLeft - startX) * 1.5;
  });
});
Interaction
#141 JS Only

CSS Custom Property Setter

Reads a data attribute value and sets it as a CSS custom property.

Attribute
data-css-var="--accent:#6366f1"
JS JavaScript
document.querySelectorAll('[data-css-var]').forEach(el => {
  el.dataset.cssVar.split(';').forEach(pair => {
    const [prop, val] = pair.split(':');
    if (prop && val) el.style.setProperty(prop.trim(), val.trim());
  });
});
Color & Theme
#142 CSS + JS

Random Accent on Load

Picks a random accent colour from a palette on each page load.

Attribute
data-random-accent
CSS
[data-random-accent] { color: var(--accent-random, #6366f1); }
JS JavaScript
const palette = ['#6366f1','#ec4899','#f59e0b','#22c55e','#3b82f6','#ef4444','#8b5cf6'];
const pick = palette[Math.floor(Math.random() * palette.length)];
document.querySelectorAll('[data-random-accent]').forEach(el => {
  el.style.setProperty('--accent-random', pick);
});
Color & Theme
#143 JS Only

System Colour Scheme Class

Adds 'prefers-dark' or 'prefers-light' class to the root based on OS setting.

Attribute
data-sys-theme
JS JavaScript
const dark = window.matchMedia('(prefers-color-scheme: dark)');
document.documentElement.classList.toggle('prefers-dark', dark.matches);
document.documentElement.classList.toggle('prefers-light', !dark.matches);
dark.addEventListener('change', e => {
  document.documentElement.classList.toggle('prefers-dark', e.matches);
  document.documentElement.classList.toggle('prefers-light', !e.matches);
});
Color & Theme
#144 CSS + JS

Colour Swatch Picker

Clicking a swatch sets a CSS variable to that colour — live theme switching.

Attribute
data-swatch="#6366f1"
CSS
[data-swatch] {
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--sw);
  cursor: pointer;
  border: 2px solid transparent;
  transition: border-color 0.15s;
}
[data-swatch].active { border-color: var(--text); }
JS JavaScript
document.querySelectorAll('[data-swatch]').forEach(el => {
  el.style.setProperty('--sw', el.dataset.swatch);
  el.addEventListener('click', () => {
    document.documentElement.style.setProperty('--accent', el.dataset.swatch);
    document.querySelectorAll('[data-swatch]').forEach(s => s.classList.remove('active'));
    el.classList.add('active');
  });
});
Color & Theme
#145 CSS Only

Invert On Dark Mode

Inverts a specific element (e.g. a logo) only when dark mode is active.

Attribute
data-invert-dark
CSS
html[data-theme="dark"] [data-invert-dark] {
  filter: invert(1);
}
Color & Theme
#146 JS Only

Obfuscate Email

Builds a mailto link from data attributes to avoid email harvesting bots.

Attribute
data-email-user="hello" data-email-domain="site.com"
JS JavaScript
document.querySelectorAll('[data-email-user]').forEach(el => {
  const email = el.dataset.emailUser + '@' + el.dataset.emailDomain;
  el.href = 'mailto:' + email;
  if (!el.textContent.trim()) el.textContent = email;
});
Misc
#147 CSS + JS

Cookie Consent Banner

Shows a banner until the user accepts. Stores consent in localStorage.

Attribute
data-cookie-banner
CSS
[data-cookie-banner] {
  position: fixed; bottom: 1rem; left: 1rem; right: 1rem;
  max-width: 480px; margin: 0 auto;
  background: #111; color: #fff;
  border-radius: 12px; padding: 1rem 1.25rem;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  font-size: 0.83rem; z-index: 9999;
}
[data-cookie-banner].hidden { display: none; }
JS JavaScript
document.querySelectorAll('[data-cookie-banner]').forEach(el => {
  if (localStorage.getItem('cookie-consent')) { el.classList.add('hidden'); return; }
  el.querySelector('[data-accept]')?.addEventListener('click', () => {
    localStorage.setItem('cookie-consent', '1');
    el.classList.add('hidden');
  });
});
Misc
#148 CSS + JS

Survey NPS Widget

Renders a 0–10 NPS scale and logs the selected score.

Attribute
data-nps
CSS
[data-nps] { display: flex; gap: 4px; flex-wrap: wrap; }
[data-nps] button {
  width: 36px; height: 36px;
  border-radius: 50%; border: 1px solid var(--border-med);
  background: none; font-size: 0.8rem; cursor: pointer; color: var(--text);
  transition: background 0.15s, color 0.15s;
}
[data-nps] button.selected { background: var(--text); color: var(--bg); }
JS JavaScript
document.querySelectorAll('[data-nps]').forEach(el => {
  for (let i = 0; i <= 10; i++) {
    const b = document.createElement('button');
    b.textContent = String(i);
    b.addEventListener('click', () => {
      el.querySelectorAll('button').forEach(x => x.classList.remove('selected'));
      b.classList.add('selected');
      el.dispatchEvent(new CustomEvent('nps-score', { detail: i, bubbles: true }));
      console.log('NPS Score:', i);
    });
    el.appendChild(b);
  }
});
Misc
#149 CSS + JS

Emoji Reaction Bar

Adds a row of emoji reaction buttons with live increment counters.

Attribute
data-reactions="👍,❤️,🔥,🎉"
CSS
[data-reactions] { display: flex; gap: 0.4rem; flex-wrap: wrap; }
[data-reactions] button {
  display: flex; align-items: center; gap: 0.3rem;
  border: 1px solid var(--border); border-radius: 999px;
  background: none; padding: 0.3rem 0.75rem;
  font-size: 0.85rem; cursor: pointer; color: var(--text);
  transition: background 0.15s, border-color 0.15s;
}
[data-reactions] button:hover { background: var(--border); }
[data-reactions] button.reacted { border-color: var(--accent, #6366f1); background: rgba(99,102,241,0.1); }
JS JavaScript
document.querySelectorAll('[data-reactions]').forEach(el => {
  el.dataset.reactions.split(',').forEach(emoji => {
    let count = 0;
    const btn = document.createElement('button');
    const span = document.createElement('span');
    span.textContent = '0';
    btn.append(emoji, ' ', span);
    btn.addEventListener('click', () => {
      count = btn.classList.toggle('reacted') ? count + 1 : count - 1;
      span.textContent = String(count);
    });
    el.appendChild(btn);
  });
});
Misc
#150 JS Only

Festive Snow Effect

Generates falling snowflakes across the viewport — great for seasonal pages.

Attribute
data-snow
CSS
@keyframes snowFall {
  to { transform: translateY(110vh) rotate(360deg); opacity: 0; }
}
JS JavaScript
if (document.querySelector('[data-snow]')) {
  for (let i = 0; i < 60; i++) {
    const flake = document.createElement('span');
    const size = Math.random() * 10 + 4;
    Object.assign(flake.style, {
      position: 'fixed',
      top: '-10px',
      left: Math.random() * 100 + 'vw',
      width: size + 'px',
      height: size + 'px',
      borderRadius: '50%',
      background: '#fff',
      opacity: Math.random() * 0.7 + 0.3,
      pointerEvents: 'none',
      zIndex: 9998,
      animation: `snowFall ${Math.random() * 4 + 4}s linear ${Math.random() * 6}s infinite`,
    });
    document.body.appendChild(flake);
  }
}
Misc

How to use in Webflow

  1. Copy the attribute — click the code chip on any card to copy it.
  2. Add to your element — select the element in Webflow, open the Element Settings panel (D key), scroll to Custom Attributes and add it.
  3. Add the CSS — paste it into a Page Settings → Custom Code → <head> block, wrapped in <style> tags.
  4. Add the JS — paste it into Page Settings → Custom Code → </body> block, wrapped in <script> tags.
  5. Publish and test — custom attributes only work in the live published site, not the designer preview.
Start a project