16 CSS Pulse Animations12 / 16

CSS + JSMIT licensed

Form Submitting Pulse

A B2B signup form with a submit button that transforms into a processing-pulse state on click — text disappears, replaced by a soft scale-pulse loader indicating the API request is in flight. Muted purple (#6b46c1) accent on soft lavender background (#f5f3ff) — Superhuman / Vercel / Linear signup aesthetic. The pulse is DELIBERATELY calm (not spinning, not bouncing) because form submission is a trust context — the user just gave you their data and needs to feel it's being handled competently.

Published

Live Demo
Try it

The code

<div class="pl-12">
  <div class="pl-12__card">
    <p class="pl-12__eyebrow">Free · Personal plan</p>
    <h1 class="pl-12__title">Create your account</h1>
    <p class="pl-12__sub">14-day trial, no credit card required.</p>
    <form class="pl-12__form" id="pl-12-form" novalidate>
      <label class="pl-12__field">
        <span class="pl-12__field-label">Work email</span>
        <input type="email" name="email" value="alex@studio.com" autocomplete="email" placeholder="you@work.com" />
      </label>
      <label class="pl-12__field">
        <span class="pl-12__field-label">Company</span>
        <input type="text" name="company" value="Northlight Studio" placeholder="Your company name" />
      </label>
      <div class="pl-12__field">
        <span class="pl-12__field-label">Team size</span>
        <div class="pl-12__pills" role="radiogroup" aria-label="Team size">
          <button type="button" class="pl-12__pill" role="radio" aria-checked="false">1–5</button>
          <button type="button" class="pl-12__pill pl-12__pill--sel" role="radio" aria-checked="true">6–20</button>
          <button type="button" class="pl-12__pill" role="radio" aria-checked="false">21–100</button>
          <button type="button" class="pl-12__pill" role="radio" aria-checked="false">100+</button>
        </div>
      </div>
      <button type="submit" class="pl-12__submit" id="pl-12-submit" aria-live="polite">
        <span class="pl-12__submit-text">Create account</span>
        <span class="pl-12__submit-loader" aria-hidden="true"></span>
        <svg class="pl-12__submit-check" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <polyline points="4 12 10 18 20 6"/>
        </svg>
      </button>
      <p class="pl-12__foot">By continuing you agree to our <a href="#">Terms</a> and <a href="#">Privacy Policy</a>.</p>
    </form>
  </div>
</div>
.pl-12 {
  --page-bg: #f5f3ff;
  --card: #ffffff;
  --ink: #1e1b4b;
  --sub: #6b7280;
  --brand: #6b46c1;
  --brand-dark: #553c9a;
  --brand-soft: rgba(107,70,193,.1);
  --brand-glow: rgba(107,70,193,.35);
  --success: #10b981;
  --line: #e9d5ff;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  padding: 32px;
  background: radial-gradient(70% 90% at 20% 0%, rgba(167,139,250,.14) 0%, transparent 55%), radial-gradient(60% 80% at 100% 100%, rgba(107,70,193,.08) 0%, transparent 55%), var(--page-bg);
  color: var(--ink);
}

.pl-12 *,
.pl-12 *::before,
.pl-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pl-12 ::selection {
  background: var(--brand);
  color: #fff;
}

.pl-12__card {
  width: min(440px,100%);
  padding: 34px 32px 28px;
  border-radius: 16px;
  background: var(--card);
  box-shadow: 0 20px 50px -12px rgba(76,29,149,.2),0 4px 12px -6px rgba(76,29,149,.08);
  border: 1px solid var(--line);
}

.pl-12__eyebrow {
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--brand);
  margin-bottom: 8px;
}

.pl-12__title {
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -.02em;
  margin-bottom: 6px;
}

.pl-12__sub {
  font-size: .85rem;
  color: var(--sub);
  margin-bottom: 20px;
}

.pl-12__form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.pl-12__field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.pl-12__field-label {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--sub);
}

.pl-12__field input {
  padding: 11px 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fafaff;
  color: var(--ink);
  font-family: inherit;
  font-size: .9rem;
  transition: border-color .15s ease,background .15s ease,box-shadow .15s ease;
}

.pl-12__field input:focus {
  outline: none;
  border-color: var(--brand);
  background: var(--card);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.pl-12__pills {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.pl-12__pill {
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: #fafaff;
  color: var(--sub);
  font-family: inherit;
  font-size: .82rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color .15s ease,background .15s ease,color .15s ease;
}

.pl-12__pill:hover {
  border-color: var(--brand);
  color: var(--ink);
}

.pl-12__pill--sel {
  background: var(--brand);
  color: #fff;
  border-color: var(--brand);
}
/* Submit button with 3-state machine: resting / submitting / success */

.pl-12__submit {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  padding: 12px 22px;
  border: none;
  border-radius: 9px;
  background: var(--brand);
  color: #fff;
  font-family: inherit;
  font-size: .95rem;
  font-weight: 700;
  cursor: pointer;
  overflow: hidden;
  transition: background .18s ease,transform .12s ease;
  margin-top: 4px;
}

.pl-12__submit:hover {
  background: var(--brand-dark);
  transform: translateY(-1px);
}

.pl-12__submit:active {
  transform: translateY(1px);
}

.pl-12__submit:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}

.pl-12__submit-text {
  transition: opacity .2s ease;
}

.pl-12__submit-loader {
  position: absolute;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  transition: opacity .2s ease;
}

.pl-12__submit-check {
  position: absolute;
  opacity: 0;
  transition: opacity .2s ease;
}
/* Submitting state — text fades, loader appears + pulses */

.pl-12__submit.is-submitting {
  pointer-events: none;
}

.pl-12__submit.is-submitting .pl-12__submit-text {
  opacity: 0;
}

.pl-12__submit.is-submitting .pl-12__submit-loader {
  opacity: 1;
  animation: pl-12-loader 1.2s ease-in-out infinite;
}

@keyframes pl-12-loader {
  0%,
    100% {
    transform: scale(.85);
    box-shadow: 0 0 0 0 rgba(255,255,255,.5);
  }

  50% {
    transform: scale(1.05);
    box-shadow: 0 0 0 6px rgba(255,255,255,0);
  }
}
/* Success state — loader gone, checkmark visible, green button */

.pl-12__submit.is-success {
  background: var(--success);
  pointer-events: none;
}

.pl-12__submit.is-success .pl-12__submit-text,
.pl-12__submit.is-success .pl-12__submit-loader {
  opacity: 0;
}

.pl-12__submit.is-success .pl-12__submit-loader {
  animation: none;
}

.pl-12__submit.is-success .pl-12__submit-check {
  opacity: 1;
}

.pl-12__foot {
  font-size: .72rem;
  color: var(--sub);
  text-align: center;
  margin-top: 6px;
}

.pl-12__foot a {
  color: var(--brand);
  text-decoration: underline;
  font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
  .pl-12__submit-loader {
    animation: none!important;
    transform: none!important;
    box-shadow: none!important;
  }

  .pl-12__submit {
    transition: background .18s ease!important;
  }
}
(function(){
  var root = document.querySelector('.pl-12');
  if (!root) return;
  var form = root.querySelector('#pl-12-form');
  var btn  = root.querySelector('#pl-12-submit');
  var pills = root.querySelectorAll('.pl-12__pill');
  if (!form || !btn) return;

  pills.forEach(function(p){
    p.addEventListener('click', function(){
      pills.forEach(function(x){
        x.classList.remove('pl-12__pill--sel');
        x.setAttribute('aria-checked','false');
      });
      p.classList.add('pl-12__pill--sel');
      p.setAttribute('aria-checked','true');
    });
  });

  var cycleTimer = null;
  function runCycle(){
    // Reset to resting
    btn.classList.remove('is-submitting','is-success');
    // Enter submitting state
    setTimeout(function(){
      btn.classList.add('is-submitting');
    }, 800);
    // Enter success state
    setTimeout(function(){
      btn.classList.remove('is-submitting');
      btn.classList.add('is-success');
    }, 3300);
    // Reset for next loop (demo purposes — production removes this)
    cycleTimer = setTimeout(runCycle, 5300);
  }

  form.addEventListener('submit', function(e){
    e.preventDefault();
    if (cycleTimer) clearTimeout(cycleTimer);
    runCycle();
  });

  // Auto-start the demo cycle so preview capture + gallery show the states.
  runCycle();
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Pulse Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Form Submitting Pulse
Source: https://codefronts.com/motion/css-pulse-animation/form-submitting-pulse/

A B2B signup form with a submit button that transforms into a processing-pulse state on click — text disappears, replaced by a soft scale-pulse loader indicating the API request is in flight. Muted purple (#6b46c1) accent on soft lavender background (#f5f3ff) — Superhuman / Vercel / Linear signup aesthetic. The pulse is DELIBERATELY calm (not spinning, not bouncing) because form submission is a trust context — the user just gave you their data and needs to feel it's being handled competently.
## HTML
```html
<div class="pl-12">
  <div class="pl-12__card">
    <p class="pl-12__eyebrow">Free · Personal plan</p>
    <h1 class="pl-12__title">Create your account</h1>
    <p class="pl-12__sub">14-day trial, no credit card required.</p>
    <form class="pl-12__form" id="pl-12-form" novalidate>
      <label class="pl-12__field">
        <span class="pl-12__field-label">Work email</span>
        <input type="email" name="email" value="alex@studio.com" autocomplete="email" placeholder="you@work.com" />
      </label>
      <label class="pl-12__field">
        <span class="pl-12__field-label">Company</span>
        <input type="text" name="company" value="Northlight Studio" placeholder="Your company name" />
      </label>
      <div class="pl-12__field">
        <span class="pl-12__field-label">Team size</span>
        <div class="pl-12__pills" role="radiogroup" aria-label="Team size">
          <button type="button" class="pl-12__pill" role="radio" aria-checked="false">1–5</button>
          <button type="button" class="pl-12__pill pl-12__pill--sel" role="radio" aria-checked="true">6–20</button>
          <button type="button" class="pl-12__pill" role="radio" aria-checked="false">21–100</button>
          <button type="button" class="pl-12__pill" role="radio" aria-checked="false">100+</button>
        </div>
      </div>
      <button type="submit" class="pl-12__submit" id="pl-12-submit" aria-live="polite">
        <span class="pl-12__submit-text">Create account</span>
        <span class="pl-12__submit-loader" aria-hidden="true"></span>
        <svg class="pl-12__submit-check" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <polyline points="4 12 10 18 20 6"/>
        </svg>
      </button>
      <p class="pl-12__foot">By continuing you agree to our <a href="#">Terms</a> and <a href="#">Privacy Policy</a>.</p>
    </form>
  </div>
</div>
```
## CSS
```css
.pl-12 {
  --page-bg: #f5f3ff;
  --card: #ffffff;
  --ink: #1e1b4b;
  --sub: #6b7280;
  --brand: #6b46c1;
  --brand-dark: #553c9a;
  --brand-soft: rgba(107,70,193,.1);
  --brand-glow: rgba(107,70,193,.35);
  --success: #10b981;
  --line: #e9d5ff;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  padding: 32px;
  background: radial-gradient(70% 90% at 20% 0%, rgba(167,139,250,.14) 0%, transparent 55%), radial-gradient(60% 80% at 100% 100%, rgba(107,70,193,.08) 0%, transparent 55%), var(--page-bg);
  color: var(--ink);
}

.pl-12 *,
.pl-12 *::before,
.pl-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pl-12 ::selection {
  background: var(--brand);
  color: #fff;
}

.pl-12__card {
  width: min(440px,100%);
  padding: 34px 32px 28px;
  border-radius: 16px;
  background: var(--card);
  box-shadow: 0 20px 50px -12px rgba(76,29,149,.2),0 4px 12px -6px rgba(76,29,149,.08);
  border: 1px solid var(--line);
}

.pl-12__eyebrow {
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--brand);
  margin-bottom: 8px;
}

.pl-12__title {
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -.02em;
  margin-bottom: 6px;
}

.pl-12__sub {
  font-size: .85rem;
  color: var(--sub);
  margin-bottom: 20px;
}

.pl-12__form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.pl-12__field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.pl-12__field-label {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--sub);
}

.pl-12__field input {
  padding: 11px 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fafaff;
  color: var(--ink);
  font-family: inherit;
  font-size: .9rem;
  transition: border-color .15s ease,background .15s ease,box-shadow .15s ease;
}

.pl-12__field input:focus {
  outline: none;
  border-color: var(--brand);
  background: var(--card);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.pl-12__pills {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.pl-12__pill {
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: #fafaff;
  color: var(--sub);
  font-family: inherit;
  font-size: .82rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color .15s ease,background .15s ease,color .15s ease;
}

.pl-12__pill:hover {
  border-color: var(--brand);
  color: var(--ink);
}

.pl-12__pill--sel {
  background: var(--brand);
  color: #fff;
  border-color: var(--brand);
}
/* Submit button with 3-state machine: resting / submitting / success */

.pl-12__submit {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  padding: 12px 22px;
  border: none;
  border-radius: 9px;
  background: var(--brand);
  color: #fff;
  font-family: inherit;
  font-size: .95rem;
  font-weight: 700;
  cursor: pointer;
  overflow: hidden;
  transition: background .18s ease,transform .12s ease;
  margin-top: 4px;
}

.pl-12__submit:hover {
  background: var(--brand-dark);
  transform: translateY(-1px);
}

.pl-12__submit:active {
  transform: translateY(1px);
}

.pl-12__submit:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}

.pl-12__submit-text {
  transition: opacity .2s ease;
}

.pl-12__submit-loader {
  position: absolute;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  transition: opacity .2s ease;
}

.pl-12__submit-check {
  position: absolute;
  opacity: 0;
  transition: opacity .2s ease;
}
/* Submitting state — text fades, loader appears + pulses */

.pl-12__submit.is-submitting {
  pointer-events: none;
}

.pl-12__submit.is-submitting .pl-12__submit-text {
  opacity: 0;
}

.pl-12__submit.is-submitting .pl-12__submit-loader {
  opacity: 1;
  animation: pl-12-loader 1.2s ease-in-out infinite;
}

@keyframes pl-12-loader {
  0%,
    100% {
    transform: scale(.85);
    box-shadow: 0 0 0 0 rgba(255,255,255,.5);
  }

  50% {
    transform: scale(1.05);
    box-shadow: 0 0 0 6px rgba(255,255,255,0);
  }
}
/* Success state — loader gone, checkmark visible, green button */

.pl-12__submit.is-success {
  background: var(--success);
  pointer-events: none;
}

.pl-12__submit.is-success .pl-12__submit-text,
.pl-12__submit.is-success .pl-12__submit-loader {
  opacity: 0;
}

.pl-12__submit.is-success .pl-12__submit-loader {
  animation: none;
}

.pl-12__submit.is-success .pl-12__submit-check {
  opacity: 1;
}

.pl-12__foot {
  font-size: .72rem;
  color: var(--sub);
  text-align: center;
  margin-top: 6px;
}

.pl-12__foot a {
  color: var(--brand);
  text-decoration: underline;
  font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
  .pl-12__submit-loader {
    animation: none!important;
    transform: none!important;
    box-shadow: none!important;
  }

  .pl-12__submit {
    transition: background .18s ease!important;
  }
}
```

## JavaScript
```js
(function(){
  var root = document.querySelector('.pl-12');
  if (!root) return;
  var form = root.querySelector('#pl-12-form');
  var btn  = root.querySelector('#pl-12-submit');
  var pills = root.querySelectorAll('.pl-12__pill');
  if (!form || !btn) return;

  pills.forEach(function(p){
    p.addEventListener('click', function(){
      pills.forEach(function(x){
        x.classList.remove('pl-12__pill--sel');
        x.setAttribute('aria-checked','false');
      });
      p.classList.add('pl-12__pill--sel');
      p.setAttribute('aria-checked','true');
    });
  });

  var cycleTimer = null;
  function runCycle(){
    // Reset to resting
    btn.classList.remove('is-submitting','is-success');
    // Enter submitting state
    setTimeout(function(){
      btn.classList.add('is-submitting');
    }, 800);
    // Enter success state
    setTimeout(function(){
      btn.classList.remove('is-submitting');
      btn.classList.add('is-success');
    }, 3300);
    // Reset for next loop (demo purposes — production removes this)
    cycleTimer = setTimeout(runCycle, 5300);
  }

  form.addEventListener('submit', function(e){
    e.preventDefault();
    if (cycleTimer) clearTimeout(cycleTimer);
    runCycle();
  });

  // Auto-start the demo cycle so preview capture + gallery show the states.
  runCycle();
})();
```

How this works

The submit button starts in its resting state showing text ("Create account"). On click, JS toggles an .is-submitting class on the button. The class change triggers three simultaneous effects via CSS: (1) the text fades out via opacity: 1 → 0 transition, (2) the button's ::after pseudo-element (which was hidden with opacity: 0) fades in — this contains a small circular loader with a soft box-shadow pulse ring, (3) the button gets pointer-events: none to prevent double-submits during processing.

The pulse animation on the loader is a soft transform: scale(.85) → scale(1.05) → scale(.85) cycle with box-shadow: 0 0 0 0 rgba(107, 70, 193, .4) → 0 0 0 6px transparent outward ring. Cycle is 1.2 seconds with ease-in-out — calm and reassuring, matching the trust context. NO rotation, NO bouncing — the button doesn't spin because spinning implies busy-ness/uncertainty; a soft pulse implies calm processing.

After 2.5 seconds (simulating API response) the JS swaps the class to .is-success — the loader fades out, a checkmark SVG fades in, and the button color transitions to green (#10b981). This creates a full state machine visible on one button: resting → submitting → success. In production, replace the setTimeout with the real fetch promise resolution.

Muted purple (#6b46c1) + soft lavender (#f5f3ff) — B2B SaaS onboarding aesthetic (Superhuman, Vercel, Linear). Deep purple (#4c1d95) for form ink, muted gray for placeholder text. Inputs use soft rounded rectangles with a subtle inner shadow. Form fields have proper labels above them (never placeholder-only — WCAG requirement). The form structure mimics a real signup: work email, company, team size selector.

Make it yours

  • Change accent color to your brand primary — the processing-pulse color, checkmark green, and button gradient all update via the CSS custom properties at the top.
  • For longer-running submissions (2s+), add a progress indicator inside the button ("Uploading… 42%") replacing the pulse. Users perceive multi-second waits as broken without a progress signal.
  • Change the resting-to-processing transition timing — 300ms feels immediate/responsive; 100ms feels twitchy; 600ms feels like the button is stuck. 300ms is the sweet spot.
  • For forms that fail (validation error, API rejection), add an .is-error state with red border + shake animation + inline error message. The state machine becomes: resting → submitting → error → resting (retry).
  • For platforms with progressive disclosure (multi-step forms), the submit button pulse only appears on the FINAL step submit — intermediate "Continue" clicks should navigate immediately without loader (they don't hit the network).

Gotchas — read before shipping

  • The button MUST become pointer-events: none or disabled during submission — otherwise double-clicks fire duplicate API calls and users create duplicate accounts / duplicate orders / duplicate charges.
  • Do NOT animate the text OUT with a scale effect ("CREATE ACCOUNT" shrinking to a dot) — text scaling triggers font-size re-render on every frame, causing blurry mid-transition text. Fade to opacity: 0 instead.
  • For screen readers, announce state transitions via aria-live="polite" region: "Creating account" on submit start, "Account created successfully" on success. Do NOT rely on visual pulse — screen-reader users hear nothing.
  • The 2.5s simulated response time is for demonstration. Real API responses are variable (200ms to 5s+). For > 3s waits, show a progress percentage or estimated time so users don't perceive the app as stuck.
  • The success state MUST be visually distinct from the loading state — swap the pulse for a static checkmark, change color to green, freeze at the terminal state. Don't leave the pulse animation running after success.
  • For accessibility, ensure the button's aria-label reflects its CURRENT state — "Create account" when resting, "Creating account" when submitting, "Account created" when success. This is done via JS updating aria-label.
  • For prefers-reduced-motion: keep the loading STATE (opacity change from text to loader), disable the pulse animation itself. Loader shows as a static purple dot with checkmark on success — no scale, no ring.

Browser support

ChromeSafariFirefoxEdge
45+10+40+45+

transform + opacity + box-shadow + @keyframes. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…