Headless UI-компоненты представляют собой архитектурный подход, при котором логика взаимодействия и состояние компонента полностью отделены от его визуального представления. В отличие от традиционных UI-библиотек, где компоненты поставляются вместе с готовыми стилями и разметкой, headless-компоненты предоставляют только поведение — без навязанного внешнего вида.
Ключевая идея: поведение (logic) + управление состоянием (state) ≠ внешний вид (presentation)
Такой подход особенно органично вписывается в экосистему SvelteKit благодаря реактивной модели Svelte и гибкости компоновки компонентов.
Headless-компоненты строго следуют принципу разделения:
Это позволяет:
Headless-компоненты часто поддерживают два режима:
Пример для dropdown:
<script>
import { createDropdown } from '$lib/headless';
const dropdown = createDropdown();
</script>
<button on:click={dropdown.toggle}>
Toggle
</button>
{#if dropdown.open}
<ul>
<li on:click={() => dropdown.select('A')}>A</li>
<li on:click={() => dropdown.select('B')}>B</li>
</ul>
{/if}
Здесь компонент не диктует структуру — только предоставляет API.
Svelte предоставляет встроенные store, которые идеально подходят для headless-логики.
Пример создания headless toggle:
import { writable } from 'svelte/store';
export function createToggle(initial = false) {
const { subscribe, set, upd ate } = writable(initial);
return {
subscribe,
toggle: () => upd ate(v => !v),
on: () => se t(true),
off: () => se t(false)
};
}
Использование:
<script>
import { createToggle } from '$lib/toggle';
const toggle = createToggle();
</script>
<button on:click={toggle.toggle}>
Переключить
</button>
{#if $toggle}
<p>Включено</p>
{/if}
Здесь отсутствует какой-либо UI-компонент — только логика.
Альтернативный подход — использование компонентов без DOM-вывода:
<script>
export let let:open;
export let let:toggle;
let isOpen = false;
function toggleFn() {
isOpen = !isOpen;
}
</script>
<slot {isOpen} toggle={toggleFn} />
Использование:
<Dropdown let:isOpen let:toggle>
<button on:click={toggle}>
Menu
</button>
{#if isOpen}
<div>Контент</div>
{/if}
</Dropdown>
Компонент выступает как поставщик состояния.
Headless UI-компоненты часто включают в себя:
Пример для списка:
function createListbox(items) {
const activeIndex = writable(0);
function next() {
activeIndex.update(i => (i + 1) % items.length);
}
function prev() {
activeIndex.update(i => (i - 1 + items.length) % items.length);
}
return {
activeIndex,
next,
prev
};
}
Визуальная часть добавляет:
<ul role="listbox">
{#each items as item, i}
<li
role="option"
aria-selected={$activeIndex === i}
class:selected={$activeIndex === i}
>
{item}
</li>
{/each}
</ul>
Headless-подход позволяет строить сложные компоненты из простых:
Пример композиции:
export function createTabs(tabs) {
const active = writable(0);
return {
active,
select: (i) => active.set(i)
};
}
Для передачи состояния без проп-дриллинга:
import { setContext, getContext } from 'svelte';
const KEY = {};
export function createTabsContext() {
const tabs = createTabs([]);
setContext(KEY, tabs);
return tabs;
}
export function useTabs() {
return getContext(KEY);
}
Headless-компоненты легко сочетаются с:
Пример с Tailwind:
<button
class="px-4 py-2 bg-blue-500 text-white rounded"
on:click={toggle.toggle}
>
Toggle
</button>
Логика остаётся независимой.
Отсутствие жёсткой привязки к стилям позволяет:
Один и тот же headless-модуль:
Логика изолирована от UI:
SvelteKit активно использует серверный рендеринг. Headless-компоненты:
Разработчик должен самостоятельно:
Требуется понимание:
Headless UI — это инструмент, а не конечный продукт:
| Характеристика | Headless UI | Классические UI-библиотеки |
|---|---|---|
| Стили | Нет | Есть |
| Гибкость | Максимальная | Ограниченная |
| Быстрый старт | Ниже | Выше |
| Контроль над DOM | Полный | Частичный |
| Accessibility | Часто встроена | Часто встроена |
src/
lib/
headless/
toggle.js
dropdown.js
modal.js
components/
Button.svelte
Dropdown.svelte
Modal.svelte
Разделение:
headless/ — чистая логикаcomponents/ — визуальные оболочкиimport { writable } from 'svelte/store';
export function createModal() {
const open = writable(false);
return {
open,
show: () => open.set(true),
hide: () => open.set(false)
};
}
Использование:
<script>
import { createModal } from '$lib/modal';
const modal = createModal();
</script>
<button on:click={modal.show}>
Открыть
</button>
{#if $modal.open}
<div class="overlay" on:click={modal.hide}>
<div class="modal" on:click|stopPropagation>
Контент
</div>
</div>
{/if}
Headless UI органично сочетается с:
$storeSvelteKit усиливает этот подход благодаря:
Headless UI в SvelteKit — это не библиотека компонентов, а способ мышления:
Этот подход формирует основу масштабируемых, поддерживаемых и гибких интерфейсов.