Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jango-blockchained/75b5fc8a9315583f7b9c1366d7af0b9d to your computer and use it in GitHub Desktop.

Select an option

Save jango-blockchained/75b5fc8a9315583f7b9c1366d7af0b9d to your computer and use it in GitHub Desktop.
Elastic neon radio buttons with GSAP and SVG
<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>
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);
});
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
@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