02 / 20
All 20 in 20 Tailwind CSS Custom Selects
- 01Tailwind Country Selector Dropdown With Flags
- 02Tailwind Currency Picker Dropdown With Symbols
- 03Tailwind Credit Card Type Selector Dropdown
- 04Tailwind Pricing Plan Switcher Dropdown
- 05Tailwind User Avatar Profile Dropdown Menu
- 06Tailwind Minimalist Language Switcher Dropdown
- 07Tailwind Account Workspace Switcher Dropdown
- 08Tailwind Dark Mode Theme Selector Dropdown
- 09Tailwind Multi-Select Tag Dropdown With Chips
- 10Tailwind Status Filter Badge Dropdown Select
- 11Tailwind Date Range Time Period Picker Dropdown
- 12Tailwind Product Sorting Filter Dropdown
- 13Tailwind Dropdown With Subtext And Descriptions
- 14Tailwind Font Family Preview Picker Dropdown
- 15Tailwind File Type Media Uploader Dropdown
- 16Tailwind Color Swatch Picker Dropdown
- 17Tailwind Utility First Select Dropdown
- 18Tailwind Animated Slide Fade Dropdown Transition
- 19Tailwind Floating Label Select Dropdown Field
- 20Tailwind Glassmorphism Dark UI Dropdown Select
Tailwind Currency Picker Dropdown With Symbols
A fintech currency select whose gradient border is a before: utility, not a stylesheet rule: the conic ring, the mask pair and the hover fade all live in the trigger's class list. Options are a three-column grid so $, € and C$ stay aligned, and the panel unfolds with an animated clip-path inset.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcs-02 w-full min-h-svh grid place-items-center bg-tcs02-bg font-tcs02-sans text-tcs02-ink px-5 py-15" aria-label="Currency picker dropdown demo">
<div class="w-75 max-w-full flex flex-col gap-2.5">
<span id="tcs02-lbl" class="text-[13px] font-semibold tracking-[0.3px] text-tcs02-mut">Pay in currency</span>
<div data-select class="group relative">
<button id="tcs02-trigger" data-trigger type="button" aria-haspopup="listbox" aria-expanded="false" aria-controls="tcs02-list" aria-labelledby="tcs02-lbl tcs02-trigger" class="relative w-full flex items-center gap-3.25 min-h-13 px-4 rounded-2xl bg-tcs02-surface text-tcs02-ink cursor-pointer overflow-hidden focus-visible:outline-none before:content-[''] before:absolute before:inset-0 before:rounded-2xl before:p-[1.5px] before:bg-[conic-gradient(from_210deg,var(--color-tcs02-acc),var(--color-tcs02-acc2),var(--color-tcs02-acc),oklch(0.45_0.1_160),var(--color-tcs02-acc))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:opacity-80 before:transition-opacity before:duration-250 hover:before:opacity-100 focus-visible:before:opacity-100 focus-visible:before:shadow-[0_0_22px_-4px_var(--color-tcs02-acc)]">
<span data-badge class="grid place-items-center w-8.5 h-8.5 rounded-[10px] bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] text-tcs02-bg font-extrabold text-[17px]">$</span>
<span class="flex-1 flex flex-col text-left leading-[1.25]"><b data-code class="text-[15px] tracking-[0.5px]">USD</b><small data-name class="text-[11.5px] text-tcs02-mut">US Dollar</small></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="w-4.5 h-4.5 fill-none stroke-tcs02-mut [stroke-width:2.2] [stroke-linecap:round] [stroke-linejoin:round] transition-[rotate] duration-250 group-data-open:rotate-180 motion-reduce:transition-none"><path d="M6 9l6 6 6-6"/></svg>
</button>
<ul id="tcs02-list" role="listbox" aria-labelledby="tcs02-lbl" tabindex="-1" class="absolute top-[calc(100%+9px)] inset-x-0 z-20 list-none max-h-70 overflow-y-auto p-1.75 rounded-2xl border border-tcs02-line bg-tcs02-surface shadow-[0_26px_54px_-14px_oklch(0.1_0.03_165/0.7)] invisible opacity-0 [clip-path:inset(0_0_100%_0)] [transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_.28s] group-data-open:visible group-data-open:opacity-100 group-data-open:[clip-path:inset(0_0_0_0)] group-data-open:[transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_0s] motion-reduce:[clip-path:none] motion-reduce:transition-none">
<li id="tcs02-o0" role="option" aria-selected="true" data-v="USD" data-sym="$" data-name="US Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">USD</span><span class="text-[13px] text-tcs02-mut">US Dollar</span></li>
<li id="tcs02-o1" role="option" aria-selected="false" data-v="EUR" data-sym="€" data-name="Euro" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">€</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">EUR</span><span class="text-[13px] text-tcs02-mut">Euro</span></li>
<li id="tcs02-o2" role="option" aria-selected="false" data-v="GBP" data-sym="£" data-name="British Pound" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">£</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">GBP</span><span class="text-[13px] text-tcs02-mut">British Pound</span></li>
<li id="tcs02-o3" role="option" aria-selected="false" data-v="JPY" data-sym="¥" data-name="Japanese Yen" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">¥</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">JPY</span><span class="text-[13px] text-tcs02-mut">Japanese Yen</span></li>
<li id="tcs02-o4" role="option" aria-selected="false" data-v="CAD" data-sym="C$" data-name="Canadian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">C$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">CAD</span><span class="text-[13px] text-tcs02-mut">Canadian Dollar</span></li>
<li id="tcs02-o5" role="option" aria-selected="false" data-v="AUD" data-sym="A$" data-name="Australian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">A$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">AUD</span><span class="text-[13px] text-tcs02-mut">Australian Dollar</span></li>
</ul>
</div>
<p data-live aria-live="polite" class="min-h-4 font-tcs02-mono text-[11px] text-tcs02-mut"></p>
</div>
</section><section class="tcs-02 w-full min-h-svh grid place-items-center bg-tcs02-bg font-tcs02-sans text-tcs02-ink px-5 py-15" aria-label="Currency picker dropdown demo">
<div class="w-75 max-w-full flex flex-col gap-2.5">
<span id="tcs02-lbl" class="text-[13px] font-semibold tracking-[0.3px] text-tcs02-mut">Pay in currency</span>
<div data-select class="group relative">
<button id="tcs02-trigger" data-trigger type="button" aria-haspopup="listbox" aria-expanded="false" aria-controls="tcs02-list" aria-labelledby="tcs02-lbl tcs02-trigger" class="relative w-full flex items-center gap-3.25 min-h-13 px-4 rounded-2xl bg-tcs02-surface text-tcs02-ink cursor-pointer overflow-hidden focus-visible:outline-none before:content-[''] before:absolute before:inset-0 before:rounded-2xl before:p-[1.5px] before:bg-[conic-gradient(from_210deg,var(--color-tcs02-acc),var(--color-tcs02-acc2),var(--color-tcs02-acc),oklch(0.45_0.1_160),var(--color-tcs02-acc))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:opacity-80 before:transition-opacity before:duration-250 hover:before:opacity-100 focus-visible:before:opacity-100 focus-visible:before:shadow-[0_0_22px_-4px_var(--color-tcs02-acc)]">
<span data-badge class="grid place-items-center w-8.5 h-8.5 rounded-[10px] bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] text-tcs02-bg font-extrabold text-[17px]">$</span>
<span class="flex-1 flex flex-col text-left leading-[1.25]"><b data-code class="text-[15px] tracking-[0.5px]">USD</b><small data-name class="text-[11.5px] text-tcs02-mut">US Dollar</small></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="w-4.5 h-4.5 fill-none stroke-tcs02-mut [stroke-width:2.2] [stroke-linecap:round] [stroke-linejoin:round] transition-[rotate] duration-250 group-data-open:rotate-180 motion-reduce:transition-none"><path d="M6 9l6 6 6-6"/></svg>
</button>
<ul id="tcs02-list" role="listbox" aria-labelledby="tcs02-lbl" tabindex="-1" class="absolute top-[calc(100%+9px)] inset-x-0 z-20 list-none max-h-70 overflow-y-auto p-1.75 rounded-2xl border border-tcs02-line bg-tcs02-surface shadow-[0_26px_54px_-14px_oklch(0.1_0.03_165/0.7)] invisible opacity-0 [clip-path:inset(0_0_100%_0)] [transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_.28s] group-data-open:visible group-data-open:opacity-100 group-data-open:[clip-path:inset(0_0_0_0)] group-data-open:[transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_0s] motion-reduce:[clip-path:none] motion-reduce:transition-none">
<li id="tcs02-o0" role="option" aria-selected="true" data-v="USD" data-sym="$" data-name="US Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">USD</span><span class="text-[13px] text-tcs02-mut">US Dollar</span></li>
<li id="tcs02-o1" role="option" aria-selected="false" data-v="EUR" data-sym="€" data-name="Euro" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">€</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">EUR</span><span class="text-[13px] text-tcs02-mut">Euro</span></li>
<li id="tcs02-o2" role="option" aria-selected="false" data-v="GBP" data-sym="£" data-name="British Pound" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">£</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">GBP</span><span class="text-[13px] text-tcs02-mut">British Pound</span></li>
<li id="tcs02-o3" role="option" aria-selected="false" data-v="JPY" data-sym="¥" data-name="Japanese Yen" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">¥</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">JPY</span><span class="text-[13px] text-tcs02-mut">Japanese Yen</span></li>
<li id="tcs02-o4" role="option" aria-selected="false" data-v="CAD" data-sym="C$" data-name="Canadian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">C$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">CAD</span><span class="text-[13px] text-tcs02-mut">Canadian Dollar</span></li>
<li id="tcs02-o5" role="option" aria-selected="false" data-v="AUD" data-sym="A$" data-name="Australian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">A$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">AUD</span><span class="text-[13px] text-tcs02-mut">Australian Dollar</span></li>
</ul>
</div>
<p data-live aria-live="polite" class="min-h-4 font-tcs02-mono text-[11px] text-tcs02-mut"></p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tcs02-bg: #04120c;
--color-tcs02-bg: oklch(0.16 0.03 165);
--color-tcs02-surface: #0a1f16;
--color-tcs02-surface: oklch(0.23 0.04 165);
--color-tcs02-line: #163a2a;
--color-tcs02-line: oklch(0.32 0.05 163);
--color-tcs02-hover: #123227;
--color-tcs02-hover: oklch(0.29 0.05 164);
--color-tcs02-symbg: #0f2c20;
--color-tcs02-symbg: oklch(0.27 0.05 165);
--color-tcs02-ink: #eafff5;
--color-tcs02-ink: oklch(0.96 0.03 165);
--color-tcs02-mut: #7fae9b;
--color-tcs02-mut: oklch(0.7 0.05 163);
--color-tcs02-lime: #4ade80;
--color-tcs02-lime: oklch(0.82 0.19 148);
--color-tcs02-acc: #10b981;
--color-tcs02-acc: oklch(0.7 0.16 163);
--color-tcs02-acc2: #0891b2;
--color-tcs02-acc2: oklch(0.62 0.12 216);
--font-tcs02-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tcs02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-tcs02: cubic-bezier(0.16, 1, 0.3, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh, so the stage is sized before the browser build has compiled. */
.tcs-02 {
width: 100%;
min-height: 100svh;
}
/* The only shim utilities cannot express: Safari still needs the -webkit- pair for the
gradient-border mask on the trigger. Everything else is a before: utility. */
.tcs-02 [data-trigger]::before {
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
}@import "tailwindcss";
@theme {
--color-tcs02-bg: #04120c;
--color-tcs02-bg: oklch(0.16 0.03 165);
--color-tcs02-surface: #0a1f16;
--color-tcs02-surface: oklch(0.23 0.04 165);
--color-tcs02-line: #163a2a;
--color-tcs02-line: oklch(0.32 0.05 163);
--color-tcs02-hover: #123227;
--color-tcs02-hover: oklch(0.29 0.05 164);
--color-tcs02-symbg: #0f2c20;
--color-tcs02-symbg: oklch(0.27 0.05 165);
--color-tcs02-ink: #eafff5;
--color-tcs02-ink: oklch(0.96 0.03 165);
--color-tcs02-mut: #7fae9b;
--color-tcs02-mut: oklch(0.7 0.05 163);
--color-tcs02-lime: #4ade80;
--color-tcs02-lime: oklch(0.82 0.19 148);
--color-tcs02-acc: #10b981;
--color-tcs02-acc: oklch(0.7 0.16 163);
--color-tcs02-acc2: #0891b2;
--color-tcs02-acc2: oklch(0.62 0.12 216);
--font-tcs02-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tcs02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-tcs02: cubic-bezier(0.16, 1, 0.3, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh, so the stage is sized before the browser build has compiled. */
.tcs-02 {
width: 100%;
min-height: 100svh;
}
/* The only shim utilities cannot express: Safari still needs the -webkit- pair for the
gradient-border mask on the trigger. Everything else is a before: utility. */
.tcs-02 [data-trigger]::before {
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
}(function () {
document.querySelectorAll('.tcs-02 [data-select]').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var trig = root.querySelector('[data-trigger]');
var list = root.querySelector('[role="listbox"]');
var opts = Array.prototype.slice.call(root.querySelectorAll('[role="option"]'));
var live = root.closest('.tcs-02').querySelector('[data-live]');
var i = opts.findIndex(function (o) { return o.getAttribute('aria-selected') === 'true'; });
if (i < 0) i = 0;
var typed = '', timer;
function keepInView(el) {
var top = el.offsetTop, bottom = top + el.offsetHeight;
if (top < list.scrollTop) list.scrollTop = top;
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
}
function mark(n) {
i = n;
opts.forEach(function (o, k) { o.toggleAttribute('data-active', k === n); });
if (opts[n].id) trig.setAttribute('aria-activedescendant', opts[n].id);
keepInView(opts[n]);
}
function set(open) {
root.toggleAttribute('data-open', open);
trig.setAttribute('aria-expanded', String(open));
if (open) mark(i);
else {
opts.forEach(function (o) { o.removeAttribute('data-active'); });
trig.removeAttribute('aria-activedescendant');
}
}
function pick(n) {
opts.forEach(function (o) { o.setAttribute('aria-selected', 'false'); });
opts[n].setAttribute('aria-selected', 'true');
i = n;
root.dispatchEvent(new CustomEvent('tcs:pick', { bubbles: true, detail: { opt: opts[n], index: n } }));
if (live) live.textContent = (opts[n].dataset.v || opts[n].textContent).trim() + ' selected';
set(false);
trig.focus();
}
trig.addEventListener('click', function () { set(!root.hasAttribute('data-open')); });
opts.forEach(function (o, k) {
o.addEventListener('click', function () { pick(k); });
o.addEventListener('mousemove', function () { if (i !== k && root.hasAttribute('data-open')) mark(k); });
});
root.addEventListener('keydown', function (e) {
var open = root.hasAttribute('data-open');
if (!open) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); set(true); }
return;
}
if (e.key === 'ArrowDown') { e.preventDefault(); mark((i + 1) % opts.length); }
else if (e.key === 'ArrowUp') { e.preventDefault(); mark((i - 1 + opts.length) % opts.length); }
else if (e.key === 'Home') { e.preventDefault(); mark(0); }
else if (e.key === 'End') { e.preventDefault(); mark(opts.length - 1); }
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(i); }
else if (e.key === 'Escape' || e.key === 'Tab') { set(false); if (e.key === 'Escape') { e.preventDefault(); trig.focus(); } }
else if (e.key.length === 1 && e.key.trim()) {
clearTimeout(timer);
typed += e.key.toLowerCase();
timer = setTimeout(function () { typed = ''; }, 600);
var hit = opts.findIndex(function (o) { return (o.dataset.v || o.textContent).trim().toLowerCase().indexOf(typed) === 0; });
if (hit >= 0) mark(hit);
}
});
document.addEventListener('pointerdown', function (e) { if (!root.contains(e.target)) set(false); });
});
})();
(function () {
document.querySelectorAll('.tcs-02').forEach(function (root) {
root.addEventListener('tcs:pick', function (e) {
var d = e.detail.opt.dataset;
root.querySelector('[data-badge]').textContent = d.sym;
root.querySelector('[data-code]').textContent = d.v;
root.querySelector('[data-name]').textContent = d.name;
});
});
})();(function () {
document.querySelectorAll('.tcs-02 [data-select]').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var trig = root.querySelector('[data-trigger]');
var list = root.querySelector('[role="listbox"]');
var opts = Array.prototype.slice.call(root.querySelectorAll('[role="option"]'));
var live = root.closest('.tcs-02').querySelector('[data-live]');
var i = opts.findIndex(function (o) { return o.getAttribute('aria-selected') === 'true'; });
if (i < 0) i = 0;
var typed = '', timer;
function keepInView(el) {
var top = el.offsetTop, bottom = top + el.offsetHeight;
if (top < list.scrollTop) list.scrollTop = top;
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
}
function mark(n) {
i = n;
opts.forEach(function (o, k) { o.toggleAttribute('data-active', k === n); });
if (opts[n].id) trig.setAttribute('aria-activedescendant', opts[n].id);
keepInView(opts[n]);
}
function set(open) {
root.toggleAttribute('data-open', open);
trig.setAttribute('aria-expanded', String(open));
if (open) mark(i);
else {
opts.forEach(function (o) { o.removeAttribute('data-active'); });
trig.removeAttribute('aria-activedescendant');
}
}
function pick(n) {
opts.forEach(function (o) { o.setAttribute('aria-selected', 'false'); });
opts[n].setAttribute('aria-selected', 'true');
i = n;
root.dispatchEvent(new CustomEvent('tcs:pick', { bubbles: true, detail: { opt: opts[n], index: n } }));
if (live) live.textContent = (opts[n].dataset.v || opts[n].textContent).trim() + ' selected';
set(false);
trig.focus();
}
trig.addEventListener('click', function () { set(!root.hasAttribute('data-open')); });
opts.forEach(function (o, k) {
o.addEventListener('click', function () { pick(k); });
o.addEventListener('mousemove', function () { if (i !== k && root.hasAttribute('data-open')) mark(k); });
});
root.addEventListener('keydown', function (e) {
var open = root.hasAttribute('data-open');
if (!open) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); set(true); }
return;
}
if (e.key === 'ArrowDown') { e.preventDefault(); mark((i + 1) % opts.length); }
else if (e.key === 'ArrowUp') { e.preventDefault(); mark((i - 1 + opts.length) % opts.length); }
else if (e.key === 'Home') { e.preventDefault(); mark(0); }
else if (e.key === 'End') { e.preventDefault(); mark(opts.length - 1); }
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(i); }
else if (e.key === 'Escape' || e.key === 'Tab') { set(false); if (e.key === 'Escape') { e.preventDefault(); trig.focus(); } }
else if (e.key.length === 1 && e.key.trim()) {
clearTimeout(timer);
typed += e.key.toLowerCase();
timer = setTimeout(function () { typed = ''; }, 600);
var hit = opts.findIndex(function (o) { return (o.dataset.v || o.textContent).trim().toLowerCase().indexOf(typed) === 0; });
if (hit >= 0) mark(hit);
}
});
document.addEventListener('pointerdown', function (e) { if (!root.contains(e.target)) set(false); });
});
})();
(function () {
document.querySelectorAll('.tcs-02').forEach(function (root) {
root.addEventListener('tcs:pick', function (e) {
var d = e.detail.opt.dataset;
root.querySelector('[data-badge]').textContent = d.sym;
root.querySelector('[data-code]').textContent = d.v;
root.querySelector('[data-name]').textContent = d.name;
});
});
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tcs02-bg: #04120c;
--color-tcs02-bg: oklch(0.16 0.03 165);
--color-tcs02-surface: #0a1f16;
--color-tcs02-surface: oklch(0.23 0.04 165);
--color-tcs02-line: #163a2a;
--color-tcs02-line: oklch(0.32 0.05 163);
--color-tcs02-hover: #123227;
--color-tcs02-hover: oklch(0.29 0.05 164);
--color-tcs02-symbg: #0f2c20;
--color-tcs02-symbg: oklch(0.27 0.05 165);
--color-tcs02-ink: #eafff5;
--color-tcs02-ink: oklch(0.96 0.03 165);
--color-tcs02-mut: #7fae9b;
--color-tcs02-mut: oklch(0.7 0.05 163);
--color-tcs02-lime: #4ade80;
--color-tcs02-lime: oklch(0.82 0.19 148);
--color-tcs02-acc: #10b981;
--color-tcs02-acc: oklch(0.7 0.16 163);
--color-tcs02-acc2: #0891b2;
--color-tcs02-acc2: oklch(0.62 0.12 216);
--font-tcs02-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tcs02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-tcs02: cubic-bezier(0.16, 1, 0.3, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh, so the stage is sized before the browser build has compiled. */
.tcs-02 {
width: 100%;
min-height: 100svh;
}
/* The only shim utilities cannot express: Safari still needs the -webkit- pair for the
gradient-border mask on the trigger. Everything else is a before: utility. */
.tcs-02 [data-trigger]::before {
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
}@import "tailwindcss";
@theme {
--color-tcs02-bg: #04120c;
--color-tcs02-bg: oklch(0.16 0.03 165);
--color-tcs02-surface: #0a1f16;
--color-tcs02-surface: oklch(0.23 0.04 165);
--color-tcs02-line: #163a2a;
--color-tcs02-line: oklch(0.32 0.05 163);
--color-tcs02-hover: #123227;
--color-tcs02-hover: oklch(0.29 0.05 164);
--color-tcs02-symbg: #0f2c20;
--color-tcs02-symbg: oklch(0.27 0.05 165);
--color-tcs02-ink: #eafff5;
--color-tcs02-ink: oklch(0.96 0.03 165);
--color-tcs02-mut: #7fae9b;
--color-tcs02-mut: oklch(0.7 0.05 163);
--color-tcs02-lime: #4ade80;
--color-tcs02-lime: oklch(0.82 0.19 148);
--color-tcs02-acc: #10b981;
--color-tcs02-acc: oklch(0.7 0.16 163);
--color-tcs02-acc2: #0891b2;
--color-tcs02-acc2: oklch(0.62 0.12 216);
--font-tcs02-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tcs02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-tcs02: cubic-bezier(0.16, 1, 0.3, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh, so the stage is sized before the browser build has compiled. */
.tcs-02 {
width: 100%;
min-height: 100svh;
}
/* The only shim utilities cannot express: Safari still needs the -webkit- pair for the
gradient-border mask on the trigger. Everything else is a before: utility. */
.tcs-02 [data-trigger]::before {
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The
::beforegradient border becomes fivebefore:utilities; only the-webkit-mask pair stays in CSS, and the stylesheet says why. --cs-colsdisappears:grid-cols-[34px_52px_1fr]states the tracks on the row that uses them..is-sel .symbecomesgroup-aria-selected/opt:on the badge — a named group, because the row already participates in the widget-levelgroup.- The clip-path pair plus delayed
visibilityneeds the arbitrary[transition:...]shorthand;delay-*cannot give two properties different delays. - Typeahead is new in the port: type eu and the cursor lands on EUR, which the CSS original had no equivalent of.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Currency Picker Dropdown With Symbols
Source: https://codefronts.com/components/tailwind-css-custom-selects/tailwind-currency-picker-dropdown-with-symbols/
A fintech currency select whose gradient border is a before: utility, not a stylesheet rule: the conic ring, the mask pair and the hover fade all live in the trigger's class list. Options are a three-column grid so $, € and C$ stay aligned, and the panel unfolds with an animated clip-path inset.
## HTML
```html
<section class="tcs-02 w-full min-h-svh grid place-items-center bg-tcs02-bg font-tcs02-sans text-tcs02-ink px-5 py-15" aria-label="Currency picker dropdown demo">
<div class="w-75 max-w-full flex flex-col gap-2.5">
<span id="tcs02-lbl" class="text-[13px] font-semibold tracking-[0.3px] text-tcs02-mut">Pay in currency</span>
<div data-select class="group relative">
<button id="tcs02-trigger" data-trigger type="button" aria-haspopup="listbox" aria-expanded="false" aria-controls="tcs02-list" aria-labelledby="tcs02-lbl tcs02-trigger" class="relative w-full flex items-center gap-3.25 min-h-13 px-4 rounded-2xl bg-tcs02-surface text-tcs02-ink cursor-pointer overflow-hidden focus-visible:outline-none before:content-[''] before:absolute before:inset-0 before:rounded-2xl before:p-[1.5px] before:bg-[conic-gradient(from_210deg,var(--color-tcs02-acc),var(--color-tcs02-acc2),var(--color-tcs02-acc),oklch(0.45_0.1_160),var(--color-tcs02-acc))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:opacity-80 before:transition-opacity before:duration-250 hover:before:opacity-100 focus-visible:before:opacity-100 focus-visible:before:shadow-[0_0_22px_-4px_var(--color-tcs02-acc)]">
<span data-badge class="grid place-items-center w-8.5 h-8.5 rounded-[10px] bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] text-tcs02-bg font-extrabold text-[17px]">$</span>
<span class="flex-1 flex flex-col text-left leading-[1.25]"><b data-code class="text-[15px] tracking-[0.5px]">USD</b><small data-name class="text-[11.5px] text-tcs02-mut">US Dollar</small></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="w-4.5 h-4.5 fill-none stroke-tcs02-mut [stroke-width:2.2] [stroke-linecap:round] [stroke-linejoin:round] transition-[rotate] duration-250 group-data-open:rotate-180 motion-reduce:transition-none"><path d="M6 9l6 6 6-6"/></svg>
</button>
<ul id="tcs02-list" role="listbox" aria-labelledby="tcs02-lbl" tabindex="-1" class="absolute top-[calc(100%+9px)] inset-x-0 z-20 list-none max-h-70 overflow-y-auto p-1.75 rounded-2xl border border-tcs02-line bg-tcs02-surface shadow-[0_26px_54px_-14px_oklch(0.1_0.03_165/0.7)] invisible opacity-0 [clip-path:inset(0_0_100%_0)] [transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_.28s] group-data-open:visible group-data-open:opacity-100 group-data-open:[clip-path:inset(0_0_0_0)] group-data-open:[transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_0s] motion-reduce:[clip-path:none] motion-reduce:transition-none">
<li id="tcs02-o0" role="option" aria-selected="true" data-v="USD" data-sym="$" data-name="US Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">USD</span><span class="text-[13px] text-tcs02-mut">US Dollar</span></li>
<li id="tcs02-o1" role="option" aria-selected="false" data-v="EUR" data-sym="€" data-name="Euro" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">€</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">EUR</span><span class="text-[13px] text-tcs02-mut">Euro</span></li>
<li id="tcs02-o2" role="option" aria-selected="false" data-v="GBP" data-sym="£" data-name="British Pound" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">£</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">GBP</span><span class="text-[13px] text-tcs02-mut">British Pound</span></li>
<li id="tcs02-o3" role="option" aria-selected="false" data-v="JPY" data-sym="¥" data-name="Japanese Yen" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">¥</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">JPY</span><span class="text-[13px] text-tcs02-mut">Japanese Yen</span></li>
<li id="tcs02-o4" role="option" aria-selected="false" data-v="CAD" data-sym="C$" data-name="Canadian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">C$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">CAD</span><span class="text-[13px] text-tcs02-mut">Canadian Dollar</span></li>
<li id="tcs02-o5" role="option" aria-selected="false" data-v="AUD" data-sym="A$" data-name="Australian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">A$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">AUD</span><span class="text-[13px] text-tcs02-mut">Australian Dollar</span></li>
</ul>
</div>
<p data-live aria-live="polite" class="min-h-4 font-tcs02-mono text-[11px] text-tcs02-mut"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcs02-bg: #04120c;
--color-tcs02-bg: oklch(0.16 0.03 165);
--color-tcs02-surface: #0a1f16;
--color-tcs02-surface: oklch(0.23 0.04 165);
--color-tcs02-line: #163a2a;
--color-tcs02-line: oklch(0.32 0.05 163);
--color-tcs02-hover: #123227;
--color-tcs02-hover: oklch(0.29 0.05 164);
--color-tcs02-symbg: #0f2c20;
--color-tcs02-symbg: oklch(0.27 0.05 165);
--color-tcs02-ink: #eafff5;
--color-tcs02-ink: oklch(0.96 0.03 165);
--color-tcs02-mut: #7fae9b;
--color-tcs02-mut: oklch(0.7 0.05 163);
--color-tcs02-lime: #4ade80;
--color-tcs02-lime: oklch(0.82 0.19 148);
--color-tcs02-acc: #10b981;
--color-tcs02-acc: oklch(0.7 0.16 163);
--color-tcs02-acc2: #0891b2;
--color-tcs02-acc2: oklch(0.62 0.12 216);
--font-tcs02-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tcs02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-tcs02: cubic-bezier(0.16, 1, 0.3, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh, so the stage is sized before the browser build has compiled. */
.tcs-02 {
width: 100%;
min-height: 100svh;
}
/* The only shim utilities cannot express: Safari still needs the -webkit- pair for the
gradient-border mask on the trigger. Everything else is a before: utility. */
.tcs-02 [data-trigger]::before {
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.tcs-02 [data-select]').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var trig = root.querySelector('[data-trigger]');
var list = root.querySelector('[role="listbox"]');
var opts = Array.prototype.slice.call(root.querySelectorAll('[role="option"]'));
var live = root.closest('.tcs-02').querySelector('[data-live]');
var i = opts.findIndex(function (o) { return o.getAttribute('aria-selected') === 'true'; });
if (i < 0) i = 0;
var typed = '', timer;
function keepInView(el) {
var top = el.offsetTop, bottom = top + el.offsetHeight;
if (top < list.scrollTop) list.scrollTop = top;
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
}
function mark(n) {
i = n;
opts.forEach(function (o, k) { o.toggleAttribute('data-active', k === n); });
if (opts[n].id) trig.setAttribute('aria-activedescendant', opts[n].id);
keepInView(opts[n]);
}
function set(open) {
root.toggleAttribute('data-open', open);
trig.setAttribute('aria-expanded', String(open));
if (open) mark(i);
else {
opts.forEach(function (o) { o.removeAttribute('data-active'); });
trig.removeAttribute('aria-activedescendant');
}
}
function pick(n) {
opts.forEach(function (o) { o.setAttribute('aria-selected', 'false'); });
opts[n].setAttribute('aria-selected', 'true');
i = n;
root.dispatchEvent(new CustomEvent('tcs:pick', { bubbles: true, detail: { opt: opts[n], index: n } }));
if (live) live.textContent = (opts[n].dataset.v || opts[n].textContent).trim() + ' selected';
set(false);
trig.focus();
}
trig.addEventListener('click', function () { set(!root.hasAttribute('data-open')); });
opts.forEach(function (o, k) {
o.addEventListener('click', function () { pick(k); });
o.addEventListener('mousemove', function () { if (i !== k && root.hasAttribute('data-open')) mark(k); });
});
root.addEventListener('keydown', function (e) {
var open = root.hasAttribute('data-open');
if (!open) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); set(true); }
return;
}
if (e.key === 'ArrowDown') { e.preventDefault(); mark((i + 1) % opts.length); }
else if (e.key === 'ArrowUp') { e.preventDefault(); mark((i - 1 + opts.length) % opts.length); }
else if (e.key === 'Home') { e.preventDefault(); mark(0); }
else if (e.key === 'End') { e.preventDefault(); mark(opts.length - 1); }
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(i); }
else if (e.key === 'Escape' || e.key === 'Tab') { set(false); if (e.key === 'Escape') { e.preventDefault(); trig.focus(); } }
else if (e.key.length === 1 && e.key.trim()) {
clearTimeout(timer);
typed += e.key.toLowerCase();
timer = setTimeout(function () { typed = ''; }, 600);
var hit = opts.findIndex(function (o) { return (o.dataset.v || o.textContent).trim().toLowerCase().indexOf(typed) === 0; });
if (hit >= 0) mark(hit);
}
});
document.addEventListener('pointerdown', function (e) { if (!root.contains(e.target)) set(false); });
});
})();
(function () {
document.querySelectorAll('.tcs-02').forEach(function (root) {
root.addEventListener('tcs:pick', function (e) {
var d = e.detail.opt.dataset;
root.querySelector('[data-badge]').textContent = d.sym;
root.querySelector('[data-code]').textContent = d.v;
root.querySelector('[data-name]').textContent = d.name;
});
});
})();
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Currency Picker Dropdown With Symbols
Source: https://codefronts.com/components/tailwind-css-custom-selects/tailwind-currency-picker-dropdown-with-symbols/
A fintech currency select whose gradient border is a before: utility, not a stylesheet rule: the conic ring, the mask pair and the hover fade all live in the trigger's class list. Options are a three-column grid so $, € and C$ stay aligned, and the panel unfolds with an animated clip-path inset.
## HTML
```html
<section class="tcs-02 w-full min-h-svh grid place-items-center bg-tcs02-bg font-tcs02-sans text-tcs02-ink px-5 py-15" aria-label="Currency picker dropdown demo">
<div class="w-75 max-w-full flex flex-col gap-2.5">
<span id="tcs02-lbl" class="text-[13px] font-semibold tracking-[0.3px] text-tcs02-mut">Pay in currency</span>
<div data-select class="group relative">
<button id="tcs02-trigger" data-trigger type="button" aria-haspopup="listbox" aria-expanded="false" aria-controls="tcs02-list" aria-labelledby="tcs02-lbl tcs02-trigger" class="relative w-full flex items-center gap-3.25 min-h-13 px-4 rounded-2xl bg-tcs02-surface text-tcs02-ink cursor-pointer overflow-hidden focus-visible:outline-none before:content-[''] before:absolute before:inset-0 before:rounded-2xl before:p-[1.5px] before:bg-[conic-gradient(from_210deg,var(--color-tcs02-acc),var(--color-tcs02-acc2),var(--color-tcs02-acc),oklch(0.45_0.1_160),var(--color-tcs02-acc))] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask-composite:exclude] before:opacity-80 before:transition-opacity before:duration-250 hover:before:opacity-100 focus-visible:before:opacity-100 focus-visible:before:shadow-[0_0_22px_-4px_var(--color-tcs02-acc)]">
<span data-badge class="grid place-items-center w-8.5 h-8.5 rounded-[10px] bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] text-tcs02-bg font-extrabold text-[17px]">$</span>
<span class="flex-1 flex flex-col text-left leading-[1.25]"><b data-code class="text-[15px] tracking-[0.5px]">USD</b><small data-name class="text-[11.5px] text-tcs02-mut">US Dollar</small></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="w-4.5 h-4.5 fill-none stroke-tcs02-mut [stroke-width:2.2] [stroke-linecap:round] [stroke-linejoin:round] transition-[rotate] duration-250 group-data-open:rotate-180 motion-reduce:transition-none"><path d="M6 9l6 6 6-6"/></svg>
</button>
<ul id="tcs02-list" role="listbox" aria-labelledby="tcs02-lbl" tabindex="-1" class="absolute top-[calc(100%+9px)] inset-x-0 z-20 list-none max-h-70 overflow-y-auto p-1.75 rounded-2xl border border-tcs02-line bg-tcs02-surface shadow-[0_26px_54px_-14px_oklch(0.1_0.03_165/0.7)] invisible opacity-0 [clip-path:inset(0_0_100%_0)] [transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_.28s] group-data-open:visible group-data-open:opacity-100 group-data-open:[clip-path:inset(0_0_0_0)] group-data-open:[transition:clip-path_.28s_var(--ease-tcs02),opacity_.2s,visibility_0s_0s] motion-reduce:[clip-path:none] motion-reduce:transition-none">
<li id="tcs02-o0" role="option" aria-selected="true" data-v="USD" data-sym="$" data-name="US Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">USD</span><span class="text-[13px] text-tcs02-mut">US Dollar</span></li>
<li id="tcs02-o1" role="option" aria-selected="false" data-v="EUR" data-sym="€" data-name="Euro" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">€</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">EUR</span><span class="text-[13px] text-tcs02-mut">Euro</span></li>
<li id="tcs02-o2" role="option" aria-selected="false" data-v="GBP" data-sym="£" data-name="British Pound" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">£</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">GBP</span><span class="text-[13px] text-tcs02-mut">British Pound</span></li>
<li id="tcs02-o3" role="option" aria-selected="false" data-v="JPY" data-sym="¥" data-name="Japanese Yen" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">¥</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">JPY</span><span class="text-[13px] text-tcs02-mut">Japanese Yen</span></li>
<li id="tcs02-o4" role="option" aria-selected="false" data-v="CAD" data-sym="C$" data-name="Canadian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">C$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">CAD</span><span class="text-[13px] text-tcs02-mut">Canadian Dollar</span></li>
<li id="tcs02-o5" role="option" aria-selected="false" data-v="AUD" data-sym="A$" data-name="Australian Dollar" class="group/opt grid grid-cols-[34px_52px_1fr] items-center gap-3 min-h-11 px-3 rounded-[11px] cursor-pointer transition-colors duration-150 data-active:bg-tcs02-hover"><span class="grid place-items-center w-8.5 h-8.5 rounded-[9px] bg-tcs02-symbg text-tcs02-lime font-extrabold text-base transition-colors duration-150 group-aria-selected/opt:bg-linear-[140deg,var(--color-tcs02-acc),var(--color-tcs02-acc2)] group-aria-selected/opt:text-tcs02-bg">A$</span><span class="font-tcs02-mono text-[13.5px] font-bold tracking-[0.5px] text-tcs02-ink">AUD</span><span class="text-[13px] text-tcs02-mut">Australian Dollar</span></li>
</ul>
</div>
<p data-live aria-live="polite" class="min-h-4 font-tcs02-mono text-[11px] text-tcs02-mut"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcs02-bg: #04120c;
--color-tcs02-bg: oklch(0.16 0.03 165);
--color-tcs02-surface: #0a1f16;
--color-tcs02-surface: oklch(0.23 0.04 165);
--color-tcs02-line: #163a2a;
--color-tcs02-line: oklch(0.32 0.05 163);
--color-tcs02-hover: #123227;
--color-tcs02-hover: oklch(0.29 0.05 164);
--color-tcs02-symbg: #0f2c20;
--color-tcs02-symbg: oklch(0.27 0.05 165);
--color-tcs02-ink: #eafff5;
--color-tcs02-ink: oklch(0.96 0.03 165);
--color-tcs02-mut: #7fae9b;
--color-tcs02-mut: oklch(0.7 0.05 163);
--color-tcs02-lime: #4ade80;
--color-tcs02-lime: oklch(0.82 0.19 148);
--color-tcs02-acc: #10b981;
--color-tcs02-acc: oklch(0.7 0.16 163);
--color-tcs02-acc2: #0891b2;
--color-tcs02-acc2: oklch(0.62 0.12 216);
--font-tcs02-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tcs02-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-tcs02: cubic-bezier(0.16, 1, 0.3, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh, so the stage is sized before the browser build has compiled. */
.tcs-02 {
width: 100%;
min-height: 100svh;
}
/* The only shim utilities cannot express: Safari still needs the -webkit- pair for the
gradient-border mask on the trigger. Everything else is a before: utility. */
.tcs-02 [data-trigger]::before {
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.tcs-02 [data-select]').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var trig = root.querySelector('[data-trigger]');
var list = root.querySelector('[role="listbox"]');
var opts = Array.prototype.slice.call(root.querySelectorAll('[role="option"]'));
var live = root.closest('.tcs-02').querySelector('[data-live]');
var i = opts.findIndex(function (o) { return o.getAttribute('aria-selected') === 'true'; });
if (i < 0) i = 0;
var typed = '', timer;
function keepInView(el) {
var top = el.offsetTop, bottom = top + el.offsetHeight;
if (top < list.scrollTop) list.scrollTop = top;
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
}
function mark(n) {
i = n;
opts.forEach(function (o, k) { o.toggleAttribute('data-active', k === n); });
if (opts[n].id) trig.setAttribute('aria-activedescendant', opts[n].id);
keepInView(opts[n]);
}
function set(open) {
root.toggleAttribute('data-open', open);
trig.setAttribute('aria-expanded', String(open));
if (open) mark(i);
else {
opts.forEach(function (o) { o.removeAttribute('data-active'); });
trig.removeAttribute('aria-activedescendant');
}
}
function pick(n) {
opts.forEach(function (o) { o.setAttribute('aria-selected', 'false'); });
opts[n].setAttribute('aria-selected', 'true');
i = n;
root.dispatchEvent(new CustomEvent('tcs:pick', { bubbles: true, detail: { opt: opts[n], index: n } }));
if (live) live.textContent = (opts[n].dataset.v || opts[n].textContent).trim() + ' selected';
set(false);
trig.focus();
}
trig.addEventListener('click', function () { set(!root.hasAttribute('data-open')); });
opts.forEach(function (o, k) {
o.addEventListener('click', function () { pick(k); });
o.addEventListener('mousemove', function () { if (i !== k && root.hasAttribute('data-open')) mark(k); });
});
root.addEventListener('keydown', function (e) {
var open = root.hasAttribute('data-open');
if (!open) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); set(true); }
return;
}
if (e.key === 'ArrowDown') { e.preventDefault(); mark((i + 1) % opts.length); }
else if (e.key === 'ArrowUp') { e.preventDefault(); mark((i - 1 + opts.length) % opts.length); }
else if (e.key === 'Home') { e.preventDefault(); mark(0); }
else if (e.key === 'End') { e.preventDefault(); mark(opts.length - 1); }
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(i); }
else if (e.key === 'Escape' || e.key === 'Tab') { set(false); if (e.key === 'Escape') { e.preventDefault(); trig.focus(); } }
else if (e.key.length === 1 && e.key.trim()) {
clearTimeout(timer);
typed += e.key.toLowerCase();
timer = setTimeout(function () { typed = ''; }, 600);
var hit = opts.findIndex(function (o) { return (o.dataset.v || o.textContent).trim().toLowerCase().indexOf(typed) === 0; });
if (hit >= 0) mark(hit);
}
});
document.addEventListener('pointerdown', function (e) { if (!root.contains(e.target)) set(false); });
});
})();
(function () {
document.querySelectorAll('.tcs-02').forEach(function (root) {
root.addEventListener('tcs:pick', function (e) {
var d = e.detail.opt.dataset;
root.querySelector('[data-badge]').textContent = d.sym;
root.querySelector('[data-code]').textContent = d.v;
root.querySelector('[data-name]').textContent = d.name;
});
});
})();
```How this works
The gradient border moves onto the element. The CSS version needs a ::before rule with six declarations. In v4 the pseudo-element is a variant, so the whole ring — conic gradient, padding-box mask, mask-composite: exclude, opacity and its hover twin — is written on the button. The one thing that cannot be: Safari's -webkit-mask-composite: xor pair, which has no utility and stays a two-line shim in the stylesheet.
The unfold is a clip-path. inset(0 0 100% 0) to inset(0 0 0 0) animates on the compositor and needs no height measurement, unlike a max-h transition which always over- or under-shoots. Both ends sit on the panel, with visibility riding the arbitrary shorthand so a closed panel is unfocusable.
Alignment is a grid, stated locally. Symbols have wildly different widths, so the row is grid-cols-[34px_52px_1fr]: badge, ISO code, name. The code column uses font-tcs02-mono, a real font utility minted by the @theme entry, so a host page's font stack cannot break the columns.
Make it yours
- Badge size: the
w-8.5 h-8.5pair on the badge and the first grid column — keep them equal. - Ring recipe: edit the
before:bg-[conic-gradient(...)]stops; they read--color-tcs02-accand--color-tcs02-acc2, so a token change retints the whole demo. - Longer ISO labels: widen the middle track in
grid-cols-[34px_52px_1fr]. - Swap the unfold for a fade by deleting the two
[clip-path:...]utilities; the ARIA layer is animation-agnostic.
Gotchas — read before shipping
- The
-webkit-mask-composite: xorshim is required for Safari — the standardmask-composite: excludealone leaves a filled rectangle there. clip-pathclips the focus ring too. Keep the ring on the trigger, which sits outside the panel.- A symbol alone is ambiguous (
$is four currencies); always pair it with the ISO code, as the option rows do. - Arbitrary values take no spaces:
inset(0_0_100%_0), notinset(0 0 100% 0).
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Animatable clip-path and masked gradient borders are Baseline; the floor quoted is Tailwind v4's own generated sheet.