Button Library · Interactive
Dropdown
Opens a short list. Esc or a click away closes.
States
Default
Hover
Active
Focus
Code
dropdown.html
<div class="btn-dropdown">
<button class="btn-dropdown__trigger" type="button" aria-expanded="false">
<span>Options</span>
<svg class="btn-dropdown__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m6 9 6 6 6-6"></path>
</svg>
</button>
<div class="btn-dropdown__menu" hidden>
<button class="btn-dropdown__item" type="button">Edit</button>
<button class="btn-dropdown__item" type="button">Duplicate</button>
</div>
</div>
dropdown.css
.btn-dropdown {
--btn-bg: #7c5cff;
--btn-fg: #ffffff;
--btn-font: inherit;
--btn-size: 15px;
--btn-weight: 600;
--btn-px: 22px;
--btn-py: 11px;
--btn-w: auto;
--btn-h: auto;
--btn-bw: 0px;
--btn-line: #8b6cff;
--btn-radius: 10px;
--btn-shadow: none;
--btn-speed: 180ms;
}
.btn-dropdown__trigger {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
box-sizing: border-box;
width: var(--btn-w);
height: var(--btn-h);
padding: var(--btn-py) var(--btn-px);
font-family: var(--btn-font);
font-size: var(--btn-size);
font-weight: var(--btn-weight);
line-height: 1.2;
color: var(--btn-fg);
background: var(--btn-bg);
border: var(--btn-bw) solid var(--btn-line);
border-radius: var(--btn-radius);
box-shadow: var(--btn-shadow);
cursor: pointer;
text-decoration: none;
white-space: nowrap;
transition: all var(--btn-speed) ease;
}
.btn-dropdown__trigger:focus-visible {
outline: 2px solid var(--btn-bg);
outline-offset: 2px;
}
.btn-dropdown__trigger:disabled {
opacity: 0.5;
pointer-events: none;
}
.btn-dropdown__icon {
width: 1.15em;
height: 1.15em;
flex-shrink: 0;
}
.btn-dropdown {
position: relative;
display: inline-flex;
gap: 1px;
}
.btn-dropdown__menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 10;
display: grid;
gap: 2px;
min-width: 100%;
box-sizing: border-box;
padding: 4px;
background: #1f1f28;
border: 1px solid #33333f;
border-radius: 10px;
box-shadow: 0 10px 24px rgba(0,0,0,.35);
}
.btn-dropdown__menu[hidden] {
display: none;
}
.btn-dropdown__item {
padding: 7px 10px;
font-family: inherit;
font-size: 14px;
color: #f4f4f6;
background: transparent;
border: 0;
border-radius: 6px;
text-align: left;
cursor: pointer;
}
.btn-dropdown__item:hover {
background: #33333f;
}
.btn-dropdown__item:focus-visible {
background: #33333f;
outline: none;
}
@media (prefers-reduced-motion: reduce) {
.btn-dropdown__trigger {
animation: none !important;
transition: none !important;
}
}
dropdown.js
document.querySelectorAll(".btn-dropdown").forEach((el) => {
const trigger = el.querySelector("[aria-expanded]");
const list = el.querySelector("[hidden]");
const set = (open) => {
list.hidden = !open;
trigger.setAttribute("aria-expanded", String(open));
};
const flip = () => set(list.hidden);
const picked = () => {
set(false);
trigger.focus();
};
const key = (e) => {
if (e.key === "Escape" && !list.hidden) picked();
};
const away = (e) => {
if (!e.composedPath().includes(el)) set(false);
};
trigger.addEventListener("click", flip);
list.addEventListener("click", picked);
el.addEventListener("keydown", key);
document.addEventListener("click", away);
return () => {
trigger.removeEventListener("click", flip);
list.removeEventListener("click", picked);
el.removeEventListener("keydown", key);
document.removeEventListener("click", away);
};
});
dropdown.tsx
import { useEffect, useRef } from "react";
import type React from "react";
import "./dropdown.css"; // the CSS tab
function attach(el: HTMLElement) {
const trigger = el.querySelector("[aria-expanded]") as HTMLElement;
const list = el.querySelector("[hidden]") as HTMLElement;
const set = (open: boolean) => {
list.hidden = !open;
trigger.setAttribute("aria-expanded", String(open));
};
const flip = () => set(list.hidden);
const picked = () => {
set(false);
trigger.focus();
};
const key = (e: KeyboardEvent) => {
if (e.key === "Escape" && !list.hidden) picked();
};
const away = (e: Event) => {
if (!e.composedPath().includes(el)) set(false);
};
trigger.addEventListener("click", flip);
list.addEventListener("click", picked);
el.addEventListener("keydown", key);
document.addEventListener("click", away);
return () => {
trigger.removeEventListener("click", flip);
list.removeEventListener("click", picked);
el.removeEventListener("keydown", key);
document.removeEventListener("click", away);
};
}
export function BtnDropdown({ children = "Options", ...props }: React.ComponentProps<"div">) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => attach(ref.current!), []);
return (
<div className="btn-dropdown" ref={ref} {...props}>
<button className="btn-dropdown__trigger" type="button" aria-expanded="false">
<span>{children}</span>
<svg className="btn-dropdown__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m6 9 6 6 6-6" />
</svg>
</button>
<div className="btn-dropdown__menu" hidden>
<button className="btn-dropdown__item" type="button">{"Edit"}</button>
<button className="btn-dropdown__item" type="button">{"Duplicate"}</button>
</div>
</div>
);
}
dropdown.html
<div class="btn-dropdown relative inline-flex gap-[1px]">
<button class="relative inline-flex items-center justify-center gap-[0.5em] [box-sizing:border-box] [padding:11px_22px] [font-family:inherit] text-[15px] font-[600] [line-height:1.2] text-[#ffffff] bg-[#7c5cff] [border:0px_solid_#8b6cff] rounded-[10px] [box-shadow:none] cursor-pointer no-underline whitespace-nowrap [transition:all_180ms_ease] focus-visible:[outline:2px_solid_#7c5cff] focus-visible:[outline-offset:2px] disabled:[opacity:0.5] disabled:[pointer-events:none] motion-reduce:[transition:none!important]" type="button" aria-expanded="false">
<span>Options</span>
<svg class="w-[1.15em] h-[1.15em] [flex-shrink:0]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m6 9 6 6 6-6"></path>
</svg>
</button>
<div class="absolute [top:calc(100%_+_6px)] [left:0] [z-index:10] grid gap-[2px] [min-width:100%] [box-sizing:border-box] [padding:4px] bg-[#1f1f28] [border:1px_solid_#33333f] rounded-[10px] [box-shadow:0_10px_24px_rgba(0,0,0,.35)] [&[hidden]]:hidden" hidden>
<button class="[padding:7px_10px] [font-family:inherit] text-[14px] text-[#f4f4f6] [background:transparent] [border:0] rounded-[6px] [text-align:left] cursor-pointer hover:bg-[#33333f] focus-visible:bg-[#33333f] focus-visible:[outline:none]" type="button">Edit</button>
<button class="[padding:7px_10px] [font-family:inherit] text-[14px] text-[#f4f4f6] [background:transparent] [border:0] rounded-[6px] [text-align:left] cursor-pointer hover:bg-[#33333f] focus-visible:bg-[#33333f] focus-visible:[outline:none]" type="button">Duplicate</button>
</div>
</div>
<!-- Needs the script from the JS tab. -->