<div class="atm-form-select ">
<select class="form-select" id="select" name="select">
<option value="1" selected>Option One</option>
<option value="2">Option Two</option>
<option value="3">Option Three</option>
</select>
</div>
<div class="atm-form-select {{modifier}}">
<select
class="form-select"
{{#if id}}id="{{id}}"{{/if}}
{{#if name}}name="{{name}}"{{/if}}
{{#if disabled}}disabled{{/if}}>
{{#each options}}
<option value="{{value}}" {{#if selected}}selected{{/if}}>{{text}}</option>
{{/each}}
</select>
</div>
{
"modifier": "",
"id": "select",
"name": "select",
"disabled": false,
"options": [
{
"value": "1",
"text": "Option One",
"selected": true
},
{
"value": "2",
"text": "Option Two",
"selected": false
},
{
"value": "3",
"text": "Option Three",
"selected": false
}
]
}
(function () {
const selects = document.querySelectorAll('[data-custom-select]');
const closeAll = () =>
selects.forEach((c) => {
const trigger = c.querySelector('.custom-select-trigger');
const dropdown = c.querySelector('.custom-select-dropdown');
if (trigger) trigger.setAttribute('aria-expanded', 'false');
if (dropdown) dropdown.classList.remove('active');
});
selects.forEach((container) => {
const native = container.querySelector('.form-select-native');
const trigger = container.querySelector('.custom-select-trigger');
const dropdown = container.querySelector('.custom-select-dropdown');
const valueDisplay = container.querySelector('.custom-select-value');
if (!trigger || !dropdown || !native) return;
let isOpen = false;
const open = () => {
closeAll();
isOpen = true;
trigger.setAttribute('aria-expanded', 'true');
dropdown.classList.add('active');
// geef elke optie focus-mogelijkheid
dropdown
.querySelectorAll('.custom-select-option')
.forEach((opt) => {
opt.tabIndex = 0;
});
// focus op geselecteerde of eerste optie
const toFocus =
dropdown.querySelector('.selected') ||
dropdown.firstElementChild;
if (toFocus) {
requestAnimationFrame(() => toFocus.focus());
}
};
const close = () => {
if (!isOpen) return;
isOpen = false;
trigger.setAttribute('aria-expanded', 'false');
dropdown.classList.remove('active');
};
const toggle = () => (isOpen ? close() : open());
const select = (opt) => {
if (!opt) return;
const value = opt.dataset.value;
const text = opt.textContent.trim();
dropdown.querySelectorAll('.custom-select-option').forEach((o) => {
o.classList.remove('selected');
o.removeAttribute('aria-selected');
});
opt.classList.add('selected');
opt.setAttribute('aria-selected', 'true');
valueDisplay.textContent = text;
native.value = value;
native.dispatchEvent(new Event('change', { bubbles: true }));
close();
trigger.focus();
};
// klik op trigger
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
toggle();
});
// toetsen op trigger
trigger.addEventListener('keydown', (e) => {
if (['Enter', ' ', 'ArrowDown'].includes(e.key)) {
e.preventDefault();
if (!isOpen) open();
} else if (e.key === 'Escape') {
e.preventDefault();
close();
}
});
// klik op optie via delegation
dropdown.addEventListener('click', (e) => {
const opt = e.target.closest('.custom-select-option');
if (opt) {
e.stopPropagation();
select(opt);
}
});
// toetsen op optie via delegation
dropdown.addEventListener('keydown', (e) => {
const opt = e.target.closest('.custom-select-option');
if (!opt) return;
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
select(opt);
break;
case 'Escape':
e.preventDefault();
close();
trigger.focus();
break;
case 'ArrowDown':
e.preventDefault();
opt.nextElementSibling?.focus();
break;
case 'ArrowUp':
e.preventDefault();
opt.previousElementSibling?.focus();
break;
}
});
// klik buiten of scroll sluit dropdown
document.addEventListener('click', (e) => {
if (!container.contains(e.target)) close();
});
window.addEventListener(
'scroll',
() => {
if (isOpen) close();
},
{ passive: true }
);
});
})();
.atm-form-select {
.form-select {
@apply h-12;
@apply w-full;
@apply border-shade;
&:focus {
@apply border-cta;
}
&[disabled] {
@apply bg-shade;
}
}
}
.atm-form-select--choices {
@apply relative;
/* Legacy select styling (fallback) */
.form-select {
@apply text-h1 font-display text-secondary-col-1;
@apply border-none outline-none;
&:focus {
@apply border-none outline-none;
}
&[disabled] {
@apply bg-shade;
}
}
/* Custom select styling */
.custom-select-wrapper {
@apply relative w-max;
}
.custom-select-trigger {
@apply text-left;
@apply text-h5 md:text-h1 font-display text-secondary-col-1;
@apply border-none;
@apply cursor-pointer flex items-center;
@apply transition-opacity duration-200;
&:hover {
@apply opacity-80;
}
&[aria-expanded='true'] {
.custom-select-arrow i {
@apply rotate-180;
}
}
}
.custom-select-arrow {
@apply ml-4 flex items-center;
i {
@apply transition-transform duration-200 text-xs md:text-h4;
}
}
.custom-select-dropdown {
@apply absolute top-full right-0 md:left-0 w-full min-w-[50vw] md:min-w-[20rem] text-left mt-3;
@apply bg-white rounded-3xl shadow-[0_4px_6px_-2px_rgba(0,0,0,0.1)];
@apply opacity-0 invisible pointer-events-none;
@apply mt-3;
@apply z-50 max-h-64 overflow-y-auto;
transition: all 0.2s ease-in-out;
list-style: none;
padding: 0;
&.active {
@apply opacity-100 visible pointer-events-auto;
}
}
.custom-select-option {
@apply px-6 py-4 cursor-pointer;
@apply text-lg font-display text-black;
@apply transition-colors duration-150;
&:hover {
@apply bg-gray-100;
}
&.selected {
@apply text-secondary-col-1;
&:hover {
@apply text-secondary-col-1;
}
}
&:first-child {
@apply rounded-t-lg;
}
&:last-child {
@apply rounded-b-lg;
}
}
}
No notes defined.