Tooltips и popovers

Tooltip и popover — два близких по назначению, но различающихся по поведению интерфейсных паттерна. Оба используются для отображения дополнительной информации без перегрузки основного интерфейса.

  • Tooltip — краткая подсказка, появляющаяся при наведении или фокусе
  • Popover — более сложный контейнер с произвольным содержимым (кнопки, формы, списки), открывающийся по клику или программно

В контексте SvelteKit UI-библиотек (например, Skeleton, Flowbite-Svelte, Melt UI) эти компоненты реализуются с учетом реактивности Svelte, доступности (a11y) и гибкости позиционирования.


Базовые принципы реализации

Состояние и реактивность

В Svelte управление tooltip/popover обычно строится через локальное состояние:

<script>
    let open = false;
</script>

<button
    on:mouseenter={() => open = true}
    on:mouseleave={() => open = false}
>
    Hover me
</button>

{#if open}
    <div class="tooltip">Подсказка</div>
{/if}

Ключевая идея — декларативное отображение через {#if} и реактивные переменные.


Позиционирование

Точное размещение всплывающего элемента относительно триггера — одна из главных задач. Используются:

  • position: absolute + вычисления
  • сторонние библиотеки (например, Floating UI)
  • встроенные утилиты UI-библиотек

Пример с вычислением координат:

<script>
    import { onMount } from 'svelte';

    let trigger;
    let tooltip;
    let style = '';

    onMount(() => {
        const rect = trigger.getBoundingClientRect();
        style = `
            top: ${rect.bottom + 8}px;
            left: ${rect.left}px;
        `;
    });
</script>

<button bind:this={trigger}>Hover</button>

<div bind:this={tooltip} style={style}>
    Tooltip
</div>

Tooltip: особенности и реализация

Поведение

  • Появляется при hover или focus
  • Исчезает при mouseleave или blur
  • Не содержит интерактивных элементов
  • Обычно имеет небольшую задержку появления

Реализация с задержкой

<script>
    let open = false;
    let timeout;

    function show() {
        timeout = setTimeout(() => open = true, 200);
    }

    function hide() {
        clearTimeout(timeout);
        open = false;
    }
</script>

<button on:mouseenter={show} on:mouseleave={hide}>
    Hover
</button>

{#if open}
    <div class="tooltip">Информация</div>
{/if}

Доступность (a11y)

Для tooltip важно:

  • использовать aria-describedby
  • обеспечивать доступ через клавиатуру
<button aria-describedby="tip1">?</button>

<div id="tip1" role="tooltip">
    Подсказка
</div>

Анимации

Svelte предоставляет встроенные переходы:

<script>
    import { fade } from 'svelte/transition';
</script>

{#if open}
    <div transition:fade>
        Tooltip
    </div>
{/if}

Popover: расширенный функционал

Отличия от tooltip

  • открывается по клику
  • может содержать интерактивные элементы
  • требует управления фокусом
  • закрывается по клику вне области

Базовая реализация

<script>
    let open = false;
</script>

<button on:click={() => open = !open}>
    Open
</button>

{#if open}
    <div class="popover">
        Контент popover
    </div>
{/if}

Закрытие при клике вне

Используется обработчик на уровне документа:

<script>
    import { onMount, onDestroy } from 'svelte';

    let open = false;
    let popover;

    function handleClick(event) {
        if (popover && !popover.contains(event.target)) {
            open = false;
        }
    }

    onMount(() => {
        document.addEventListener('click', handleClick);
    });

    onDestroy(() => {
        document.removeEventListener('click', handleClick);
    });
</script>

<button on:click={() => open = !open}>Toggle</button>

{#if open}
    <div bind:this={popover}>
        Popover content
    </div>
{/if}

Управление фокусом

Popover должен:

  • получать фокус при открытии
  • возвращать фокус на триггер при закрытии
<script>
    let popoverEl;
    let buttonEl;

    function openPopover() {
        popoverEl?.focus();
    }
</script>

<button bind:this={buttonEl} on:click={openPopover}>
    Open
</button>

<div tabindex="0" bind:this={popoverEl}>
    Content
</div>

Использование UI-библиотек

Melt UI (headless подход)

Позволяет полностью контролировать разметку:

<script>
    import { createPopover } from '@melt-ui/svelte';

    const popover = createPopover();
</script>

<button use:popover.trigger>
    Open
</button>

<div use:popover.content>
    Content
</div>

Особенности:

  • логика отделена от представления
  • высокая гибкость
  • встроенная доступность

Flowbite-Svelte

Готовые стилизованные компоненты:

<script>
    import { Tooltip } from 'flowbite-svelte';
</script>

<Tooltip text="Подсказка">
    <button>Hover</button>
</Tooltip>

Плюсы:

  • минимальная настройка
  • готовый дизайн
  • быстрый старт

Skeleton UI

Фокус на Tailwind и адаптивности:

<script>
    import { Popup } from '@skeletonlabs/skeleton';
</script>

<Popup>
    <button slot="trigger">Open</button>
    <div slot="content">Content</div>
</Popup>

Управление позицией и collision detection

Продвинутые popover используют:

  • автоматический выбор стороны (top, bottom, left, right)
  • предотвращение выхода за границы экрана
  • смещение (offset)

Пример логики:

if (rect.bottom + tooltipHeight > window.innerHeight) {
    position = 'top';
}

Современные библиотеки делегируют это Floating UI.


Portal и layering

Popover часто рендерится вне текущего DOM-дерева (в <body>), чтобы:

  • избежать проблем с overflow
  • управлять z-index

В Svelte:

<svelte:body>
    {#if open}
        <div class="popover">...</div>
    {/if}
</svelte:body>

Управление стеком (z-index)

Всплывающие элементы должны:

  • перекрывать основной контент
  • не конфликтовать друг с другом

Типичная схема:

  • tooltip: z-index 1000
  • popover: z-index 1100
  • modal: z-index 1200

Сценарии использования

Tooltip

  • пояснение иконок
  • краткие описания полей
  • вспомогательные подсказки

Popover

  • меню действий
  • формы ввода
  • карточки с подробной информацией

Производительность

Важно учитывать:

  • не создавать лишние DOM-узлы
  • использовать условный рендеринг
  • избегать частых пересчетов layout

Оптимизация:

{#if open}
    <PopoverContent />
{/if}

Типичные ошибки

  • Tooltip с интерактивным содержимым
  • Отсутствие поддержки клавиатуры
  • Неправильное закрытие popover
  • Жестко заданные координаты
  • Игнорирование mobile-устройств

Адаптация под мобильные устройства

Tooltip:

  • заменяется на tap/long press
  • часто отключается

Popover:

  • должен учитывать touch-события
  • закрываться по тапу вне области

Архитектурные подходы

Headless компоненты

  • логика отделена от UI
  • высокая переиспользуемость

Fully styled компоненты

  • быстрый старт
  • ограниченная кастомизация

Интеграция с SvelteKit

В контексте маршрутизации:

  • popover не должен сбрасываться при навигации без необходимости
  • состояние можно хранить в store
import { writable } from 'svelte/store';

export const popoverOpen = writable(false);

Расширенные возможности

  • вложенные popover
  • динамическое содержимое (async данные)
  • управление через store
  • глобальные менеджеры всплывающих элементов

Тестирование

Проверяются:

  • открытие/закрытие
  • позиционирование
  • доступность
  • взаимодействие с клавиатурой

Инструменты:

  • Playwright
  • Testing Library

Безопасность и UX

  • избегать перекрытия важных элементов
  • не блокировать основной поток взаимодействия
  • не злоупотреблять tooltip’ами

Правильная реализация tooltip и popover в SvelteKit обеспечивает баланс между информативностью интерфейса и его чистотой, сохраняя производительность и доступность на высоком уровне.