1. Collect
Save anything you like into a collection as you browse.
1. Collect
Save anything you like into a collection as you browse.
import { useState } from 'react';
const STEPS = [
{ title: 'Collect', text: 'Save anything you like into a collection as you browse.' },
{ title: 'Arrange', text: 'Drag things into order and group them by project.' },
{ title: 'Share', text: 'Send one link and everyone sees the same set.' },
];
const GAP = 14;
const DOT = 6;
const css = `
.card-onboarding { box-sizing: border-box; width: 240px; padding: 14px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); overflow: hidden; }
.card-onboarding__art { position: relative; display: grid; place-items: center; height: 72px; border-radius: 10px; background: light-dark(#ebedf0, #232529); overflow: hidden; }
.card-onboarding__step { animation: card-onboarding-in 0.4s cubic-bezier(0.3, 1.2, 0.5, 1) both; }
.card-onboarding__step--back { animation-name: card-onboarding-back; }
@keyframes card-onboarding-in { from { opacity: 0; transform: translateX(28px); } }
@keyframes card-onboarding-back { from { opacity: 0; transform: translateX(-28px); } }
.card-onboarding__shape { display: flex; gap: 6px; }
.card-onboarding__shape span { width: 22px; height: 22px; border-radius: 6px; background: light-dark(#6d7179, #7d8087); }
.card-onboarding__shape--0 span:nth-child(2) { background: light-dark(#50535b, #c6c8cd); border-radius: 50%; }
.card-onboarding__shape--1 span:nth-child(1) { transform: translateY(6px); }
.card-onboarding__shape--1 span:nth-child(3) { transform: translateY(-6px); background: light-dark(#50535b, #c6c8cd); }
.card-onboarding__shape--2 span { border-radius: 50%; }
.card-onboarding__shape--2 span:nth-child(2) { background: light-dark(#373a40, #f1f2f3); }
.card-onboarding__title { margin: 12px 0 2px; font-size: 13px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); }
.card-onboarding__text { height: 32px; margin: 0; font-size: 11.5px; line-height: 1.4; color: light-dark(#50535b, #c6c8cd); }
.card-onboarding__foot { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
.card-onboarding__dots { position: relative; width: ${GAP * 2 + DOT}px; height: ${DOT}px; }
.card-onboarding__dot { position: absolute; top: 0; width: ${DOT}px; height: ${DOT}px; border-radius: 50%; background: light-dark(#dbdde1, #2e3035); }
/* One pill that stretches to the new dot with its leading edge, then pulls its trailing edge after it. */
.card-onboarding__pill { position: absolute; top: 0; height: ${DOT}px; border-radius: ${DOT / 2}px; background: light-dark(#373a40, #f1f2f3); }
.card-onboarding__btns { display: flex; gap: 6px; }
.card-onboarding__btn { height: 28px; padding: 0 12px; border: 0; border-radius: 8px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; }
.card-onboarding__btn--ghost { background: light-dark(#ebedf0, #232529); color: light-dark(#42454c, #e5e6e9); }
.card-onboarding__btn:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.card-onboarding__step { animation: none; }
.card-onboarding__pill { transition: none !important; }
}
`;
export function OnboardingCard() {
const [s, setS] = useState({ i: 0, back: false });
const go = (i: number) => setS((o) => ({ i, back: i < o.i }));
const last = s.i === STEPS.length - 1;
const lead = '0.25s cubic-bezier(0.3, 1.2, 0.5, 1)';
const trail = '0.3s cubic-bezier(0.3, 1.2, 0.5, 1) 0.12s';
return (
<>
<style>
{css}
</style>
<article className="card-onboarding" aria-roledescription="carousel" aria-label="Getting started">
<div className="card-onboarding__art" aria-hidden="true">
<div key={s.i} className={`card-onboarding__step${s.back ? ' card-onboarding__step--back' : ''}`}>
<div className={`card-onboarding__shape card-onboarding__shape--${s.i}`}>
<span />
<span />
<span />
</div>
</div>
</div>
<div key={s.i} className={`card-onboarding__step${s.back ? ' card-onboarding__step--back' : ''}`} aria-live="polite">
<p className="card-onboarding__title">
{s.i + 1}. {STEPS[s.i].title}
</p>
<p className="card-onboarding__text">{STEPS[s.i].text}</p>
</div>
<div className="card-onboarding__foot">
<div className="card-onboarding__dots" aria-hidden="true">
{STEPS.map((_, i) => (
<span key={i} className="card-onboarding__dot" style={{ left: i * GAP }} />
))}
<span
className="card-onboarding__pill"
style={{
left: s.i * GAP,
right: (STEPS.length - 1 - s.i) * GAP,
transition: s.back ? `left ${lead}, right ${trail}` : `right ${lead}, left ${trail}`,
}}
/>
</div>
<div className="card-onboarding__btns">
{s.i > 0 && (
<button type="button" className="card-onboarding__btn card-onboarding__btn--ghost" onClick={() => go(s.i - 1)}>
Back
</button>
)}
<button type="button" className="card-onboarding__btn" onClick={() => go(last ? 0 : s.i + 1)}>
{last ? 'Get started' : 'Next'}
</button>
</div>
</div>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardOnboarding(el) on the mounted element instead.
<article class="card-onboarding" aria-roledescription="carousel" aria-label="Getting started">
<div class="card-onboarding__art" aria-hidden="true">
<div class="card-onboarding__step">
<div class="card-onboarding__shape card-onboarding__shape--0">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
<div class="card-onboarding__step" aria-live="polite">
<p class="card-onboarding__title">1. Collect</p>
<p class="card-onboarding__text">Save anything you like into a collection as you browse.</p>
</div>
<div class="card-onboarding__foot">
<div class="card-onboarding__dots" aria-hidden="true">
<span class="card-onboarding__dot" style="left:0"></span>
<span class="card-onboarding__dot" style="left:14px"></span>
<span class="card-onboarding__dot" style="left:28px"></span>
<span class="card-onboarding__pill" style="left:0;right:28px;transition:right 0.25s cubic-bezier(0.3, 1.2, 0.5, 1), left 0.3s cubic-bezier(0.3, 1.2, 0.5, 1) 0.12s"></span>
</div>
<div class="card-onboarding__btns">
<button type="button" class="card-onboarding__btn">Next</button>
</div>
</div>
</article>.card-onboarding { box-sizing: border-box; width: 240px; padding: 14px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); overflow: hidden; }
.card-onboarding__art { position: relative; display: grid; place-items: center; height: 72px; border-radius: 10px; background: light-dark(#ebedf0, #232529); overflow: hidden; }
.card-onboarding__step { animation: card-onboarding-in 0.4s cubic-bezier(0.3, 1.2, 0.5, 1) both; }
.card-onboarding__step--back { animation-name: card-onboarding-back; }
@keyframes card-onboarding-in { from { opacity: 0; transform: translateX(28px); } }
@keyframes card-onboarding-back { from { opacity: 0; transform: translateX(-28px); } }
.card-onboarding__shape { display: flex; gap: 6px; }
.card-onboarding__shape span { width: 22px; height: 22px; border-radius: 6px; background: light-dark(#6d7179, #7d8087); }
.card-onboarding__shape--0 span:nth-child(2) { background: light-dark(#50535b, #c6c8cd); border-radius: 50%; }
.card-onboarding__shape--1 span:nth-child(1) { transform: translateY(6px); }
.card-onboarding__shape--1 span:nth-child(3) { transform: translateY(-6px); background: light-dark(#50535b, #c6c8cd); }
.card-onboarding__shape--2 span { border-radius: 50%; }
.card-onboarding__shape--2 span:nth-child(2) { background: light-dark(#373a40, #f1f2f3); }
.card-onboarding__title { margin: 12px 0 2px; font-size: 13px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); }
.card-onboarding__text { height: 32px; margin: 0; font-size: 11.5px; line-height: 1.4; color: light-dark(#50535b, #c6c8cd); }
.card-onboarding__foot { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
.card-onboarding__dots { position: relative; width: 34px; height: 6px; }
.card-onboarding__dot { position: absolute; top: 0; width: 6px; height: 6px; border-radius: 50%; background: light-dark(#dbdde1, #2e3035); }
/* One pill that stretches to the new dot with its leading edge, then pulls its trailing edge after it. */
.card-onboarding__pill { position: absolute; top: 0; height: 6px; border-radius: 3px; background: light-dark(#373a40, #f1f2f3); }
.card-onboarding__btns { display: flex; gap: 6px; }
.card-onboarding__btn { height: 28px; padding: 0 12px; border: 0; border-radius: 8px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; }
.card-onboarding__btn--ghost { background: light-dark(#ebedf0, #232529); color: light-dark(#42454c, #e5e6e9); }
.card-onboarding__btn:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.card-onboarding__step { animation: none; }
.card-onboarding__pill { transition: none !important; }
}function initCardOnboarding(root) {
var STEPS = [
{ title: 'Collect', text: 'Save anything you like into a collection as you browse.' },
{ title: 'Arrange', text: 'Drag things into order and group them by project.' },
{ title: 'Share', text: 'Send one link and everyone sees the same set.' },
];
var GAP = 14;
var LEAD = '0.25s cubic-bezier(0.3, 1.2, 0.5, 1)';
var TRAIL = '0.3s cubic-bezier(0.3, 1.2, 0.5, 1) 0.12s';
var steps = root.querySelectorAll('.card-onboarding__step');
var shape = root.querySelector('.card-onboarding__shape');
var title = root.querySelector('.card-onboarding__title');
var text = root.querySelector('.card-onboarding__text');
var pill = root.querySelector('.card-onboarding__pill');
var btns = root.querySelector('.card-onboarding__btns');
var next = root.querySelector('.card-onboarding__btn');
var back = document.createElement('button');
back.type = 'button';
back.className = 'card-onboarding__btn card-onboarding__btn--ghost';
back.textContent = 'Back';
var at = 0;
function go(i) {
var backwards = i < at;
at = i;
shape.className = 'card-onboarding__shape card-onboarding__shape--' + at;
title.textContent = at + 1 + '. ' + STEPS[at].title;
text.textContent = STEPS[at].text;
// The art and the words slide in from the side you are heading to; restarting the animation replays it.
steps.forEach(function (step) {
step.classList.toggle('card-onboarding__step--back', backwards);
step.style.animation = 'none';
void step.offsetWidth;
step.style.animation = '';
});
// One pill stretches to the new dot with its leading edge, then pulls its trailing edge after it.
pill.style.transition = backwards ? 'left ' + LEAD + ', right ' + TRAIL : 'right ' + LEAD + ', left ' + TRAIL;
pill.style.left = at * GAP + 'px';
pill.style.right = (STEPS.length - 1 - at) * GAP + 'px';
if (at > 0 && !back.parentNode) btns.insertBefore(back, next);
if (at === 0 && back.parentNode) back.remove();
next.textContent = at === STEPS.length - 1 ? 'Get started' : 'Next';
}
function onNext() {
go(at === STEPS.length - 1 ? 0 : at + 1);
}
function onBack() {
go(at - 1);
}
next.addEventListener('click', onNext);
back.addEventListener('click', onBack);
return function () {
next.removeEventListener('click', onNext);
back.removeEventListener('click', onBack);
};
}
document.querySelectorAll('.card-onboarding').forEach(initCardOnboarding);Goal: Create a "Onboarding" card.
Visual behavior: A three-step intro card: Next slides the new step in from the right and Back from the left, while the progress pill stretches across to the new dot and pulls its tail after it.
Technique:
- The art and the text of each step replay a slide-in keyframe; a back class flips its direction.
- The progress pill is one element whose left and right are set from the step. Its leading edge moves first and the trailing edge follows 0.12s later, so it stretches across and then catches up.
- Back appears from the second step, and on the last step the button reads Get started and loops to the start.
Accessibility: Real buttons, with the step's title and text in a polite live region so each change is read. The art and the dots are aria-hidden. Under prefers-reduced-motion the steps swap without sliding.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Onboarding" card.
Visual behavior: A three-step intro card: Next slides the new step in from the right and Back from the left, while the progress pill stretches across to the new dot and pulls its tail after it.
Technique:
- The art and the text of each step replay a slide-in keyframe; a back class flips its direction.
- The progress pill is one element whose left and right are set from the step. Its leading edge moves first and the trailing edge follows 0.12s later, so it stretches across and then catches up.
- Back appears from the second step, and on the last step the button reads Get started and loops to the start.
Accessibility: Real buttons, with the step's title and text in a polite live region so each change is read. The art and the dots are aria-hidden. Under prefers-reduced-motion the steps swap without sliding.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
A three-step intro card: Next slides the new step in from the right and Back from the left, while the progress pill stretches across to the new dot and pulls its tail after it.
Real buttons, with the step's title and text in a polite live region so each change is read. The art and the dots are aria-hidden. Under prefers-reduced-motion the steps swap without sliding.