Scroll revealsCC0-style snippets
Staggered list
A calm sequence for cards or list items, with the delay controlled by a CSS custom property.
[data-sc-effect="stagger"] [data-sc-stagger-item] {
opacity: 0;
transform: translateY(12px);
transition: opacity 450ms ease, transform 450ms ease;
transition-delay: calc(var(--sc-index, 0) * 70ms);
}
[data-sc-effect="stagger"].is-visible [data-sc-stagger-item] {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="stagger"] [data-sc-stagger-item] { opacity: 1; transform: none; transition: none; }
}
const staggerObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(({isIntersecting, target}) => {
if (!isIntersecting) return;
target.querySelectorAll('[data-sc-stagger-item]').forEach((item, index) => {
item.style.setProperty('--sc-index', index);
});
target.classList.add('is-visible');
observer.unobserve(target);
});
}, { threshold: 0.12 });
document.querySelectorAll('[data-sc-effect="stagger"]').forEach((el) => staggerObserver.observe(el));
Micro-interactionsCC0-style snippets
Spotlight hover
A pointer-following radial highlight that adds depth without a heavy visual layer.
[data-sc-effect="spotlight"] {
--sc-x: 50%;
--sc-y: 50%;
background: radial-gradient(circle at var(--sc-x) var(--sc-y), rgba(116, 125, 255, .22), transparent 34%), #141822;
transition: background 180ms ease, transform 180ms ease;
}
[data-sc-effect="spotlight"]:hover { transform: translateY(-2px); }
document.querySelectorAll('[data-sc-effect="spotlight"]').forEach((card) => {
card.addEventListener('pointermove', (event) => {
const box = card.getBoundingClientRect();
card.style.setProperty('--sc-x', `${event.clientX - box.left}px`);
card.style.setProperty('--sc-y', `${event.clientY - box.top}px`);
}, { passive: true });
});
Micro-interactionsCC0-style snippets
Magnetic button
A restrained pointer response for a primary CTA that remains keyboard and touch friendly.
[data-sc-effect="magnetic"] {
transition: transform 180ms cubic-bezier(.2, .8, .2, 1);
will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="magnetic"] { transition: none; }
}
document.querySelectorAll('[data-sc-effect="magnetic"]').forEach((button) => {
button.addEventListener('pointermove', (event) => {
const box = button.getBoundingClientRect();
const x = (event.clientX - (box.left + box.width / 2)) * .12;
const y = (event.clientY - (box.top + box.height / 2)) * .12;
button.style.transform = `translate(${x}px, ${y}px)`;
}, { passive: true });
button.addEventListener('pointerleave', () => { button.style.transform = ''; });
});
NavigationCC0-style snippets
Scroll progress line
A single fixed progress line that gives long-form pages a clear sense of place.
<div data-sc-effect="scroll-progress" aria-hidden="true"></div>
[data-sc-effect="scroll-progress"] {
position: fixed; inset: 0 auto auto 0; z-index: 10;
width: 100%; height: 3px; transform: scaleX(0); transform-origin: left;
background: #5b5ce2; pointer-events: none;
}
const progress = document.querySelector('[data-sc-effect="scroll-progress"]');
let progressTicking = false;
window.addEventListener('scroll', () => {
if (progressTicking) return;
progressTicking = true;
requestAnimationFrame(() => {
const max = document.documentElement.scrollHeight - window.innerHeight;
progress.style.transform = `scaleX(${max > 0 ? window.scrollY / max : 0})`;
progressTicking = false;
});
}, { passive: true });
BackgroundsCC0-style snippets
Cursor glow
A soft ambient glow for a hero or dark section that never blocks content or clicks.
[data-sc-effect="cursor-glow"] {
--sc-x: 50%; --sc-y: 50%;
background: radial-gradient(420px circle at var(--sc-x) var(--sc-y), rgba(91, 92, 226, .3), transparent 70%), #111318;
}
document.querySelectorAll('[data-sc-effect="cursor-glow"]').forEach((section) => {
section.addEventListener('pointermove', (event) => {
const box = section.getBoundingClientRect();
section.style.setProperty('--sc-x', `${event.clientX - box.left}px`);
section.style.setProperty('--sc-y', `${event.clientY - box.top}px`);
}, { passive: true });
});
BackgroundsCC0-style snippets
Animated gradient mesh
A low-frequency CSS background motion that gives a hero a little life without adding an image asset.
[data-sc-effect="gradient-mesh"] { position: relative; isolation: isolate; overflow: hidden; background: #111318; }
[data-sc-effect="gradient-mesh"]::before {
content: ""; position: absolute; inset: -35%; z-index: -1;
background: radial-gradient(circle at 20% 30%, #5b5ce2 0 12%, transparent 38%), radial-gradient(circle at 80% 70%, #9db7ff 0 8%, transparent 34%);
filter: blur(30px); opacity: .38; animation: sc-mesh 16s ease-in-out infinite alternate;
}
@keyframes sc-mesh { to { transform: translate3d(4%, -3%, 0) scale(1.08); } }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="gradient-mesh"]::before { animation: none; } }
Text motionCC0-style snippets
Split text reveal
A small line-by-line headline reveal that avoids a runtime text-splitting dependency.
[data-sc-effect="split-text"] [data-sc-line] {
display: block; clip-path: inset(0 0 100% 0); transform: translateY(1em);
transition: clip-path 700ms cubic-bezier(.2, .8, .2, 1), transform 700ms cubic-bezier(.2, .8, .2, 1);
transition-delay: calc(var(--sc-index, 0) * 90ms);
}
[data-sc-effect="split-text"].is-visible [data-sc-line] { clip-path: inset(0); transform: none; }
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="split-text"] [data-sc-line] { clip-path: inset(0); transform: none; transition: none; }
}
document.querySelectorAll('[data-sc-effect="split-text"]').forEach((heading) => {
heading.querySelectorAll('[data-sc-line]').forEach((line, index) => line.style.setProperty('--sc-index', index));
requestAnimationFrame(() => heading.classList.add('is-visible'));
});
Micro-interactionsCC0-style snippets
Hover lift card
A subtle elevation cue for clickable cards that keeps the hit area stable and the focus state visible.
[data-sc-effect="hover-lift"] {
transition: transform 180ms ease, box-shadow 180ms ease;
}
[data-sc-effect="hover-lift"]:hover,
[data-sc-effect="hover-lift"]:focus-visible {
transform: translateY(-5px);
box-shadow: 0 16px 30px rgba(17, 19, 24, .14);
}
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="hover-lift"] { transition: none; }
}
Micro-interactionsCC0-style snippets
Button ripple
A contained click ripple that confirms activation without replacing the button label or focus ring.
[data-sc-effect="ripple"] { position: relative; overflow: hidden; isolation: isolate; }
[data-sc-effect="ripple"] .sc-ripple {
position: absolute; width: 12px; aspect-ratio: 1; border-radius: 50%;
background: currentColor; opacity: .22; transform: scale(0);
animation: sc-ripple 550ms ease-out forwards; pointer-events: none;
}
@keyframes sc-ripple { to { opacity: 0; transform: scale(18); } }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="ripple"] .sc-ripple { display: none; } }
document.querySelectorAll('[data-sc-effect="ripple"]').forEach((button) => {
button.addEventListener('click', (event) => {
const box = button.getBoundingClientRect();
const ripple = document.createElement('span');
ripple.className = 'sc-ripple';
ripple.style.left = `${event.clientX - box.left - 6}px`;
ripple.style.top = `${event.clientY - box.top - 6}px`;
button.append(ripple);
ripple.addEventListener('animationend', () => ripple.remove(), { once: true });
});
});
ComponentsCC0-style snippets
Accordion disclosure
An accessible disclosure pattern with native button semantics and a restrained height transition.
<button type="button" aria-expanded="false" aria-controls="answer-1">What is included?</button>
<div id="answer-1" hidden>Short, useful supporting content.</div>
const trigger = document.querySelector('[aria-controls="answer-1"]');
const panel = document.getElementById(trigger.getAttribute('aria-controls'));
trigger.addEventListener('click', () => {
const open = trigger.getAttribute('aria-expanded') === 'true';
trigger.setAttribute('aria-expanded', String(!open));
panel.hidden = open;
});
ComponentsCC0-style snippets
Tooltip label
A compact contextual label for icon-only controls, shown on hover and keyboard focus.
[data-sc-tooltip] { position: relative; }
[data-sc-tooltip]::after {
content: attr(data-sc-tooltip); position: absolute; left: 50%; bottom: calc(100% + 8px);
transform: translate(-50%, 4px); opacity: 0; pointer-events: none; white-space: nowrap;
padding: 5px 7px; border-radius: 5px; background: #111318; color: #fff;
font: 12px/1.2 system-ui, sans-serif; transition: opacity 150ms ease, transform 150ms ease;
}
[data-sc-tooltip]:hover::after,
[data-sc-tooltip]:focus-visible::after { opacity: 1; transform: translate(-50%, 0); }
@media (prefers-reduced-motion: reduce) { [data-sc-tooltip]::after { transition: none; } }
NavigationCC0-style snippets
Active nav underline
A sliding active indicator that makes the current navigation context obvious.
[data-sc-effect="nav-underline"] { --sc-underline-x: 0%; --sc-underline-width: 0px; position: relative; }
[data-sc-effect="nav-underline"]::after {
content: ""; position: absolute; left: var(--sc-underline-x); bottom: -5px;
width: var(--sc-underline-width); height: 2px; background: currentColor;
transition: left 180ms ease, width 180ms ease;
}
@media (prefers-reduced-motion: reduce) { [data-sc-effect="nav-underline"]::after { transition: none; } }
const nav = document.querySelector('[data-sc-effect="nav-underline"]');
nav.querySelectorAll('a').forEach((link) => {
link.addEventListener('pointerenter', () => {
nav.style.setProperty('--sc-underline-x', `${link.offsetLeft}px`);
nav.style.setProperty('--sc-underline-width', `${link.offsetWidth}px`);
});
link.addEventListener('click', (event) => {
event.preventDefault();
nav.querySelectorAll('a').forEach((item) => item.removeAttribute('aria-current'));
link.setAttribute('aria-current', 'page');
});
});
ComponentsCC0-style snippets
Tabs switcher
A keyboard-friendly tab switcher with real selected state and a small content transition.
<div data-sc-effect="tabs-switcher">
<div role="tablist"><button type="button" role="tab" aria-selected="true" aria-controls="tab-a">A</button><button type="button" role="tab" aria-selected="false" aria-controls="tab-b">B</button></div>
<div id="tab-a" role="tabpanel">First view</div><div id="tab-b" role="tabpanel" hidden>Second view</div>
</div>
document.querySelectorAll('[data-sc-effect="tabs-switcher"] [role="tab"]').forEach((tab) => {
tab.addEventListener('click', () => {
const group = tab.closest('[role="tablist"]').parentElement;
group.querySelectorAll('[role="tab"]').forEach((item) => item.setAttribute('aria-selected', String(item === tab)));
group.querySelectorAll('[role="tabpanel"]').forEach((panel) => panel.hidden = panel.id !== tab.getAttribute('aria-controls'));
});
});
Media & layoutCC0-style snippets
Horizontal scroll cue
A touch-friendly horizontal rail with snap points and a visible cue that more content is available.
[data-sc-effect="horizontal-scroll"] {
display: flex; gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-inline: contain;
scrollbar-width: thin;
}
[data-sc-effect="horizontal-scroll"] > * { flex: 0 0 min(78vw, 280px); scroll-snap-align: start; }
Text motionCC0-style snippets
Count-up number
A short numeric transition that gives a metric a sense of arrival without hiding the final value.
<span data-sc-effect="count-up" data-value="240">240</span>
const count = document.querySelector('[data-sc-effect="count-up"]');
const end = Number(count.dataset.value || 240);
const start = performance.now();
function tick(now) {
const progress = Math.min((now - start) / 900, 1);
count.textContent = Math.round(end * (1 - Math.pow(1 - progress, 3))).toLocaleString();
if (progress < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
Text motionCC0-style snippets
Typewriter text
A short, decorative typewriter cue for a headline or status line, with the full copy preserved for assistive technology.
<span data-sc-effect="typewriter" data-text="Build with clarity">Build with clarity</span>
const node = document.querySelector('[data-sc-effect="typewriter"]');
const text = node.dataset.text || node.textContent;
node.setAttribute('aria-label', text);
node.textContent = '';
let index = 0;
const type = () => { node.textContent = text.slice(0, ++index); if (index < text.length) setTimeout(type, 55); };
type();
Text motionCC0-style snippets
Pauseable marquee
A low-priority looping strip with a pause control, suitable for decorative keywords or partner labels.
[data-sc-effect="marquee"] { overflow: hidden; }
[data-sc-effect="marquee"] .sc-marquee-track { display: flex; width: max-content; animation: sc-marquee 18s linear infinite; }
[data-sc-effect="marquee"]:has(button:focus-visible) .sc-marquee-track,
[data-sc-effect="marquee"].is-paused .sc-marquee-track { animation-play-state: paused; }
@keyframes sc-marquee { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="marquee"] .sc-marquee-track { animation: none; } }
<div data-sc-effect="marquee"><div class="sc-marquee-track"><span>Clarity · Motion · Speed · </span><span aria-hidden="true">Clarity · Motion · Speed · </span></div><button type="button" data-sc-marquee-toggle>Pause</button></div>
<script>
const marquee = document.querySelector('[data-sc-effect="marquee"]');
marquee.querySelector('[data-sc-marquee-toggle]').addEventListener('click', (event) => {
marquee.classList.toggle('is-paused');
event.currentTarget.textContent = marquee.classList.contains('is-paused') ? 'Play' : 'Pause';
});
</script>
BackgroundsCC0-style snippets
Shimmer skeleton
A restrained loading placeholder that communicates waiting without pretending content has arrived.
[data-sc-effect="shimmer"] { position: relative; overflow: hidden; background: #e8ecf4; }
[data-sc-effect="shimmer"]::after {
content: ""; position: absolute; inset: 0; transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255,255,255,.7), transparent);
animation: sc-shimmer 1.5s ease-in-out infinite;
}
@keyframes sc-shimmer { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="shimmer"]::after { animation: none; } }
BackgroundsCC0-style snippets
Blob morph background
A soft organic background shape for a creative hero, kept behind content and disabled for reduced motion.
[data-sc-effect="blob-morph"] { position: relative; isolation: isolate; overflow: hidden; }
[data-sc-effect="blob-morph"]::before {
content: ""; position: absolute; width: 58%; aspect-ratio: 1; inset: 12% auto auto 22%; z-index: -1;
border-radius: 62% 38% 41% 59% / 53% 44% 56% 47%; background: #aab1ff; filter: blur(4px);
animation: sc-blob 9s ease-in-out infinite alternate;
}
@keyframes sc-blob { to { border-radius: 36% 64% 57% 43% / 41% 52% 48% 59%; transform: rotate(18deg) scale(1.08); } }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="blob-morph"]::before { animation: none; } }
Text motionCC0-style snippets
Underline sweep
A bright underline that draws attention to a link while leaving the text and focus state intact.
[data-sc-effect="underline-sweep"] { background: linear-gradient(currentColor, currentColor) 0 100% / 0 2px no-repeat; transition: background-size 220ms ease; }
[data-sc-effect="underline-sweep"]:hover,
[data-sc-effect="underline-sweep"]:focus-visible { background-size: 100% 2px; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="underline-sweep"] { transition: none; } }
Media & layoutCC0-style snippets
Image zoom frame
A restrained image scale on hover that keeps the crop contained and does not alter the layout.
[data-sc-effect="image-zoom"] { overflow: hidden; }
[data-sc-effect="image-zoom"] img { display: block; width: 100%; transition: transform 350ms ease; }
[data-sc-effect="image-zoom"]:hover img,
[data-sc-effect="image-zoom"]:focus-within img { transform: scale(1.06); }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="image-zoom"] img { transition: none; } }
Media & layoutCC0-style snippets
Pointer parallax layer
A small pointer-based depth shift for a decorative layer, with touch and reduced-motion fallbacks.
<div data-sc-effect="parallax"><div data-sc-parallax-layer>Focal visual</div></div>
<style>[data-sc-effect="parallax"] { overflow: hidden; } [data-sc-parallax-layer] { transition: transform 180ms ease; }</style>
<script>
const scene = document.querySelector('[data-sc-effect="parallax"]');
const layer = scene.querySelector('[data-sc-parallax-layer]');
scene.addEventListener('pointermove', (event) => {
const box = scene.getBoundingClientRect();
const x = (event.clientX - box.left) / box.width - .5;
const y = (event.clientY - box.top) / box.height - .5;
layer.style.transform = `translate3d(${x * 12}px, ${y * 12}px, 0)`;
}, { passive: true });
scene.addEventListener('pointerleave', () => { layer.style.transform = ''; });
ComponentsCC0-style snippets
Toast notification
A compact status message for a completed action, with live-region semantics and an explicit close affordance.
<button type="button" data-sc-toast-trigger>Save</button>
<div data-sc-effect="toast" role="status" hidden>Saved just now <button type="button" data-sc-toast-close aria-label="Close">×</button></div>
<script>
const toast = document.querySelector('[data-sc-effect="toast"]');
document.querySelector('[data-sc-toast-trigger]').addEventListener('click', () => {
toast.hidden = false;
window.setTimeout(() => { toast.hidden = true; }, 3200);
});
toast.querySelector('[data-sc-toast-close]').addEventListener('click', () => { toast.hidden = true; });
ComponentsCC0-style snippets
Toggle switch
A clear on/off control with a native checkbox underneath the visual switch.
<label class="switch"><input type="checkbox"><span aria-hidden="true"></span><b>Enable updates</b></label>
.switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.switch input { position: absolute; opacity: 0; }
.switch span { width: 38px; height: 22px; border-radius: 999px; background: #cfd6e3; transition: background 180ms ease; }
.switch span::after { content: ""; display: block; width: 18px; height: 18px; margin: 2px; border-radius: 50%; background: #fff; transition: transform 180ms ease; }
.switch input:checked + span { background: #5b5ce2; }
.switch input:checked + span::after { transform: translateX(16px); }
.switch input:focus-visible + span { outline: 3px solid #aab1ff; outline-offset: 3px; }
Scroll revealsCC0-style snippets
Timeline draw
A vertical progress line that draws as milestones enter the viewport.
<div data-sc-effect="timeline"><i aria-hidden="true"></i><span data-sc-timeline-step>Plan</span><span data-sc-timeline-step>Build</span><span data-sc-timeline-step>Ship</span></div>
<style>[data-sc-effect="timeline"] { --sc-timeline-progress: 0%; position: relative; display: flex; gap: 16px; } [data-sc-effect="timeline"] > i { position: absolute; left: 0; right: 0; top: 50%; height: 2px; background: #d9deec; } [data-sc-effect="timeline"] > i::after { content: ""; display: block; width: var(--sc-timeline-progress); height: 100%; background: #5b5ce2; } [data-sc-effect="timeline"] > span { position: relative; padding: 5px; background: #fff; }</style>
<script>
const timeline = document.querySelector('[data-sc-effect="timeline"]');
const timelineObserver = new IntersectionObserver((entries) => entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
const visible = timeline.querySelectorAll('.is-visible').length;
const total = timeline.querySelectorAll('[data-sc-timeline-step]').length;
timeline.style.setProperty('--sc-timeline-progress', `${total > 1 ? ((visible - 1) / (total - 1)) * 100 : 100}%`);
}
}), { threshold: .35 });
timeline.querySelectorAll('[data-sc-timeline-step]').forEach((step) => timelineObserver.observe(step));
Micro-interactionsCC0-style snippets
Card flip reveal
A click-to-reveal two-sided card that works with keyboard focus and keeps both sides in the DOM.
[data-sc-effect="card-flip"] { perspective: 800px; }
[data-sc-effect="card-flip"] .sc-flip-inner { transition: transform 500ms ease; transform-style: preserve-3d; }
[data-sc-effect="card-flip"] .sc-flip-inner > * { position: absolute; inset: 0; display: grid; place-items: center; backface-visibility: hidden; }
[data-sc-effect="card-flip"] .sc-flip-inner > strong { transform: rotateY(180deg); }
[data-sc-effect="card-flip"].is-flipped .sc-flip-inner { transform: rotateY(180deg); }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="card-flip"] .sc-flip-inner { transition: none; } }
<button type="button" data-sc-effect="card-flip" aria-pressed="false"><span class="sc-flip-inner"><b>Front</b><strong>Back</strong></span></button>
<script>
const flip = document.querySelector('[data-sc-effect="card-flip"]');
flip.addEventListener('click', () => { const open = flip.classList.toggle('is-flipped'); flip.setAttribute('aria-pressed', String(open)); });
</script>
Media & layoutCC0-style snippets
Scroll-snap gallery
A small gallery with native scroll snapping and button controls for users who prefer explicit navigation.
<div data-sc-effect="scroll-snap-gallery"><div class="sc-gallery-track"><article>01</article><article>02</article><article>03</article></div><button type="button" data-sc-gallery-prev>Previous</button><button type="button" data-sc-gallery-next>Next</button></div>
<style>[data-sc-effect="scroll-snap-gallery"] { overflow: hidden; } [data-sc-effect="scroll-snap-gallery"] .sc-gallery-track { display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory; } [data-sc-effect="scroll-snap-gallery"] article { flex: 0 0 78%; scroll-snap-align: start; }</style>
<script>
const rail = document.querySelector('[data-sc-effect="scroll-snap-gallery"]');
rail.querySelector('[data-sc-gallery-next]').addEventListener('click', () => rail.scrollBy({ left: rail.clientWidth * .8, behavior: 'smooth' }));
rail.querySelector('[data-sc-gallery-prev]').addEventListener('click', () => rail.scrollBy({ left: -rail.clientWidth * .8, behavior: 'smooth' }));
Scroll-drivenCC0-style snippets
Scroll-scrub scale
Tie a visual scale and opacity shift to scroll position with a bounded, progressive enhancement.
[data-sc-effect="scroll-scrub-scale"] {
--sc-progress: 0;
opacity: calc(1 - (var(--sc-progress) * .35));
transform: scale(calc(1 + (var(--sc-progress) * .12)));
transform-origin: center;
will-change: transform, opacity;
}
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="scroll-scrub-scale"] { opacity: 1; transform: none; }
}
const scrubTarget = document.querySelector('[data-sc-effect="scroll-scrub-scale"]');
let scrubFrame = 0;
function updateScrub() {
scrubFrame = 0;
const box = scrubTarget.getBoundingClientRect();
const progress = Math.min(1, Math.max(0, (window.innerHeight - box.top) / (window.innerHeight + box.height)));
scrubTarget.style.setProperty('--sc-progress', progress.toFixed(3));
}
window.addEventListener('scroll', () => {
if (!scrubFrame) scrubFrame = requestAnimationFrame(updateScrub);
}, { passive: true });
updateScrub();
Scroll-drivenCC0-style snippets
Sticky section progress
Keep a compact step marker pinned while the related section moves through view.
[data-sc-effect="sticky-section-progress"] { position: relative; }
[data-sc-effect="sticky-section-progress"] .sc-sticky-marker {
position: sticky; top: 1rem; z-index: 2; width: max-content;
padding: .55rem .75rem; border: 1px solid #dce3ee; border-radius: 999px;
background: rgba(255,255,255,.92); backdrop-filter: blur(10px);
}
[data-sc-effect="sticky-section-progress"] .sc-sticky-marker [aria-current="true"] { color: #155eef; font-weight: 800; }
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="sticky-section-progress"] .sc-sticky-marker { scroll-behavior: auto; }
}
Scroll-drivenCC0-style snippets
Scroll-highlight navigation
Highlight the navigation item for the section currently in view using native IntersectionObserver.
[data-sc-effect="scroll-highlight"] a { color: #66758a; transition: color 180ms ease; }
[data-sc-effect="scroll-highlight"] a[aria-current="location"] { color: #155eef; font-weight: 800; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="scroll-highlight"] a { transition: none; } }
const highlightNav = document.querySelector('[data-sc-effect="scroll-highlight"]');
const highlightLinks = [...highlightNav.querySelectorAll('a[href^="#"]')];
const highlightObserver = new IntersectionObserver((entries) => {
entries.forEach(({ isIntersecting, target }) => {
if (!isIntersecting) return;
highlightLinks.forEach((link) => link.removeAttribute('aria-current'));
highlightNav.querySelector(`a[href="#${target.id}"]`)?.setAttribute('aria-current', 'location');
});
}, { rootMargin: '-35% 0px -55% 0px' });
highlightLinks.forEach((link) => document.querySelector(link.hash) && highlightObserver.observe(document.querySelector(link.hash)));
Scroll-drivenCC0-style snippets
SVG path draw
Draw a small inline SVG path into view without an animation library or an external asset.
[data-sc-effect="svg-path-draw"] path {
fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round;
stroke-dasharray: 1; stroke-dashoffset: 1;
pathLength: 1; animation: sc-path-draw 1.1s cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes sc-path-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="svg-path-draw"] path { animation: none; stroke-dashoffset: 0; }
}
Text motionCC0-style snippets
Text mask wipe
Reveal a short heading with a clean mask wipe that preserves the text in the document tree.
[data-sc-effect="text-mask-wipe"] {
clip-path: inset(0 100% 0 0); transform: translateX(-.2em);
animation: sc-mask-wipe 800ms cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes sc-mask-wipe { to { clip-path: inset(0); transform: none; } }
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="text-mask-wipe"] { clip-path: inset(0); transform: none; animation: none; }
}
Text motionCC0-style snippets
Gradient text flow
Move a restrained brand gradient across a short text treatment while keeping a solid-color fallback.
[data-sc-effect="gradient-text-flow"] {
color: #155eef; background: linear-gradient(100deg, #155eef, #8b5cf6, #0ea5a1, #155eef);
background-size: 220% auto; background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; animation: sc-gradient-text 7s linear infinite;
}
@keyframes sc-gradient-text { to { background-position: 220% center; } }
@media (prefers-reduced-motion: reduce) {
[data-sc-effect="gradient-text-flow"] { animation: none; background-position: 0 center; }
}
Text motionCC0-style snippets
Text scramble
Resolve a short label through a burst of characters before landing on the real message.
[data-sc-effect="text-scramble"] { font-variant-numeric: tabular-nums; }
const scramble = document.querySelector('[data-sc-effect="text-scramble"]');
const finalText = scramble.textContent;
const chars = 'アカサタナ0123456789';
let frame = 0;
const scrambleTimer = setInterval(() => {
scramble.textContent = finalText.split('').map((letter, index) => index < frame / 3 ? letter : chars[Math.floor(Math.random() * chars.length)]).join('');
frame += 1;
if (frame > finalText.length * 3) { clearInterval(scrambleTimer); scramble.textContent = finalText; }
}, 42);
Text motionCC0-style snippets
Number ticker
Animate a metric to a final value with a readable numeric fallback and no dependency.
[data-sc-effect="number-ticker"] { font-variant-numeric: tabular-nums; }
const ticker = document.querySelector('[data-sc-effect="number-ticker"]');
const targetValue = Number(ticker.dataset.value || 240);
const reduceTicker = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceTicker) ticker.textContent = targetValue.toLocaleString();
else {
const start = performance.now();
const tick = (now) => {
const progress = Math.min(1, (now - start) / 900);
ticker.textContent = Math.round(targetValue * (1 - Math.pow(1 - progress, 3))).toLocaleString();
if (progress < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
ComponentsCC0-style snippets
Progress ring
A compact SVG progress ring that communicates completion without relying on color alone.
[data-sc-effect="progress-ring"] { --sc-progress: 0; }
[data-sc-effect="progress-ring"] circle { fill: none; stroke: #155eef; stroke-width: 8; stroke-linecap: round; stroke-dasharray: 100; stroke-dashoffset: calc(100 - var(--sc-progress)); pathLength: 100; transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 500ms ease; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="progress-ring"] circle { transition: none; } }
Advanced interactionsCC0-style snippets
Cursor trail
A lightweight pointer trail made from CSS variables and a few decorative dots, disabled for touch and reduced motion.
[data-sc-effect="cursor-trail"] { --sc-x: 50%; --sc-y: 50%; position: relative; isolation: isolate; }
[data-sc-effect="cursor-trail"]::before,
[data-sc-effect="cursor-trail"]::after { content: ""; position: absolute; z-index: -1; width: 10px; aspect-ratio: 1; border-radius: 50%; left: var(--sc-x); top: var(--sc-y); background: #8b5cf6; transform: translate(-50%, -50%); filter: blur(1px); pointer-events: none; transition: transform 220ms ease, opacity 220ms ease; }
[data-sc-effect="cursor-trail"]::after { width: 34px; background: #155eef; opacity: .2; transition-duration: 500ms; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="cursor-trail"]::before, [data-sc-effect="cursor-trail"]::after { display: none; } }
const trail = document.querySelector('[data-sc-effect="cursor-trail"]');
trail.addEventListener('pointermove', (event) => { const box = trail.getBoundingClientRect(); trail.style.setProperty('--sc-x', `${event.clientX - box.left}px`); trail.style.setProperty('--sc-y', `${event.clientY - box.top}px`); });
Advanced interactionsCC0-style snippets
Hover lens
Move a soft lens across a visual card to reveal depth without loading a WebGL layer.
[data-sc-effect="hover-lens"] { --sc-x: 50%; --sc-y: 50%; position: relative; overflow: hidden; }
[data-sc-effect="hover-lens"]::after { content: ""; position: absolute; inset: 0; background: radial-gradient(100px circle at var(--sc-x) var(--sc-y), rgba(255,255,255,.7), transparent 64%); mix-blend-mode: screen; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="hover-lens"]::after { display: none; } }
document.querySelectorAll('[data-sc-effect="hover-lens"]').forEach((card) => card.addEventListener('pointermove', (event) => { const box = card.getBoundingClientRect(); card.style.setProperty('--sc-x', `${event.clientX - box.left}px`); card.style.setProperty('--sc-y', `${event.clientY - box.top}px`); }));
3D & perspectiveCC0-style snippets
Tilt parallax card
A layered 3D card response that moves foreground content slightly more than its surface.
[data-sc-effect="tilt-parallax-card"] { perspective: 900px; }
[data-sc-effect="tilt-parallax-card"] .sc-tilt-surface { transform: rotateX(var(--sc-rotate-x, 0deg)) rotateY(var(--sc-rotate-y, 0deg)); transition: transform 180ms ease; transform-style: preserve-3d; }
[data-sc-effect="tilt-parallax-card"] .sc-tilt-foreground { transform: translateZ(22px); }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="tilt-parallax-card"] .sc-tilt-surface { transform: none; transition: none; } }
const tiltCards = document.querySelectorAll('[data-sc-effect="tilt-parallax-card"]');
tiltCards.forEach((card) => { const surface = card.querySelector('.sc-tilt-surface'); card.addEventListener('pointermove', (event) => { const box = card.getBoundingClientRect(); const x = (event.clientX - box.left) / box.width - .5; const y = (event.clientY - box.top) / box.height - .5; surface.style.setProperty('--sc-rotate-x', `${y * -8}deg`); surface.style.setProperty('--sc-rotate-y', `${x * 10}deg`); }); card.addEventListener('pointerleave', () => { surface.style.setProperty('--sc-rotate-x', '0deg'); surface.style.setProperty('--sc-rotate-y', '0deg'); }); });
3D & perspectiveCC0-style snippets
Card stack shuffle
Cycle a small stack of cards with explicit controls and a natural static order.
[data-sc-effect="card-stack"] { position: relative; display: grid; }
[data-sc-effect="card-stack"] > * { grid-area: 1 / 1; transition: transform 300ms ease, opacity 300ms ease; }
[data-sc-effect="card-stack"] > *:nth-child(2) { transform: translate(8px, 7px) scale(.96); opacity: .7; }
[data-sc-effect="card-stack"] > *:nth-child(3) { transform: translate(16px, 14px) scale(.92); opacity: .45; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="card-stack"] > * { transition: none; } }
3D & perspectiveCC0-style snippets
Perspective grid
Create a subtle perspective grid that shifts with a pointer while staying entirely CSS-based.
[data-sc-effect="perspective-grid"] { --sc-grid-x: 0px; --sc-grid-y: 0px; position: relative; overflow: hidden; background: #111827; }
[data-sc-effect="perspective-grid"]::before { content: ""; position: absolute; inset: -35%; background-image: linear-gradient(rgba(148,163,184,.25) 1px, transparent 1px), linear-gradient(90deg, rgba(148,163,184,.25) 1px, transparent 1px); background-size: 28px 28px; transform: perspective(260px) rotateX(54deg) translate(var(--sc-grid-x), var(--sc-grid-y)); transform-origin: center bottom; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="perspective-grid"]::before { transform: perspective(260px) rotateX(54deg); } }
const grid = document.querySelector('[data-sc-effect="perspective-grid"]');
grid.addEventListener('pointermove', (event) => { const box = grid.getBoundingClientRect(); grid.style.setProperty('--sc-grid-x', `${(event.clientX - box.left - box.width / 2) * .08}px`); grid.style.setProperty('--sc-grid-y', `${(event.clientY - box.top - box.height / 2) * .04}px`); });
Advanced interactionsCC0-style snippets
Modal dialog
A focus-friendly native dialog pattern with a real close button and escape behavior.
[data-sc-effect="modal-dialog"] dialog { max-width: min(90vw, 32rem); border: 1px solid #dce3ee; border-radius: 12px; padding: 1.25rem; box-shadow: 0 24px 80px rgba(15,23,42,.2); }
[data-sc-effect="modal-dialog"] dialog::backdrop { background: rgba(15,23,42,.45); }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="modal-dialog"] dialog { scroll-behavior: auto; } }
<div data-sc-effect="modal-dialog"><button type="button" data-dialog-open aria-haspopup="dialog">Open details</button><dialog data-dialog><p>Short supporting content.</p><button type="button" data-dialog-close>Close</button></dialog></div>
<script>const modalRoot = document.querySelector('[data-sc-effect="modal-dialog"]'); const dialog = modalRoot.querySelector('[data-dialog]'); modalRoot.querySelector('[data-dialog-open]').addEventListener('click', () => dialog.showModal()); modalRoot.querySelector('[data-dialog-close]').addEventListener('click', () => dialog.close());</script>
Advanced interactionsCC0-style snippets
Command palette
Expose a small keyboard-friendly command surface for power users without adding a framework.
[data-sc-effect="command-palette"] [role="dialog"] { border: 1px solid #dce3ee; border-radius: 12px; background: #fff; box-shadow: 0 24px 70px rgba(15,23,42,.18); }
[data-sc-effect="command-palette"] [aria-selected="true"] { background: #edf4ff; color: #155eef; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="command-palette"] [role="dialog"] { transition: none; } }
const palette = document.querySelector('[data-sc-effect="command-palette"]'); const paletteDialog = palette.querySelector('[role="dialog"]'); const paletteButton = palette.querySelector('[data-palette-open]'); paletteButton.addEventListener('click', () => paletteDialog.hidden = false); palette.addEventListener('keydown', (event) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); paletteDialog.hidden = false; paletteDialog.querySelector('input')?.focus(); } if (event.key === 'Escape') paletteDialog.hidden = true; });
Advanced interactionsCC0-style snippets
Filter chip group
A compact filter group with real pressed state and a live result label.
[data-sc-effect="filter-chip-group"] { display: flex; flex-wrap: wrap; gap: .5rem; }
[data-sc-effect="filter-chip-group"] button[aria-pressed="true"] { background: #155eef; border-color: #155eef; color: #fff; }
Forms & feedbackCC0-style snippets
Range slider output
Keep a range input and its visible formatted value synchronized for pricing and preference controls.
[data-sc-effect="range-slider-output"] { display: grid; gap: .5rem; }
[data-sc-effect="range-slider-output"] input { accent-color: #155eef; }
[data-sc-effect="range-slider-output"] output { color: #155eef; font-weight: 800; font-variant-numeric: tabular-nums; }
const rangeRoot = document.querySelector('[data-sc-effect="range-slider-output"]'); const range = rangeRoot.querySelector('input[type="range"]'); const output = rangeRoot.querySelector('output'); const updateRange = () => { output.value = `${range.value}%`; output.textContent = `${range.value}%`; }; range.addEventListener('input', updateRange); updateRange();
Forms & feedbackCC0-style snippets
Floating label input
A compact form label that stays visible and does not rely on placeholder text as the only label.
[data-sc-effect="floating-label-input"] { position: relative; display: block; }
[data-sc-effect="floating-label-input"] input { width: 100%; padding: 1rem .7rem .45rem; border: 1px solid #cbd5e1; border-radius: 7px; background: #fff; }
[data-sc-effect="floating-label-input"] label { position: absolute; left: .7rem; top: .72rem; color: #64748b; pointer-events: none; transition: transform 150ms ease, color 150ms ease, background 150ms ease; }
[data-sc-effect="floating-label-input"] input:focus + label,
[data-sc-effect="floating-label-input"] input:not(:placeholder-shown) + label { transform: translateY(-.72rem) scale(.78); padding-inline: .2rem; background: #fff; color: #155eef; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="floating-label-input"] label { transition: none; } }
Forms & feedbackCC0-style snippets
Password strength meter
Give users immediate, plain-language password feedback without exposing or storing the password value.
[data-sc-effect="password-strength-meter"] [role="progressbar"] { height: .35rem; overflow: hidden; border-radius: 99px; background: #e2e8f0; }
[data-sc-effect="password-strength-meter"] [role="progressbar"] span { display: block; width: var(--sc-strength, 0%); height: 100%; background: #22c55e; transition: width 180ms ease, background 180ms ease; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="password-strength-meter"] [role="progressbar"] span { transition: none; } }
const strengthRoot = document.querySelector('[data-sc-effect="password-strength-meter"]'); const password = strengthRoot.querySelector('input'); const meter = strengthRoot.querySelector('[role="progressbar"]'); const label = strengthRoot.querySelector('[data-strength-label]'); password.addEventListener('input', () => { const value = password.value; const score = Math.min(4, Number(value.length >= 8) + Number(/[A-Z]/.test(value)) + Number(/[0-9]/.test(value)) + Number(/[^A-Za-z0-9]/.test(value))); meter.style.setProperty('--sc-strength', `${score * 25}%`); meter.setAttribute('aria-valuenow', score * 25); label.textContent = score < 2 ? 'Needs more variety' : score < 4 ? 'Getting stronger' : 'Good mix'; });
Forms & feedbackCC0-style snippets
Dropzone feedback
Make a file drop area communicate drag state and keyboard activation without a file-upload dependency.
[data-sc-effect="dropzone-feedback"] { display: grid; place-items: center; min-height: 6rem; padding: 1rem; border: 1px dashed #8fa4c7; border-radius: 10px; background: #f8fbff; text-align: center; transition: border-color 150ms ease, background 150ms ease; }
[data-sc-effect="dropzone-feedback"].is-dragging, [data-sc-effect="dropzone-feedback"]:focus-visible { border-color: #155eef; background: #edf4ff; outline: none; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="dropzone-feedback"] { transition: none; } }
const dropzone = document.querySelector('[data-sc-effect="dropzone-feedback"]'); ['dragenter', 'dragover'].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.add('is-dragging'); })); ['dragleave', 'drop'].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.remove('is-dragging'); })); dropzone.addEventListener('drop', (event) => { dropzone.querySelector('[data-dropzone-status]').textContent = `${event.dataTransfer.files.length} file(s) ready`; });
Forms & feedbackCC0-style snippets
Skeleton to content
Transition a loading skeleton to real content while keeping the loading state short and honest.
[data-sc-effect="skeleton-to-content"] { position: relative; }
[data-sc-effect="skeleton-to-content"] [data-skeleton] { min-height: 3rem; border-radius: 6px; background: linear-gradient(90deg, #e2e8f0, #fff, #e2e8f0); background-size: 200% 100%; animation: sc-skeleton 1.4s linear infinite; }
[data-sc-effect="skeleton-to-content"] [data-content] { display: none; }
[data-sc-effect="skeleton-to-content"].is-loaded [data-skeleton] { display: none; }
[data-sc-effect="skeleton-to-content"].is-loaded [data-content] { display: block; }
@keyframes sc-skeleton { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="skeleton-to-content"] [data-skeleton] { animation: none; } }
Media & layoutCC0-style snippets
Image reveal wipe
Reveal an image from a clipped color block with a simple CSS mask and no asset dependency in the demo.
[data-sc-effect="image-reveal-wipe"] { overflow: hidden; clip-path: inset(0 100% 0 0); animation: sc-image-reveal 850ms cubic-bezier(.2,.8,.2,1) forwards; }
@keyframes sc-image-reveal { to { clip-path: inset(0); } }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="image-reveal-wipe"] { clip-path: inset(0); animation: none; } }
Media & layoutCC0-style snippets
Video hover preview
Preview a muted video or motion frame only after an explicit interaction, with a still-image fallback.
[data-sc-effect="video-hover-preview"] { position: relative; overflow: hidden; }
[data-sc-effect="video-hover-preview"] video { display: block; width: 100%; }
[data-sc-effect="video-hover-preview"] [data-video-play] { position: absolute; inset: auto .75rem .75rem auto; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="video-hover-preview"] video { animation: none; } }
const videoRoot = document.querySelector('[data-sc-effect="video-hover-preview"]'); const video = videoRoot.querySelector('video'); videoRoot.addEventListener('pointerenter', () => { if (!matchMedia('(prefers-reduced-motion: reduce)').matches) video.play(); }); videoRoot.addEventListener('pointerleave', () => video.pause());
Advanced interactionsCC0-style snippets
Theme switcher
Switch a component between light and dark visual tokens while keeping the state explicit and keyboard accessible.
[data-sc-effect="theme-switcher"] { --sc-surface: #fff; --sc-ink: #172033; padding: 1rem; border-radius: 10px; background: var(--sc-surface); color: var(--sc-ink); transition: background 180ms ease, color 180ms ease; }
[data-sc-effect="theme-switcher"].is-dark { --sc-surface: #172033; --sc-ink: #fff; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="theme-switcher"] { transition: none; } }
const themeRoot = document.querySelector('[data-sc-effect="theme-switcher"]'); themeRoot.querySelector('button').addEventListener('click', () => { const dark = themeRoot.classList.toggle('is-dark'); themeRoot.querySelector('[data-theme-label]').textContent = dark ? 'Dark' : 'Light'; });
Forms & feedbackCC0-style snippets
Copy button feedback
Confirm a copy action with a live status message that does not shift the button layout.
[data-sc-effect="copy-button-feedback"] { display: inline-flex; align-items: center; gap: .6rem; }
[data-sc-effect="copy-button-feedback"] [role="status"] { min-width: 4.5rem; color: #16805d; font-size: .85rem; }
const copyRoot = document.querySelector('[data-sc-effect="copy-button-feedback"]'); copyRoot.querySelector('button').addEventListener('click', async () => { const value = copyRoot.querySelector('[data-copy-value]').textContent; try { await navigator.clipboard.writeText(value); copyRoot.querySelector('[role="status"]').textContent = 'Copied'; } catch { copyRoot.querySelector('[role="status"]').textContent = 'Select to copy'; } });
Scroll-drivenCC0-style snippets
Reading time progress
Pair a reading-time estimate with a small progress indicator for long-form content.
[data-sc-effect="reading-time-progress"] { --sc-reading-progress: 0%; }
[data-sc-effect="reading-time-progress"]::before { content: ""; display: block; width: var(--sc-reading-progress); height: 3px; background: #155eef; transition: width 120ms linear; }
@media (prefers-reduced-motion: reduce) { [data-sc-effect="reading-time-progress"]::before { transition: none; } }
const reading = document.querySelector('[data-sc-effect="reading-time-progress"]'); let readingFrame = 0; const updateReading = () => { readingFrame = 0; const max = document.documentElement.scrollHeight - innerHeight; reading.style.setProperty('--sc-reading-progress', `${max > 0 ? (scrollY / max) * 100 : 0}%`); }; addEventListener('scroll', () => { if (!readingFrame) readingFrame = requestAnimationFrame(updateReading); }, { passive: true }); updateReading();