
Create animated, accessible, modern dropdown menus using plain JavaScript and CSS/CSS3.
How to use it:
1. Code the HTML for the dropdown menu.
<!-- Default -->
<div class="select" tabindex="0" role="button">
<button tabindex="0">My Projects</button>
<div>
<a role="button" tabindex="0" href="#"><span>jQueryScript</span></a>
<a role="button" tabindex="0" href="#"><span>CSSScript</span></a>
<a role="button" tabindex="0" href="#"><span>VueScript</span></a>
</div>
</div>
<!-- Accessible -->
<div class="select" tabindex="0" role="button" style="--a11y: 1;--outline-color: hsla(calc(var(--accent-color-hue)*1deg) 100% 58% / calc(var(--a11y)*100%));">
<button tabindex="0">My Projects</button>
<div>
<a role="button" tabindex="0" href="#"><span>jQueryScript</span></a>
<a role="button" tabindex="0" href="#"><span>CSSScript</span></a>
<a role="button" tabindex="0" href="#"><span>VueScript</span></a>
</div>
</div>2. The required CSS rules.
@layer properties {
@property --max-height {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --bg-x {
syntax: "<number>";
inherits: true;
initial-value: 50;
}
@property --bg-y {
syntax: "<number>";
inherits: true;
initial-value: -200;
}
@property --scale {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
@property --accent-color-hue {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --accent-color-hue {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --item-y {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --item-opacity {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --accent-color {
syntax: "<color>";
inherits: true;
initial-value: hsl(calc(var(--accent-color-hue)*1deg), 100%, 58%);
}
@property --radial-bg-color {
syntax: "<color>";
inherits: true;
initial-value: black;
}
}
:root {
--background-color: hsl(222deg 17% 14%);
--dark-color: hsl(227deg 37% 3%);
--light-color: hsl(211deg 23% 51%);
--accent-color-hue: 219;
--accent-color: hsl(calc(var(--accent-color-hue)*1deg) 100% 58%);
--radial-bg-color: var(--dark-color);
--max-height: 0;
--bg-y: -50;
--bg-x: 200;
--item-y: 20;
--item-opacity: 0;
/* Misc */
--_inner-radius: 10;
--_inner-padding: 6;
--inner-radius: calc(var(--_inner-radius) * 1px);
--inner-padding: calc(var(--_inner-padding) * 1px);
--outer-radius: calc(calc(var(--_inner-radius) + var(--_inner-padding)) * 1px);
--debug: 0;
--a11y: 0;
--outline-color: hsla(calc(var(--accent-color-hue)*1deg) 100% 58% / calc(var(--a11y)*100%));
}
div.select {
color: white;
background: var(--dark-color) radial-gradient(ellipse 70% 70% at calc(var(--bg-x)*1%) calc(var(--bg-y)*1%), var(--radial-bg-color) 0%, var(--dark-color) 100%);
padding: var(--inner-padding);
border-radius: var(--outer-radius);
position: relative;
width: 200px;
z-index: 1;
transition: background 0.3s ease, --bg-y 0.4 ease, --bg-x 0.4s ease;
/*
* :focus-within or :has(button:focus) */
}
div.select:hover {
animation: glow 1.2s ease-in-out;
}
div.select:hover > button:after {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='58' height='98' fill='none'%3E%3Cpath fill='hsl(219deg 100% 58%)' d='M25.536 6c1.54-2.667 5.388-2.667 6.928 0l18.187 31.5c1.54 2.667-.385 6-3.465 6H10.814c-3.079 0-5.003-3.333-3.464-6L25.536 6ZM25.536 92c1.54 2.667 5.388 2.667 6.928 0l18.187-31.5c1.54-2.667-.385-6-3.465-6H10.814c-3.079 0-5.003 3.333-3.464 6L25.536 92Z'/%3E%3C/svg%3E") no-repeat center center/0.6em;
}
div.select:before {
content: "";
display: block;
position: absolute;
width: calc(100% - 2px);
height: calc(100% - 2px);
top: 1px;
left: 1px;
background: var(--dark-color);
border-radius: var(--outer-radius);
z-index: -1;
}
div.select > button {
padding: calc(var(--inner-padding)*2) calc(var(--inner-padding)*2);
background: var(--background-color);
border-radius: var(--inner-radius);
border: 0;
color: white;
text-align: left;
font-size: 1em;
width: 100%;
cursor: pointer;
position: relative;
box-shadow: inset 0 2px 1px -1px rgba(255, 255, 255, 0.1);
transform: scale(var(--scale));
animation-duration: 0.2s;
animation-timing-function: cubic-bezier(0.66, -0.82, 0.33, 1.73);
}
div.select > button:focus {
outline: 1px solid var(--accent-color);
outline-offset: -1px;
}
div.select > button:after {
content: "";
position: absolute;
right: 8px;
height: 100%;
width: 1em;
top: 0;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='58' height='98' fill='none'%3E%3Cpath fill='hsl(211deg 23% 51%)' d='M25.536 6c1.54-2.667 5.388-2.667 6.928 0l18.187 31.5c1.54 2.667-.385 6-3.465 6H10.814c-3.079 0-5.003-3.333-3.464-6L25.536 6ZM25.536 92c1.54 2.667 5.388 2.667 6.928 0l18.187-31.5c1.54-2.667-.385-6-3.465-6H10.814c-3.079 0-5.003 3.333-3.464 6L25.536 92Z'/%3E%3C/svg%3E") no-repeat center center/0.6em;
}
div.select > div {
display: flex;
flex-direction: column;
overflow: hidden;
height: 0;
/* transition: --max-height .2s ease; ?? not working ?? */
transition: height 0.3s ease-in-out;
}
div.select > div > a {
display: block;
padding: calc(var(--inner-padding)*1.6) calc(var(--inner-padding)*1.2);
color: var(--light-color);
cursor: pointer;
margin-top: 8px;
text-decoration: none;
border-radius: var(--inner-radius);
position: relative;
}
div.select > div > a > span {
position: relative;
display: block;
transform: translateY(calc(var(--item-y)*1px));
opacity: var(--item-opacity);
transition: --item-y 0.2s ease 0.1s, --item-opacity 0.2s 0.1s;
}
div.select > div > a:nth-child(1) span {
transition-delay: 0.1s;
}
div.select > div > a:nth-child(2) span {
transition-delay: 0.15s;
}
div.select > div > a:nth-child(3) span {
transition-delay: 0.2s;
}
div.select > div > a:focus {
outline: 1px solid var(--outline-color);
outline-offset: -1px;
}
div.select > div > a:hover, div.select > div > a:focus {
color: var(--accent-color);
}
div.select:focus-within {
outline: 1px dashed var(--outline-color);
}
div.select:hover > div, div.select:has(button:focus) > div, div.select:focus-within > div {
height: calc(var(--max-height)*1px);
--item-y: 0;
--item-opacity: 1;
}
.select.nomotion {
transition: none !important;
animation: none !important;
}
.select.nomotion:before, .select.nomotion:after,
.select.nomotion *, .select.nomotion *:before, .select.nomotion *:after {
transition: none !important;
animation: none !important;
}
@media (prefers-reduced-motion: reduce) {
.select {
transition: none !important;
animation: none !important;
}
.select:before, .select:after,
.select *, .select *:before, .select *:after {
transition: none !important;
animation: none !important;
}
}
@keyframes glow {
from {
--radial-bg-color: var(--accent-color);
--bg-x: 100;
--bg-y: 0;
}
50% {
--radial-bg-color: hsl(290deg 100% 58%);
--bg-x: 60;
--bg-y: 120;
}
to {
--radial-bg-color: var(--dark-color);
--bg-x: 60;
--bg-y: 120;
}
}
@keyframes popOut {
from {
--scale: 1;
}
50% {
--scale: 1.02;
}
to {
--scale: 1;
}
}3. Enable the dropdown menus.
const selects = document.querySelectorAll('.select');
window.addEventListener('DOMContentLoaded', () => {
selects.forEach(select => {
const button = select.querySelector('button');
const full_height = [];
[...select.querySelectorAll('div > a')].map(link => {
const styles = window.getComputedStyle(link);
const box = link.getBoundingClientRect();
const margin = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom) || 0;
const height = box.height + margin;
full_height.push(height);
link.addEventListener('click', () => {
const link_text = link.textContent;
const button_text = button.textContent;
button.textContent = link_text;
button.style.animationName="popOut";
button.addEventListener("animationend", () => {
button.style.animationName="none"
});
const span = document.createElement('span');
span.textContent = button_text;
link.innerHTML = "";
link.appendChild(span)
link.blur()
})
});
const totalHeight = full_height.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
select.dataset.totalHeight = totalHeight;
select.style.setProperty('--max-height', totalHeight);
});
});






