Дропдауны и меню в экосистеме SvelteKit реализуются через композицию реактивных состояний, DOM-позиционирования и управления фокусом. В отличие от традиционных UI-фреймворков, Svelte позволяет минимизировать абстракции и работать напрямую с поведением компонентов, что особенно важно для интерактивных элементов.
Ключевые составляющие любого дропдауна:
Минимальная реализация опирается на реактивность Svelte:
<script>
let open = false;
function toggle() {
open = !open;
}
function close() {
open = false;
}
</script>
<div class="dropdown">
<button on:click={toggle}>
Меню
</button>
{#if open}
<div class="menu">
<a href="/profile">Профиль</a>
<a href="/settings">Настройки</a>
<button on:click={close}>Выход</button>
</div>
{/if}
</div>
Особенности:
#if удаляет DOM при закрытии → экономия ресурсовБез закрытия по клику вне области дропдаун считается неполноценным. Реализация через кастомный action:
export function clickOutside(node) {
function handleClick(event) {
if (!node.contains(event.target)) {
node.dispatchEvent(new CustomEvent('outclick'));
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
Использование:
<script>
import { clickOutside } from './clickOutside';
let open = false;
</script>
<div use:clickOutside on:outclick={() => (open = false)}>
<button on:click={() => (open = !open)}>Меню</button>
{#if open}
<div class="menu">...</div>
{/if}
</div>
Дропдауны должны соответствовать стандартам доступности:
aria-expandedaria-haspopupПример:
<button
aria-haspopup="true"
aria-expanded={open}
on:click={toggle}
on:keydown={(e) => {
if (e.key === 'Escape') open = false;
}}
>
Меню
</button>
Навигация по пунктам:
<div role="menu">
<button role="menuitem">Профиль</button>
<button role="menuitem">Настройки</button>
</div>
Проблема: дропдаун должен корректно отображаться относительно триггера.
Подходы:
.dropdown {
position: relative;
}
.menu {
position: absolute;
top: 100%;
left: 0;
}
Недостатки:
Библиотеки UI (например Skeleton, Melt UI) используют Floating UI для динамического позиционирования:
import { computePosition } from '@floating-ui/dom';
Преимущества:
Svelte предоставляет встроенные transition-функции:
<script>
import { fade, scale } from 'svelte/transition';
</script>
{#if open}
<div transition:fade>
Меню
</div>
{/if}
Комбинированный вариант:
<div in:scale={{ duration: 150 }} out:fade>
Меню
</div>
Практика:
UI-библиотеки строят дропдауны как composable-компоненты:
<Dropdown>
<Dropdown.Trigger>
Кнопка
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Item>Пункт 1</Dropdown.Item>
</Dropdown.Content>
</Dropdown>
Внутри реализуется:
setContext, getContext)import { setContext, getContext } from 'svelte';
const KEY = {};
export function createDropdown() {
let open = false;
function toggle() {
open = !open;
}
setContext(KEY, { open, toggle });
}
export function useDropdown() {
return getContext(KEY);
}
Сложность:
Подход:
Плюсы:
Плюсы:
Минусы:
Пример hover:
<div
on:mouseenter={() => (open = true)}
on:mouseleave={() => (open = false)}
>
Популярные библиотеки (Melt UI, Radix-подобные решения) используют headless-подход:
Пример API:
<script>
import { createDropdown } from '@melt-ui/svelte';
const dropdown = createDropdown();
</script>
<button use:dropdown.trigger>
Меню
</button>
<div use:dropdown.content>
Элементы
</div>
Дропдауны часто рендерятся вне DOM-иерархии для избежания overflow:
<svelte:body>
{#if open}
<div class="portal-menu">...</div>
{/if}
</svelte:body>
Проблемы, которые решаются:
Для глобальных меню:
import { writable } from 'svelte/store';
export const dropdownStore = writable(null);
Использование:
{#if}Типичные возможности:
1. Typeahead (поиск по первым буквам)
let buffer = '';
function handleKey(e) {
buffer += e.key;
}
2. Controlled vs uncontrolled
3. Multi-select dropdown
Такой подход формирует основу большинства современных UI-библиотек для SvelteKit и позволяет создавать масштабируемые, доступные и гибкие интерфейсные компоненты.