SPB Git

spb/svgarden Public

SVGarden — searchable bank of 74 self-contained SVG+CSS animation snippets (svgarden.dev)

HTML 79.2% Astro 10.3% JavaScript 6% CSS 3.7% Shell 0.8%
4.4 KB · 129 lines html
Raw Blame History
1<!--2  ============================================================3  SVGarden — https://www.svgarden.dev4  Author : Simon-Pierre Boucher5  Contact: contact@spboucher.ai6  File   : snippets/buttons/btn-success-morph.html7  Desc   : Click morphs the button into a spinner, then a success check8  ============================================================9-->10<!--svgarden11title: Success-morph button12slug: btn-success-morph13category: buttons14tags: [button, state-machine, spinner, success, js]15difficulty: advanced16techniques: [data-state-attribute, width-morph, pathLength, animation-choreography]17how_it_works: >18  The button is a three-state machine driven entirely by a data-state attribute19  — idle, loading, done — and the script's only job is to advance that attribute20  (with one setTimeout standing in for a real request). Every visual is CSS21  keyed off [data-state]: the pill's max-width collapses until it equals its22  height and the border-radius makes it a circle, the spinner arc fades in and23  spins, then the checkmark draws itself with the pathLength trick while the24  background settles green. Keeping state in an attribute instead of classes25  means the CSS reads like a truth table and impossible combinations simply26  cannot exist.27customizable:28  - { var: "--sg-color",   label: "Base color",    type: color, default: "#7F77DD" }29  - { var: "--sg-success", label: "Success color", type: color, default: "#2BA05A" }30  - { var: "--sg-size",    label: "Font size",     type: range, min: 12, max: 24, default: 16, unit: px }31created: 2026-08-1032-->33<button class="sg-btn-success-morph" type="button" data-state="idle" style="--sg-color:#7F77DD; --sg-success:#2BA05A; --sg-size:16px;">34  <span class="sg-btn-success-morph-label">Save changes</span>35  <svg viewBox="0 0 28 28" aria-hidden="true" focusable="false">36    <circle class="sg-btn-success-morph-arc" cx="14" cy="14" r="10" pathLength="100" />37    <path class="sg-btn-success-morph-check" d="M8 14.5 L12.5 19 L20 9.5" pathLength="100" />38  </svg>39</button>40<script>41  document.querySelectorAll('.sg-btn-success-morph:not([data-sg-init])').forEach(function (btn) {42    btn.setAttribute('data-sg-init', '');43    btn.addEventListener('click', function () {44      var state = btn.getAttribute('data-state');45      if (state === 'idle') {46        btn.setAttribute('data-state', 'loading');47        setTimeout(function () { btn.setAttribute('data-state', 'done'); }, 1400);48      } else if (state === 'done') {49        btn.setAttribute('data-state', 'idle');50      }51    });52  });53</script>54<style>55  .sg-btn-success-morph {56    appearance: none;57    border: 0;58    font: inherit;59    font-size: var(--sg-size);60    font-weight: 600;61    color: #ffffff;62    background: var(--sg-color);63    height: 3em;64    max-width: 12em;65    padding: 0 1.8em;66    border-radius: 1.5em;67    cursor: pointer;68    position: relative;69    overflow: hidden;70    white-space: nowrap;71    transition: max-width 0.45s cubic-bezier(0.6, 0, 0.3, 1), padding 0.45s ease, background 0.4s ease;72  }73  .sg-btn-success-morph-label {74    transition: opacity 0.2s ease;75  }76  .sg-btn-success-morph svg {77    position: absolute;78    inset: 0;79    margin: auto;80    width: 1.9em;81    height: 1.9em;82  }83  .sg-btn-success-morph-arc,84  .sg-btn-success-morph-check {85    fill: none;86    stroke: #ffffff;87    stroke-width: 2.5;88    stroke-linecap: round;89    stroke-linejoin: round;90  }91  .sg-btn-success-morph-arc {92    stroke-dasharray: 28 72;93    opacity: 0;94    transform-box: fill-box;95    transform-origin: center;96  }97  .sg-btn-success-morph-check {98    stroke-dasharray: 100;99    stroke-dashoffset: 100;100  }101  .sg-btn-success-morph[data-state='loading'] {102    max-width: 3em;103    padding: 0;104    cursor: progress;105  }106  .sg-btn-success-morph[data-state='loading'] .sg-btn-success-morph-label,107  .sg-btn-success-morph[data-state='done'] .sg-btn-success-morph-label {108    opacity: 0;109  }110  .sg-btn-success-morph[data-state='loading'] .sg-btn-success-morph-arc {111    opacity: 1;112    animation: sg-btn-success-morph-spin 0.8s linear infinite;113  }114  .sg-btn-success-morph[data-state='done'] {115    max-width: 3em;116    padding: 0;117    background: var(--sg-success);118  }119  .sg-btn-success-morph[data-state='done'] .sg-btn-success-morph-check {120    animation: sg-btn-success-morph-draw 0.45s ease-out 0.1s forwards;121  }122  @keyframes sg-btn-success-morph-spin {123    to { transform: rotate(360deg); }124  }125  @keyframes sg-btn-success-morph-draw {126    to { stroke-dashoffset: 0; }127  }128</style>129