GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Onboarding UI Design — 9 Copy-Paste Tour Widgets

Good onboarding ui design points to the next thing to click, on the screen the user already sees.

Auto-plays · click a tile to jump to its section · all nine in one zip

They are ordered the way a brand-new account meets them, not by popularity. A welcome modal (01) opens first, and then an onboarding checklist ui design (02) holds the remaining work. When someone can't find a control, a coachmark (03) dims the screen and lights up one spot; when several controls need explaining in order, a stepped tooltip tour (04) walks forward and back. A progress strip (05) says how far along things are, a hint balloon (06) and a pulse badge (07) sit beside features nobody opens, an empty-state card (08) stands in for a list with nothing in it, and confetti (09) closes out the flow. What the tour leaves behind as status marks is picked up by Status Badge Design UI, while the consent strip that should greet visitors even earlier sits in Cookie Consent Banner UI.

01Welcome modal stepper

What you see in this user onboarding ui design

The modal rises 18px as it opens, and of the three dots below the text, only the current one stretches sideways into a pill. A faint ::after copy trails each dot through the same keyframes 120ms behind, and the hand icon swings from a top pivot, damped -14°→10°→-6°→3°.

Welcome modal stepper
Open in a new tab

01 · Welcome modal stepper — Measured area 23.541% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateY 18px점→알약radius 16

Where to look in the code

The dot grows with scaleX rather than width, so nothing around it reflows.

.dots i {
  width: 20px; height: 8px;
  border-radius: $r-pill;
  background: #cbd5e1;
  transform: scaleX(.4);
  animation-name: step;
  animation-duration: $duration;
  animation-timing-function: steps(1, end);
  animation-iteration-count: infinite;
}
.d2 { animation-delay: calc(#{$duration} / 3); }
@keyframes step {
  0%     { transform: scaleX(1); background: $color; }
  33.33% { transform: scaleX(.4); background: #cbd5e1; }
}

02Checklist progress card

What happens on screen

The two remaining rows tick half a cycle apart, and a strikethrough draws across each label from the left. The bar leaves a 120ms-late translucent trail of itself, and the check settles through a damped 1.22→0.92→1.06→1 bounce.

Checklist progress card
Open in a new tab

02 · Checklist progress card — Measured area 0.872% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

scaleX 취소선50%→75%radius 16

How to read the code

The bar at the top of the card steps from 50% to 75% to 100% on the same beat.

.bar i {
  position: absolute; inset: 0;
  background: $color;
  transform: scaleX(.5);
  transform-origin: left center;
}
@keyframes fill {
  0%, 10%   { transform: scaleX(.5); }
  40%, 55%  { transform: scaleX(.75); }
  85%, 100% { transform: scaleX(1); }
}
@keyframes strike {
  0%, 18%   { transform: scaleX(0); }
  44%, 100% { transform: scaleX(1); }
}

03Coachmark spotlight

What moves

A scrim covers the screen with exactly one hole lighting up the target, and the hole travels from the left button to the right card. The balloon swings from its bottom tip, damped -2.5°→1.8°→-1°, while the plus icon pops elastically only while the spotlight sits on the button.

Coachmark spotlight
Open in a new tab

03 · Coachmark spotlight — Measured area 8.648% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

box-shadow 스포트라이트구멍 이동radius 12

What to change

The scrim is not a separate overlay element — it is the 9999px spread of one box-shadow on the hole itself.

.spot {
  position: absolute;
  left: 12px; top: 12px;
  width: 120px; height: 40px;
  border-radius: $r-control;
  box-shadow: 0 0 0 9999px rgba(23, 20, 26, .74);
  transform-origin: left center;
}
@keyframes move {
  0%, 18%  { transform: translateX(0) scaleX(1); }
  50%, 68% { transform: translateX(124px) scaleX(.8333); }
  100%     { transform: translateX(0) scaleX(1); }
}

04Stepped tooltip tour

What you see

Tooltips slide sideways each time the next button is pressed, and wrapping from the last step to the first never rewinds. The button rebounds .88→1.08 after each press, and the counter pops 1.3→.95 exactly when a slide arrives.

Stepped tooltip tour
Open in a new tab

04 · Stepped tooltip tour — Measured area 2.407% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

첫 장 복제translateX 단계radius 12

Where to look in the code

A copy of slide one sits at the end of the track, so the motion always continues in the same direction.

// four slides, not three — the last one is a copy of slide 1
.track { display: flex; width: 400%; }
.slide { width: 25%; flex: 0 0 25%; }
@keyframes walk {
  0%,   16% { transform: translateX(0); }
  25%,  41% { transform: translateX(-25%); }
  50%,  66% { transform: translateX(-50%); }
  75%, 100% { transform: translateX(-75%); }
}

05Progress strip

What happens on screen

Five segments fill from the left, 0.2s apart, and only the one that just filled gets a white veil that fades straight back out. Each fill briefly overshoots to 1.15 and clips at the segment edge, and the caption lifts 2px and settles as the last segment lands.

Progress strip
Open in a new tab

05 · Progress strip — Measured area 1.842% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

5칸 세그먼트채워진 칸 점등radius 999

How to read the code

Each segment is a fixed 47px rather than an equal-flex share, so the filled fraction matches the number of steps you wrote.

.seg { flex: 0 0 auto; width: 47px; height: 8px; overflow: hidden; }
.fill {
  position: absolute; inset: 0;
  background: $color;
  transform: scaleX(0);
  transform-origin: left center;
  animation-name: fill;
  animation-duration: $duration;
}
.s2 .fill { animation-delay: .2s; }
.s3 .fill { animation-delay: .4s; }
@keyframes fill {
  0%        { transform: scaleX(0); }
  16%, 100% { transform: scaleX(1); }
}

06Hint balloon

What moves

The balloon drifts 4px up and down above the control it points at, with its icon tucked inside. The bulb icon swings from a fixed top pivot, damped 13°→-8°→4.5°→-2°, and bounces 1.38→.88→1.07 when the balloon appears.

Hint balloon
Open in a new tab

06 · Hint balloon — Measured area 4.122% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateY 4px꼬리 일체형radius 12

What to change

Its triangular tail is an ::after on the balloon rather than a separate element, so the two never drift apart mid-animation.

.hint {
  position: relative;
  background: $color;
  transform: translateY(0);
}
.hint::after {
  content: "";
  position: absolute;
  left: 50%; bottom: -6px;
  width: 12px; height: 12px;
  background: $color;
  transform: translateX(-50%) rotate(45deg);
}
@keyframes nudge {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}

07Feature highlight pulse badge

What you see

A ring expands to 2.4 times the dot's size and fades out behind it. The sparkle icon swings ±10° with damping, and the dot fires a second pop (1.32→.86→1.06) at 1.24s for a double heartbeat.

Feature highlight pulse badge
Open in a new tab

07 · Feature highlight pulse badge — Measured area 0.577% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

scale 1→2.4레이아웃 불변radius 50%

Where to look in the code

Taking the dot out of flow with position: absolute means the ring can grow as large as it likes without nudging the line height of the menu row.

.dot {
  position: absolute;
  right: 10px; top: 8px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: $color;
}
.ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  background: $color;
  opacity: .55;
}
@keyframes pulse {
  0%        { opacity: .55; transform: scale(1); }
  70%, 100% { opacity: 0;   transform: scale(2.4); }
}

08Empty state guide card

What happens on screen

The dashed border travels slowly in one direction. The folder icon hangs from its bottom edge and swings 8°→-4.8°→2.4°→-.8°, while the CTA adds a restrained 1.08 pop.

Empty state guide card
Open in a new tab

08 · Empty state guide card — Measured area 2.603% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

점선 흐름dashoffsetradius 16

How to read the code

CSS border-style: dashed cannot be animated, so this moves stroke-dashoffset on an SVG rect instead, shifting exactly one notch (10+8=18) per cycle so the loop has no seam.

// the border is an SVG rect with stroke-dasharray: 10 8, not border: dashed
.edge rect {
  animation-name: flow;
  animation-duration: $duration;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes flow {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: -18; }
}

09Completion confetti

What moves

An orange seal squashes and springs back while twelve pieces drop from above it. The seal twists -6°→4°→-2° as it lands, settles 3.5°→-2°→1° after impact, and the message pops on the same beat.

Completion confetti
Open in a new tab

09 · Completion confetti — Measured area 5.094% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

12조각 시차scale 0.8→1.12radius 50%

What to change

Every piece carries its own horizontal position, color, tilt, and start time, which is what keeps them from reading as one batch released on a single frame.

.b {
  position: absolute;
  top: -12px;
  width: 6px; height: 10px;
  border-radius: $r-xs;
  opacity: 0;
}
.b3 { left: 26%; animation-delay: .21s; --r: 61deg; }
.b8 { left: 69%; animation-delay: .4s;  --r: -29deg; }
@keyframes fall {
  0%   { opacity: 0; transform: translateY(0) rotate(0deg); }
  12%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(168px) rotate(var(--r)); }
}

Where it breaks — your stagger silently becomes zero

The first render of 01 and 05 had no stagger at all. All three dots lit up together, and all five segments filled at once. The cause was writing animation-delay in one low-specificity rule while the shared rule used the shorthand animation: fill 2s ease-out infinite. A shorthand resets every sub-property you leave out, so the higher-specificity shared rule won and knocked the delay back to its initial 0s. Reading the computed style in the browser confirmed it: all five segments reported an animationDelay of 0s. Rewriting the shared rule as separate animation-name, animation-duration, animation-timing-function, and animation-iteration-count declarations brought the stagger back, and the moving-frame count for 05 went from 8 of 23 to 18 of 23. If a delay is going to arrive from another rule or from an inline style, never put the animation shorthand on that element.

The second thing to break was the segment width in 05. The five segments started out as flex: 1 1 0 equal shares with only the fill animated by scaleX, and the check step rejected that pairing outright. Equal-flex leaves the rendered width of each segment up to whatever the container happens to be, so it can drift away from a geometry whose fill ratio is computed from a fixed number. A fill written for a 78px segment running inside a segment that is really 62px throws every ratio off. Swapping the segments to flex: 0 0 auto at a fixed 47px settled it. At the 480px demo size the two versions look identical; the drift only shows up once the container is a width the fill was never written for.

Item 09 had to be reworked for a different reason. Starting the twelve confetti pieces outside the top edge (top: -12px) and dropping them 168px added their bounding boxes to the demo's own height, which put a scrollbar in its cell of the grid. The measure for "does this demo fit its own frame" is scrollHeight, and an element pushed away by transform still counts toward it. Nothing shrinks that number unless the box is genuinely clipped, whether by overflow: hidden or clip. Putting overflow: hidden on the .scene that holds the pieces brought it back into range.

The icons are not hand-drawn: eight Phosphor Duotone glyphs were downloaded and their SVG markup pasted inline. The first host returned 403 on all eight requests, and switching to the jsdelivr @phosphor-icons/core path fetched the set in one pass. Because the markup is baked into the file, nothing is fetched at runtime, and the dependency count stays at zero. Icons faked from stacked <div>s and border-radius drift the moment you scale them, while a real SVG only needs its width changed. One last measurement: of the nine, 07's pulse badge covers the least ground at 0.58% changed area, since an 8px dot and the ring behind it are all there is to move. To push the welcome screen further, the glowing sections of AI Landing Page Design Kit can sit under the first stepper. The password for the archive is gs9cg6k2, and typing it against the downloaded zip opens all nine folders at once.

Accessibility

In a bright onboarding palette, the thing that usually fails isn't motion — it's text contrast. Pastels and orange look great but still miss the body-text threshold on white. So all nine keep their text in ink (#17141a) or slate (#475569) and spend the pastels only on backgrounds, borders, and dots. The numbers below come from running the WCAG 2.1 relative-luminance formula on the exact pairs used.

Text Background Ratio Body (4.5:1)
Ink #17141a White card #ffffff 18.24:1 Passes
Ink #17141a Mint balloon #4cd4a6 9.79:1 Passes
Slate #475569 White card #ffffff 7.58:1 Passes
Brand blue #2f6df6 White card #ffffff 4.53:1 Passes, barely
White #ffffff Orange seal #ff4d1f 3.32:1 Fails — large text only
Mint #4cd4a6 White card #ffffff 1.86:1 Fails — decoration only

That 3.32:1 row is why the "All done" line in 09 sits on the tinted background in ink rather than inside the orange seal in white. On the motion side, every one of the nine settles into its end state under prefers-reduced-motion: reduce: 03 leaves the hole parked on its first target, 05 shows all five segments full, and 09 drops no pieces and keeps only the seal. Both rules are documented on MDN's prefers-reduced-motion page and on the WCAG contrast minimum page.

FAQ

In a guided tour ui design, does the coachmark scrim need its own full-screen div?

No. As in 03, putting box-shadow: 0 0 0 9999px on the element that forms the hole turns everything outside it into the scrim, and overflow: hidden on the parent stops it from leaking past the frame. A separate overlay element means recalculating two things every time the hole moves, which is exactly how they drift out of alignment. The checkout that receives a finished onboarding is covered next in Checkout Page Design.

Can I skip the duplicate slide and just jump back to the first step?

It works, but the screen visibly rewinds three panels instead of advancing one. Item 04 keeps a copy of slide one at the end so the motion is always forward, and the loop's jump back to the start is invisible because the panel it jumps from is that same slide. A similar loop is covered in CSS tooltip bubbles.

Why animate the progress bar with scaleX instead of width?

Both animate. But width recalculates layout on every frame, so in a card like 02 with several text rows, the lines below the bar can shift while it grows. transform: scaleX() with transform-origin: left center is handled by the compositor and leaves its neighbors alone. There's more on bars themselves in CSS progress bars and gauges.

Enter the archive password

The password is inside this article. You will find it as you read.