A Pen by Josh Dillon on CodePen.
Created
October 9, 2025 21:24
-
-
Save jango-blockchained/75b5fc8a9315583f7b9c1366d7af0b9d to your computer and use it in GitHub Desktop.
Elastic neon radio buttons with GSAP and SVG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="container"> | |
| <div class="radio-btn-group"> | |
| <input type="radio" name="options" value="1" id="option-1" /> | |
| <label for="option-1"> | |
| <svg width="60" height="60"> | |
| <defs> | |
| <filter id="pink-shadow" x="-50%" y="-50%" width="200%" height="200%"> | |
| <feDropShadow dx="2" dy="2" stdDeviation="6" flood-color="#e707f7" flood-opacity="1" /> | |
| </filter> | |
| </defs> | |
| <circle class="unchecked" cx="30" cy="30" r="15" fill="none" stroke-width="4" transform="rotate(90 30 30)" stroke-linecap="round" /> | |
| <g class="checked"> | |
| <circle class="pink" cx="30" cy="30" r="16" fill="none" stroke-width="7" transform="rotate(90 30 30)" stroke-linecap="round" filter="url(#pink-shadow)" /> | |
| <circle class="blue" cx="30" cy="27" r="16" fill="none" stroke-width="7" transform="rotate(90 30 30)" stroke-linecap="round" /> | |
| </g> | |
| </svg> | |
| <span>Option one</span> | |
| </label> | |
| </div> | |
| <div class="radio-btn-group"> | |
| <input type="radio" name="options" value="2" id="option-2" /> | |
| <label for="option-2"> | |
| <svg width="60" height="60"> | |
| <circle class="unchecked" cx="30" cy="30" r="15" fill="none" stroke-width="4" transform="rotate(90 30 30)" stroke-linecap="round" /> | |
| <g class="checked"> | |
| <circle class="pink" cx="30" cy="30" r="16" fill="none" stroke-width="7" transform="rotate(90 30 30)" stroke-linecap="round" filter="url(#pink-shadow)" /> | |
| <circle class="blue" cx="30" cy="27" r="16" fill="none" stroke-width="7" transform="rotate(90 30 30)" stroke-linecap="round" /> | |
| </g> | |
| </svg> | |
| <span>Option two</span> | |
| </label> | |
| </div> | |
| <div class="radio-btn-group"> | |
| <input type="radio" name="options" value="2" id="option-3" /> | |
| <label for="option-3"> | |
| <svg width="60" height="60"> | |
| <circle class="unchecked" cx="30" cy="30" r="15" fill="none" stroke-width="4" transform="rotate(90 30 30)" stroke-linecap="round" /> | |
| <g class="checked"> | |
| <circle class="pink" cx="30" cy="30" r="16" fill="none" stroke-width="7" transform="rotate(90 30 30)" stroke-linecap="round" filter="url(#pink-shadow)" /> | |
| <circle class="blue" cx="30" cy="27" r="16" fill="none" stroke-width="7" transform="rotate(90 30 30)" stroke-linecap="round" /> | |
| </g> | |
| </svg> | |
| <span>Option three</span> | |
| </label> | |
| </div> | |
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class RadioButtonEffect { | |
| constructor(radioBtnGroups) { | |
| this.previousRadioBtn = null; | |
| this.isMobile = window.innerWidth <= 768; | |
| this.setCircleStroke(); | |
| radioBtnGroups.forEach((group) => { | |
| const radioBtn = gsap.utils.selector(group)("input[type='radio']")[0]; | |
| radioBtn.addEventListener("change", () => { | |
| const nodes = this.getNodes(radioBtn); | |
| if (this.previousRadioBtn && this.previousRadioBtn !== radioBtn) { | |
| this.changeEffect(this.getNodes(this.previousRadioBtn), false); | |
| } | |
| this.changeEffect(nodes, true); | |
| this.previousRadioBtn = radioBtn; | |
| }); | |
| }); | |
| } | |
| setCircleStroke() { | |
| document | |
| .querySelectorAll(".radio-btn-group g.checked circle") | |
| .forEach((circle) => { | |
| const length = circle.getTotalLength(); | |
| circle.style.strokeDasharray = `${length}px`; | |
| circle.style.strokeDashoffset = `${length}px`; | |
| }); | |
| } | |
| getNodes(radioBtn) { | |
| const container = radioBtn.closest(".radio-btn-group"); | |
| return [ | |
| gsap.utils.selector(container)("circle.blue")[0], | |
| gsap.utils.selector(container)("circle.pink")[0] | |
| ]; | |
| } | |
| changeEffect(nodes, isChecked) { | |
| const blueCircle = nodes[0]; | |
| const pinkCircle = nodes[1]; | |
| const length = Math.ceil(blueCircle.getTotalLength()); | |
| if (isChecked) { | |
| gsap.to([blueCircle, pinkCircle], { | |
| strokeDashoffset: 0, | |
| duration: this.isMobile ? 2 : 2.5, | |
| ease: this.isMobile ? "elastic.in(0.8, 0.2)" : "elastic.out(2.5, 0.2)" | |
| }); | |
| } else { | |
| gsap.killTweensOf([blueCircle, pinkCircle]); | |
| gsap.to([blueCircle, pinkCircle], { | |
| strokeDashoffset: `${length}px`, | |
| duration: 0.3, | |
| ease: "power2.out" | |
| }); | |
| } | |
| } | |
| } | |
| document.addEventListener("DOMContentLoaded", () => { | |
| const radioBtnGroups = document.querySelectorAll(".radio-btn-group"); | |
| new RadioButtonEffect(radioBtnGroups); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400&display=swap"); | |
| html, | |
| body { | |
| height: 100%; | |
| } | |
| body { | |
| align-items: center; | |
| background-color: #3d3e4a; | |
| color: #fff; | |
| display: flex; | |
| justify-content: center; | |
| } | |
| label { | |
| align-items: center; | |
| cursor: pointer; | |
| display: flex; | |
| font-family: "IBM Plex Mono", monospace; | |
| font-weight: 400; | |
| font-size: 24px; | |
| padding-right: 15px; | |
| position: relative; | |
| span { | |
| margin-left: 5px; | |
| transition: text-shadow 1000ms ease, color 1000ms ease; | |
| } | |
| } | |
| input[type="radio"] { | |
| opacity: 0; | |
| position: absolute; | |
| &:checked + label span { | |
| color: #fcdcf3; | |
| text-shadow: #e324f2 0px 0px 12px; | |
| } | |
| } | |
| input[type="radio"]:focus-visible + label { | |
| outline: 2px solid #faed76; | |
| border-radius: 6px; | |
| } | |
| circle { | |
| &.unchecked { | |
| stroke: #252630; | |
| } | |
| &.blue { | |
| mix-blend-mode: color-dodge; | |
| stroke: #05ddfa; | |
| } | |
| &.pink { | |
| stroke: #fc51c9; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment