How do you animate form fields without harming accessibility?

Animated forms guide people while they type: labels that float above the field, underlines that grow on focus, inputs that shake on an error and buttons that show sending and success states. They rely on CSS transitions and selectors such as :focus-visible, :placeholder-shown and :user-invalid, with JavaScript only for richer states.

How to choose, performance and accessibility
  • Floating Label saves space on sign-up and contact forms, Password Strength and Shake Validation give instant feedback, and Submit Button States reassure during slow requests.
  • Always keep a real label element, announce errors in text rather than by motion or colour alone, and make sure no animation hides what the user typed.
  • Floating Label and Underline Focus are free with their full code; the other form effects come with Premium.

Floating Label

The label floats upward when the input is focused or filled.

CSS Focus Transform
View source code
HTML
<div class="demo-preview">
  <div class="floating-label-group">
    <input type="text" id="floating-input" placeholder=" " />
    <label for="floating-input">Your email</label>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --bg-elevated: #1a1a24;
  --text-muted: #8b8b93;
}

.floating-label-group {
  position: relative;
  width: 280px;
}

.floating-label-group input {
  width: 100%;
  padding: 16px 12px 8px;
  font-size: 1rem;
  font-family: inherit;
  background: rgba(255, 255, 255, 0.05);
  border: 2px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  color: white;
  outline: none;
  transition: all 0.3s ease;
}

.floating-label-group label {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  background: transparent;
  padding: 0 4px;
}

.floating-label-group input:focus,
.floating-label-group input:not(:placeholder-shown) {
  border-color: #6366f1;
  background: rgba(99, 102, 241, 0.05);
}

.floating-label-group input:focus+label,
.floating-label-group input:not(:placeholder-shown)+label {
  top: 0;
  font-size: 0.75rem;
  color: #6366f1;
  background: var(--bg-elevated);
}
JavaScript
/* Pure CSS — no JavaScript required */

Underline Focus

Animated line that extends from the center on focus.

CSS Focus Gradient
View source code
HTML
<div class="demo-preview">
  <div class="underline-input-group">
    <input type="text" id="underline-input" placeholder=" " />
    <label for="underline-input">Full name</label>
    <span class="underline"></span>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --text-muted: #8b8b93;
}

.underline-input-group {
  position: relative;
  width: 280px;
}

.underline-input-group input {
  width: 100%;
  padding: 12px 0;
  font-size: 1rem;
  font-family: inherit;
  background: transparent;
  border: none;
  border-bottom: 2px solid rgba(255, 255, 255, 0.2);
  color: white;
  outline: none;
}

.underline-input-group .underline {
  position: absolute;
  bottom: 0;
  left: 50%;
  width: 0;
  height: 2px;
  background: linear-gradient(90deg, #6366f1, #d946ef);
  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.underline-input-group input:focus~.underline {
  left: 0;
  width: 100%;
}

.underline-input-group label {
  position: absolute;
  left: 0;
  top: 12px;
  color: var(--text-muted);
  pointer-events: none;
  transition: all 0.3s ease;
}

.underline-input-group input:focus+label,
.underline-input-group input:not(:placeholder-shown)+label {
  top: -16px;
  font-size: 0.75rem;
  color: #6366f1;
}
JavaScript
/* Pure CSS — no JavaScript required */

Border Animation

Border that draws itself progressively around the input.

CSS Focus Border

Shake Validation

The input shakes to indicate a validation error.

JS Animation Validation

Success Checkmark

Animated checkmark that appears when the input is valid.

JS SVG Validation

Password Strength

Animated password strength indicator.

JS Animation Security

Search Expand

Search field that expands on click.

CSS Focus Transform

Input with Icon

Icon that animates on input focus.

CSS Focus Icon

Textarea Auto-grow

Text area that auto-grows.

JS Resize Interactive

Submit Button States

Button with animated loading, success and error states.

JS Animation States

Floating Label V2

Label that elegantly floats above the input with colored background.

CSS Focus Transform

Underline Animation

Line that extends from the center on focus.

CSS Focus Pseudo-element

Glow Input

Input with glow effect on focus.

CSS Box-shadow Focus

Checkbox Animated

Checkbox with animated checkmark that appears progressively.

CSS Transform Checkbox

Toggle iOS Style

Toggle switch with elastic iOS-style animation.

CSS Cubic-bezier Toggle

Radio Ripple

Radio buttons with ripple effect on selection.

CSS Animation Radio

Password Strength V2

Compact strength indicator with colored bar.

JS Animation Security

Select Animated

Dropdown with animated arrow on focus.

CSS Transform Select

Character Counter

Textarea with built-in character counter.

JS Counter Textarea

Search Expand V2

Compact search field that expands on focus.

CSS Focus Width

Pricing Toggle Odometer

Monthly/annual toggle where the price scrolls digit by digit (odometer effect) with an animated savings badge. Perfect for pushing annual subscriptions.

💡 Perfect for: pricing page, SaaS, plan comparison, annual upsell, Black Friday.

Vanilla JS Odometer Pricing Premium

My 2027 resolutions

12 day streak

Year launched

Resolution Tracker

Checkboxes with a springy fill, a drawn SVG tick and a hand-drawn wavy strikethrough. The line is struck through first (350 ms), then the row glides to the bottom (multi-item FLIP). The progress ring fills with a glint at each step; at 5/5, an understated celebration (golden halo, particles, message). Everything unchecks in a cascade, then the demo starts again.

💡 Perfect for: a habit app home screen, a New Year goals widget, a productivity landing page.

Forms Animation FLIP Progress
View source code
HTML
<div class="demo-preview">

      <div class="rcl-card" data-celebration-msg="Year launched" data-progress-label="Progress: {n} of {t}">

        <header class="rcl-header">
          <div class="rcl-header-info">
            <p class="rcl-title">My 2027 resolutions</p>
            <span class="rcl-streak" aria-label="12-day streak">
              <svg class="rcl-flame" viewBox="0 0 12 16" aria-hidden="true"><path d="M6 1C5 4 3 5 3 8a3 3 0 006 0C9 5 7 4 6 1zm-1.5 9c0-1.5 1.5-2 1.5-3.5 0 1.5 1.5 2 1.5 3.5a1.5 1.5 0 01-3 0z"/></svg>
              <span class="rcl-streak-count">12</span>
              <span class="rcl-streak-suffix">day streak</span>
            </span>
          </div>
          <div class="rcl-ring-wrap">
            <svg class="rcl-ring" viewBox="0 0 100 100"
                 role="img" aria-label="Progress: 0 of 5">
              <defs>
                <linearGradient id="rcl-grad" x1="0" y1="0" x2="1" y2="1">
                  <stop offset="0%" stop-color="#8b5cf6"/>
                  <stop offset="100%" stop-color="#f59e0b"/>
                </linearGradient>
              </defs>
              <circle class="rcl-ring-bg" cx="50" cy="50" r="44"/>
              <circle class="rcl-ring-fill" cx="50" cy="50" r="44"/>
            </svg>
            <div class="rcl-ring-text" aria-hidden="true">
              <span class="rcl-count">0</span>
              <span class="rcl-count-total">/5</span>
            </div>
          </div>
        </header>

        <ul class="rcl-list" role="list" aria-label="2027 resolutions">
          <li class="rcl-item" role="listitem" style="--acc:#a78bfa">
            <label class="rcl-label">
              <input type="checkbox" class="rcl-check-input">
              <span class="rcl-icon" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M8 2a1 1 0 1 0 0-2M5 5l-2 7h3M5 5l2 5 3-5"/></svg></span>
              <span class="rcl-checkbox" aria-hidden="true">
                <svg class="rcl-check-svg" viewBox="0 0 20 20" aria-hidden="true">
                  <path class="rcl-check-path" d="M4 10 l4.5 4.5 L16 6.5"/>
                </svg>
              </span>
              <span class="rcl-text-wrap">
                <span class="rcl-text">Run 3 times a week</span>
              </span>
            </label>
          </li>
          <li class="rcl-item" role="listitem" style="--acc:#60a5fa">
            <label class="rcl-label">
              <input type="checkbox" class="rcl-check-input">
              <span class="rcl-icon" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M3 2h5v12H3zM8 2h5v12H8z"/></svg></span>
              <span class="rcl-checkbox" aria-hidden="true">
                <svg class="rcl-check-svg" viewBox="0 0 20 20" aria-hidden="true">
                  <path class="rcl-check-path" d="M4 10 l4.5 4.5 L16 6.5"/>
                </svg>
              </span>
              <span class="rcl-text-wrap">
                <span class="rcl-text">Read 12 books</span>
              </span>
            </label>
          </li>
          <li class="rcl-item" role="listitem" style="--acc:#34d399">
            <label class="rcl-label">
              <input type="checkbox" class="rcl-check-input">
              <span class="rcl-icon" aria-hidden="true"><svg viewBox="0 0 16 16"><circle cx="8" cy="6" r="4"/><path d="M5 14c1-2 3-3 3-3s2 1 3 3"/></svg></span>
              <span class="rcl-checkbox" aria-hidden="true">
                <svg class="rcl-check-svg" viewBox="0 0 20 20" aria-hidden="true">
                  <path class="rcl-check-path" d="M4 10 l4.5 4.5 L16 6.5"/>
                </svg>
              </span>
              <span class="rcl-text-wrap">
                <span class="rcl-text">Meditate every morning</span>
              </span>
            </label>
          </li>
          <li class="rcl-item" role="listitem" style="--acc:#fbbf24">
            <label class="rcl-label">
              <input type="checkbox" class="rcl-check-input">
              <span class="rcl-icon" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M3 7h10v7H3zM6 7V4M10 7V4"/></svg></span>
              <span class="rcl-checkbox" aria-hidden="true">
                <svg class="rcl-check-svg" viewBox="0 0 20 20" aria-hidden="true">
                  <path class="rcl-check-path" d="M4 10 l4.5 4.5 L16 6.5"/>
                </svg>
              </span>
              <span class="rcl-text-wrap">
                <span class="rcl-text">Cook at home 4×/week</span>
              </span>
            </label>
          </li>
          <li class="rcl-item" role="listitem" style="--acc:#f472b6">
            <label class="rcl-label">
              <input type="checkbox" class="rcl-check-input">
              <span class="rcl-icon" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M11 2v8M8 10a2 2 0 1 0 .01 0M11 2 8 4"/></svg></span>
              <span class="rcl-checkbox" aria-hidden="true">
                <svg class="rcl-check-svg" viewBox="0 0 20 20" aria-hidden="true">
                  <path class="rcl-check-path" d="M4 10 l4.5 4.5 L16 6.5"/>
                </svg>
              </span>
              <span class="rcl-text-wrap">
                <span class="rcl-text">Learn the piano</span>
              </span>
            </label>
          </li>
        </ul>

        <div class="rcl-celebration" role="status" aria-live="polite" aria-atomic="true">
          <canvas class="rcl-celebration-canvas" aria-hidden="true"></canvas>
          <p class="rcl-celebration-msg">Year launched</p>
        </div>

      </div><!-- .rcl-card -->

    </div>
CSS
*,
*::before,
*::after { box-sizing: border-box }

body { margin: 0; background: #0a0a0f; color: #fff; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: Inter, -apple-system, sans-serif; overflow: hidden; }

.demo-preview { display: flex; align-items: center; justify-content: center; width: 100%; min-height: 300px; position: relative; }

/* Card container */
.rcl-card {
  background: linear-gradient(160deg, #16161f 0%, #111119 100%);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 20px;
  padding: 14px 16px;
  width: 100%;
  max-width: 330px;
  position: relative;
  overflow: visible;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  box-sizing: border-box;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.05),
    inset 0 0 0 1px rgba(0,0,0,0.6),
    0 20px 60px rgba(0,0,0,0.5),
    0 4px 16px rgba(0,0,0,0.35);
}

/* Header */
.rcl-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}

.rcl-header-info {
  flex: 1;
  min-width: 0;
}

.rcl-title {
  font-size: 13.5px;
  font-weight: 800;
  color: #f4f4f5;
  letter-spacing: -0.025em;
  margin: 0 0 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rcl-streak-count { font-weight: 600; font-variant-numeric: tabular-nums; }
.rcl-streak-suffix { opacity: .85; }
.rcl-streak {
  font-size: 11px;
  color: #52525b;
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Flame SVG icon */
.rcl-flame {
  width: 11px;
  height: 13px;
  fill: #f97316;
  flex-shrink: 0;
}

/* Progress ring */
.rcl-ring-wrap {
  position: relative;
  flex-shrink: 0;
  width: 56px;
  height: 56px;
}

.rcl-ring {
  width: 56px;
  height: 56px;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
  overflow: visible;
  display: block;
}

.rcl-ring-bg {
  fill: none;
  stroke: #1c1c2a;
  stroke-width: 7;
}

.rcl-ring-fill {
  fill: none;
  stroke: url(#rcl-grad);
  stroke-width: 7;
  stroke-linecap: round;
  transition: stroke-dashoffset 0.5s cubic-bezier(0.34, 1.56, 0.64, 1),
              filter 0.5s ease;
}

.rcl-ring.is-complete .rcl-ring-fill {
  filter: drop-shadow(0 0 8px rgba(245,158,11,0.65));
}

@keyframes rcl-ring-pulse {
  0%   { filter: drop-shadow(0 0 0 transparent); }
  40%  { filter: drop-shadow(0 0 9px rgba(139,92,246,0.7)); }
  100% { filter: drop-shadow(0 0 2px rgba(139,92,246,0.1)); }
}

.rcl-ring-fill.rcl-pulsed {
  animation: rcl-ring-pulse 0.65s ease-out forwards;
}

.rcl-ring-text {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.rcl-count {
  font-size: 15px;
  font-weight: 800;
  color: #f4f4f5;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
}

.rcl-count-total {
  font-size: 9px;
  color: #52525b;
  line-height: 1;
  margin-top: 2px;
}

/* List */
.rcl-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

/* Item */
.rcl-item {
  position: relative;
  will-change: transform;
}

/* Label row */
.rcl-label {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 8px;
  border-radius: 9px;
  cursor: pointer;
  position: relative;
  transition: background 0.12s;
  outline: none;
}

.rcl-label:hover { background: rgba(255,255,255,0.04); }

/* Focus ring only on keyboard navigation */
.rcl-label:has(.rcl-check-input:focus-visible) {
  background: rgba(139,92,246,0.08);
  outline: 2px solid rgba(139,92,246,0.45);
  outline-offset: 1px;
}

/* Native checkbox — invisible but accessible */
.rcl-check-input {
  position: absolute;
  opacity: 0;
  width: 22px;
  height: 22px;
  margin: 0;
  cursor: pointer;
  z-index: 2;
  left: 8px;
  top: 50%;
  transform: translateY(-50%);
}

/* Item icon */
.rcl-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  color: var(--acc, #7c3aed);
  transition: color 0.3s;
  pointer-events: none;
}

.rcl-icon svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rcl-item.is-done .rcl-icon { color: #3a3a52; }

/* Custom checkbox visual */
.rcl-checkbox {
  width: 20px;
  height: 20px;
  border: 1.5px solid #38384e;
  border-radius: 6px;
  flex-shrink: 0;
  position: relative;
  transition: border-color 0.2s;
  pointer-events: none;
  box-sizing: border-box;
}

@keyframes rcl-spring-fill {
  0%   { transform: scale(0); }
  52%  { transform: scale(1.18); }
  72%  { transform: scale(0.91); }
  86%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

@keyframes rcl-spring-unfill {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.08); }
  100% { transform: scale(0); }
}

.rcl-checkbox::before {
  content: '';
  position: absolute;
  inset: -1px;
  background: #8b5cf6;
  border-radius: 5px;
  transform: scale(0);
  transform-origin: center;
}

.rcl-item.is-done .rcl-checkbox {
  border-color: #8b5cf6;
}

.rcl-item.is-done .rcl-checkbox::before {
  animation: rcl-spring-fill 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.rcl-item.unchecking .rcl-checkbox::before {
  animation: rcl-spring-unfill 0.25s ease-in forwards;
}

/* SVG checkmark */
.rcl-check-svg {
  position: absolute;
  inset: 1px;
  width: calc(100% - 2px);
  height: calc(100% - 2px);
  pointer-events: none;
  z-index: 1;
  overflow: visible;
}

.rcl-check-path {
  fill: none;
  stroke: #fff;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
  transition: stroke-dashoffset 0.25s cubic-bezier(0.22, 1, 0.36, 1) 0.08s;
}

.rcl-item.is-done .rcl-check-path {
  stroke-dashoffset: 0;
}

.rcl-item.unchecking .rcl-check-path {
  stroke-dashoffset: 20;
  transition: stroke-dashoffset 0.15s ease-in;
}

/* Text + strikethrough wrapper */
.rcl-text-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

.rcl-text {
  font-size: 12.5px;
  font-weight: 500;
  color: #d4d4d8;
  line-height: 1.4;
  display: block;
  transition: color 0.3s 0.05s;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  letter-spacing: -0.005em;
}

.rcl-item.is-done .rcl-text { color: #3f3f56; }

/* Animated wavy strikethrough */
.rcl-strike-svg {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 100%;
  height: 8px;
  pointer-events: none;
  overflow: visible;
}

.rcl-strike-path {
  fill: none;
  stroke: none;
  stroke-width: 1.2;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.rcl-item.is-done .rcl-strike-path {
  stroke: #4a4a68;
  /* dashoffset set by JS; transition to 0 draws left→right over 350ms */
  transition: stroke-dashoffset 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.rcl-item.unchecking .rcl-strike-path {
  stroke: #4a4a68;
  transition: stroke-dashoffset 0.15s ease-in;
}

/* Celebration overlay */
.rcl-celebration {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
  z-index: 10;
  border-radius: 20px;
  background: rgba(8,8,18,0.74);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

.rcl-celebration.is-visible { opacity: 1; }

.rcl-celebration-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 20px;
  pointer-events: none;
}

.rcl-celebration-msg {
  font-size: 18px;
  font-weight: 800;
  color: #f59e0b;
  letter-spacing: -0.01em;
  margin: 0;
  position: relative;
  z-index: 1;
  text-shadow: 0 0 28px rgba(245,158,11,0.5);
  transform: translateY(10px) scale(0.92);
  opacity: 0;
  transition: transform 0.48s cubic-bezier(0.34, 1.56, 0.64, 1) 0.15s,
              opacity 0.3s ease 0.15s;
}

.rcl-celebration.is-visible .rcl-celebration-msg {
  transform: translateY(0) scale(1);
  opacity: 1;
}

/* Ring halo at 5/5 */
@keyframes rcl-ring-halo {
  0%,100% { opacity:1; transform: rotate(-90deg) scale(1); }
  50%      { opacity:0.85; transform: rotate(-90deg) scale(1.06); }
}

.rcl-ring.is-complete {
  animation: rcl-ring-halo 1.8s ease-in-out infinite;
  transform-origin: 50% 50%;
}

/* prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .rcl-checkbox::before,
  .rcl-item.is-done .rcl-checkbox::before,
  .rcl-item.unchecking .rcl-checkbox::before {
    animation: none !important;
    transform: scale(var(--rcl-fill-final, 0));
    transition: transform 0.15s linear;
  }
  .rcl-ring-fill,
  .rcl-ring-fill.rcl-pulsed,
  .rcl-check-path,
  .rcl-item.unchecking .rcl-check-path,
  .rcl-celebration,
  .rcl-celebration-msg,
  .rcl-text,
  .rcl-label { transition: none !important; animation: none !important; }
  .rcl-strike-path,
  .rcl-item.unchecking .rcl-strike-path { transition: none !important; }
  .rcl-ring.is-complete { animation: none !important; }
}

@media (prefers-reduced-transparency: reduce) {
  .rcl-celebration { backdrop-filter: none; -webkit-backdrop-filter: none; background: rgba(8,8,18,0.93); }
}

/* High-contrast */
.rcl-card.high-contrast .rcl-text { color: #fff; }
.rcl-card.high-contrast .rcl-item.is-done .rcl-text { color: #71717a; }
.rcl-card.high-contrast .rcl-checkbox { border-color: #fff; border-width: 2px; }
.rcl-card.high-contrast .rcl-strike-path { stroke: #a1a1aa; stroke-width: 2; }
.rcl-card.high-contrast .rcl-celebration-msg { color: #ffd700; text-shadow: none; }
JavaScript
(function () {
  'use strict';

  var REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var CIRC = 2 * Math.PI * 44;

  /* ── Inject wavy strikethrough SVG */
  function injectStrike(textWrap) {
    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.classList.add('rcl-strike-svg');
    svg.setAttribute('viewBox', '0 0 100 8');
    svg.setAttribute('preserveAspectRatio', 'none');
    svg.setAttribute('aria-hidden', 'true');

    var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.classList.add('rcl-strike-path');
    path.setAttribute('d', 'M 1 4 C 12 2.6 22 5.4 33 4 C 44 2.6 54 5.4 65 4 C 76 2.6 86 5.4 99 4');
    svg.appendChild(path);
    textWrap.appendChild(svg);

    requestAnimationFrame(function () {
      var len = path.getTotalLength() || 99;
      path.style.strokeDasharray = len;
      path.style.strokeDashoffset = len;

      var textEl = textWrap.querySelector('.rcl-text');
      if (textEl && textEl.textContent) {
        var mc = document.createElement('canvas');
        var ct = mc.getContext('2d');
        if (ct) {
          var cs = window.getComputedStyle(textEl);
          ct.font = [cs.fontStyle, cs.fontWeight, cs.fontSize, cs.fontFamily].filter(Boolean).join(' ');
          var tw = ct.measureText(textEl.textContent).width;
          svg.style.width = Math.ceil(Math.min(tw + 4, textWrap.offsetWidth)) + 'px';
        }
      }
    });
    return path;
  }

  /* ── FLIP: cancel any in-progress transforms, then animate to bottom */
  function flipToBottom(item, list) {
    /* Step 0 — commit any in-flight FLIP so we measure true positions */
    var all = Array.prototype.slice.call(list.children);
    var hadFlight = false;
    all.forEach(function (el) {
      if (el.style.transform && el.style.transform !== '') {
        el.style.transition = '';
        el.style.transform = '';
        hadFlight = true;
      }
    });
    if (hadFlight) void list.offsetHeight;

    /* Step 1 — FIRST positions */
    var firstMap = new Map();
    all.forEach(function (el) { firstMap.set(el, el.getBoundingClientRect().top); });

    /* Step 2 — mutate DOM */
    list.appendChild(item);

    /* Step 3 — apply inverse transforms */
    var toAnimate = [];
    Array.prototype.slice.call(list.children).forEach(function (child) {
      var first = firstMap.get(child);
      if (first === undefined) return;
      var dy = first - child.getBoundingClientRect().top;
      if (Math.abs(dy) < 0.5) return;
      child.style.transition = 'none';
      child.style.transform = 'translateY(' + dy + 'px)';
      toAnimate.push(child);
    });
    if (!toAnimate.length) return;
    void list.offsetHeight;

    /* Step 4 — animate to natural position */
    toAnimate.forEach(function (el) {
      el.style.transition = 'transform 0.38s cubic-bezier(0.22, 1, 0.36, 1)';
      el.style.transform = '';
      el.addEventListener('transitionend', function h(e) {
        if (e.propertyName !== 'transform') return;
        el.removeEventListener('transitionend', h);
        el.style.transition = '';
      });
    });
  }

  /* ── Update ring + pulse glow */
  function updateRing(card, count, total) {
    var ringFill = card.querySelector('.rcl-ring-fill');
    var ring = card.querySelector('.rcl-ring');
    var countEl = card.querySelector('.rcl-count');

    var offset = CIRC * (1 - count / total);
    ringFill.style.strokeDasharray = CIRC;
    ringFill.style.strokeDashoffset = offset;
    if (countEl) countEl.textContent = count;
    ring.setAttribute('aria-label', (card.dataset.progressLabel || '{n} / {t}').replace('{n}', count).replace('{t}', total));

    if (count === total) {
      ring.classList.add('is-complete');
    } else {
      ring.classList.remove('is-complete');
    }

    /* Pulse glow on each step */
    if (!REDUCED && count > 0) {
      ringFill.classList.remove('rcl-pulsed');
      void ringFill.offsetWidth;
      ringFill.classList.add('rcl-pulsed');
    }
  }

  /* ── Canvas particles */
  function burstParticles(canvas) {
    if (!canvas || REDUCED) return;
    var parent = canvas.parentElement;
    var rect = parent.getBoundingClientRect();
    canvas.width = rect.width || 330;
    canvas.height = rect.height || 250;
    var ctx = canvas.getContext('2d');
    if (!ctx) return;

    var cx = canvas.width * 0.5;
    var cy = canvas.height * 0.4;
    var COLORS = ['#f59e0b', '#fbbf24', '#fcd34d', '#d97706', '#fde68a', '#fff7ed'];
    var particles = [];

    for (var i = 0; i < 36; i++) {
      var angle = Math.random() * Math.PI * 2;
      var speed = 1.8 + Math.random() * 3.2;
      var isSquare = Math.random() > 0.55;
      particles.push({
        x: cx, y: cy,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed - (0.5 + Math.random() * 1.5),
        r: isSquare ? 2 + Math.random() * 3 : 1.5 + Math.random() * 2.5,
        alpha: 1,
        color: COLORS[Math.floor(Math.random() * COLORS.length)],
        rot: Math.random() * Math.PI * 2,
        rotV: (Math.random() - 0.5) * 0.22,
        square: isSquare
      });
    }

    function frame() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      var alive = false;
      particles.forEach(function (pt) {
        pt.x += pt.vx; pt.y += pt.vy; pt.vy += 0.09;
        pt.alpha -= 0.016; pt.rot += pt.rotV;
        if (pt.alpha > 0) {
          alive = true;
          ctx.save();
          ctx.globalAlpha = Math.max(0, pt.alpha);
          ctx.translate(pt.x, pt.y);
          ctx.rotate(pt.rot);
          ctx.fillStyle = pt.color;
          if (pt.square) {
            ctx.fillRect(-pt.r, -pt.r, pt.r * 2, pt.r * 2);
          } else {
            ctx.beginPath();
            ctx.moveTo(0, -pt.r * 1.5);
            ctx.lineTo(pt.r * 0.6, 0);
            ctx.lineTo(0, pt.r * 1.5);
            ctx.lineTo(-pt.r * 0.6, 0);
            ctx.closePath();
            ctx.fill();
          }
          ctx.restore();
        }
      });
      if (alive) requestAnimationFrame(frame);
      else ctx.clearRect(0, 0, canvas.width, canvas.height);
    }
    requestAnimationFrame(frame);
  }

  /* ── Init one .rcl-card */
  function initCard(card) {
    var list = card.querySelector('.rcl-list');
    var inputs = Array.prototype.slice.call(card.querySelectorAll('.rcl-check-input'));
    var celebration = card.querySelector('.rcl-celebration');
    var celebMsg = card.querySelector('.rcl-celebration-msg');
    var celebCanvas = card.querySelector('.rcl-celebration-canvas');
    var TOTAL = inputs.length;
    if (!list || !TOTAL) return;

    /* Celebration text from HTML attribute */
    var celebText = card.dataset.celebrationMsg || 'Bonne année';
    if (celebMsg) celebMsg.textContent = celebText;

    /* Init ring */
    var ringFill = card.querySelector('.rcl-ring-fill');
    if (ringFill) { ringFill.style.strokeDasharray = CIRC; ringFill.style.strokeDashoffset = CIRC; }

    /* Inject strikethroughs */
    var strikePaths = new WeakMap();
    inputs.forEach(function (inp) {
      var tw = inp.closest('.rcl-item').querySelector('.rcl-text-wrap');
      if (tw) strikePaths.set(inp, injectStrike(tw));
    });

    /* Original DOM order */
    var origOrder = Array.prototype.slice.call(list.querySelectorAll('.rcl-item'));

    function countChecked() {
      return inputs.filter(function (i) { return i.checked; }).length;
    }

    /* --- Check item: strikethrough first, FLIP after 350ms --- */
    function checkItem(item, inp) {
      if (item.classList.contains('is-done')) return;
      item.classList.remove('unchecking');
      item.classList.add('is-done');
      item.querySelector('.rcl-checkbox').style.setProperty('--rcl-fill-final', '1');

      /* Start strikethrough immediately */
      var sp = strikePaths.get(inp);
      if (sp) requestAnimationFrame(function () { sp.style.strokeDashoffset = '0'; });

      /* FLIP after strikethrough draws (~350ms) */
      if (!REDUCED) {
        setTimeout(function () { flipToBottom(item, list); }, 350);
      } else {
        list.appendChild(item);
      }

      var n = countChecked();
      updateRing(card, n, TOTAL);

      /* Celebration fires after last FLIP completes (350ms strike + 380ms flip + buffer) */
      if (n === TOTAL) setTimeout(triggerCelebration, 850);
    }

    /* --- Uncheck item --- */
    function uncheckItem(item, inp, silent) {
      if (!item.classList.contains('is-done')) return;
      item.classList.add('unchecking');
      item.classList.remove('is-done');
      item.querySelector('.rcl-checkbox').style.setProperty('--rcl-fill-final', '0');

      var sp = strikePaths.get(inp);
      if (sp) {
        var len = parseFloat(sp.style.strokeDasharray) || 102;
        sp.style.strokeDashoffset = len;
      }

      setTimeout(function () { item.classList.remove('unchecking'); }, 300);
      updateRing(card, countChecked(), TOTAL);
      if (!silent) resetAutoTimer();
    }

    /* --- Celebration --- */
    function triggerCelebration() {
      if (!celebration) return;
      celebration.classList.add('is-visible');
      burstParticles(celebCanvas);

      setTimeout(function () {
        celebration.classList.remove('is-visible');

        /* Cascade uncheck (bottom → top) */
        var done = Array.prototype.slice.call(list.querySelectorAll('.rcl-item.is-done')).reverse();
        done.forEach(function (item, idx) {
          setTimeout(function () {
            var inp = item.querySelector('.rcl-check-input');
            if (inp) { inp.checked = false; uncheckItem(item, inp, true); }
            if (idx === done.length - 1) {
              setTimeout(function () {
                origOrder.forEach(function (it) { list.appendChild(it); });
                updateRing(card, 0, TOTAL);
                card.querySelector('.rcl-ring').classList.remove('is-complete');
                autoRunning = false;
                resetAutoTimer();
              }, 350);
            }
          }, idx * 140);
        });
      }, 2500);
    }

    /* --- User interaction --- */
    inputs.forEach(function (inp) {
      var item = inp.closest('.rcl-item');
      inp.addEventListener('change', function () {
        if (inp.checked) { checkItem(item, inp); resetAutoTimer(); }
        else { uncheckItem(item, inp, false); }
      });
    });

    /* --- Auto-play sequence --- */
    var autoTimer = null;
    var autoRunning = false;

    function resetAutoTimer() {
      if (autoTimer) clearTimeout(autoTimer);
      if (!autoRunning) autoTimer = setTimeout(startAutoSequence, 5000);
    }

    function startAutoSequence() {
      autoRunning = true;

      /* Full reset */
      inputs.forEach(function (inp) { inp.checked = false; });
      Array.prototype.slice.call(list.querySelectorAll('.rcl-item')).forEach(function (item) {
        /* Cancel in-flight transforms */
        item.style.transition = '';
        item.style.transform = '';
        item.classList.remove('is-done', 'unchecking');
        item.querySelector('.rcl-checkbox').style.setProperty('--rcl-fill-final', '0');
        var inp2 = item.querySelector('.rcl-check-input');
        var sp = inp2 ? strikePaths.get(inp2) : null;
        if (sp) { sp.style.strokeDashoffset = parseFloat(sp.style.strokeDasharray) || 102; }
      });
      origOrder.forEach(function (it) { list.appendChild(it); });
      updateRing(card, 0, TOTAL);
      card.querySelector('.rcl-ring').classList.remove('is-complete');

      /* Check items one by one; 900ms gap ensures each strike+FLIP completes */
      var seq = inputs.slice(); /* copy — snapshot of current inputs */
      var idx = 0;
      function next() {
        if (idx >= seq.length) return;
        var inp = seq[idx++];
        inp.checked = true;
        checkItem(inp.closest('.rcl-item'), inp);
        if (idx < seq.length) setTimeout(next, 900);
      }
      setTimeout(next, 600);
    }

    /* Pause when off-screen */
    if (typeof IntersectionObserver !== 'undefined') {
      new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) { if (autoTimer) clearTimeout(autoTimer); }
          else if (!autoRunning) resetAutoTimer();
        });
      }, { threshold: 0.1 }).observe(card);
    }

    resetAutoTimer();
  }

  /* ── Init all cards */
  function init() { document.querySelectorAll('.rcl-card').forEach(initCard); }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
  else init();

  window.ResolutionTracker = { init: init };
})();

Ink Signature Pledge

A real signature pad rendered like a fountain pen: the nib's bevel gives thick and thin strokes depending on direction, speed and stylus pressure, the blue-black ink pools where the pen rests, feathers finely into the cream paper fibres, darkens where strokes cross and dries before your eyes (glossy → matte in 1.5 s). When idle, a handwritten signature draws itself at the pace of a human hand. Undo, clear, confirm: transparent PNG + SVG export and a sgn:sign event. Keyboard alternative (typed name in cursive), prefers-reduced-motion → direct drawing.

💡 Perfect for: a New Year “contract with yourself”, quote or T&C acceptance, contract e-signature, approval forms, guest books — one component for every case.

Canvas Form Handwritten Paper Premium

Frequently Asked Questions

How to create a floating label in CSS? +

Place the label after the input in the HTML. Use the :placeholder-shown selector to detect if the input is empty, and the :focus selector for the active state. Animate the label position and size with transform and transition.

How to style form validation in CSS? +

Use the :valid and :invalid pseudo-classes to change input styles based on their state. Add a green or red border with a smooth transition. Combine with :not(:placeholder-shown) to only validate filled fields.

Are animated forms accessible? +

Yes, as long as you keep labels associated with inputs (for/id attribute), maintain sufficient contrast, and do not hide error messages behind animations. Always test with a screen reader.

How to animate input focus? +

Use the :focus-within selector on the container and :focus on the input. Animate the border, add a colored box-shadow, or reveal an animated line with a ::after pseudo-element and a width transition.

What is the best framework for animated forms? +

For simple forms, pure CSS is enough with the :focus, :valid, :invalid selectors. For complex forms, combine with vanilla JavaScript for real-time validation. No framework needed — Effect.Labs provides ready-to-use CSS code.