Начало работы: SvelteKit
SSR без вспышки: hooks.server.ts подставляет тему из cookie в <html> серверного HTML; стили импортируются в корневом layout
1. Создайте приложение
Проверено с SvelteKit 3.0 и Svelte 5.57. В SvelteKit 3 конфиг Kit (и адаптер) живёт в vite.config.ts, а $lib заменён subpath-импортами #lib.
npx sv create my-app --template minimal --types ts --no-add-ons --install npm
cd my-app
2. Установите пакет
Из пакета нужен только CSS: valyro-ui/styles.css (плюс файл темы, если тема не из двух встроенных). Шрифты — отдельные файлы: тема vercel (по умолчанию в ThemeProvider) использует Geist, поэтому рядом со стилями импортируйте valyro-ui/fonts/geist.css и valyro-ui/fonts/geist-mono.css, иначе текст будет системным шрифтом; glass использует системные шрифты, ей файлы не нужны. npm сам положит react и react-dom в node_modules как peer-зависимости, но в бандл они не попадут, пока вы их не импортируете. Пока пакет не опубликован, установите tarball: npm install ../valyro-ui-0.1.0.tgz.
npm install valyro-ui
3. Подключите стили
<script lang="ts">
import 'valyro-ui/styles.css';
import favicon from '#lib/assets/favicon.svg';
import type { LayoutProps } from './$types';
let { children }: LayoutProps = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children()}
4. Тема без вспышки
data-theme (vercel, glass или подключённая отдельным файлом тема) и data-mode (light или dark) на <html> включают тему: от них зависят токены --ui-* и фон страницы. В шаблоне документа вместо значений — плейсхолдеры, их заменяет серверный хук.
<!doctype html>
<html lang="en" data-theme="%ui.theme%" data-mode="%ui.mode%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
5. Серверный хук
Хук читает cookie, пропускает только известные значения и пишет их в HTML до отправки. В SvelteKit 3 тип Handle импортируется из @sveltejs/kit/hooks.
import type { Handle } from '@sveltejs/kit/hooks';
const THEMES = ['glass', 'vercel'] as const;
const MODES = ['light', 'dark'] as const;
// Cookie values are user input: only known values reach the HTML.
function pick<T extends string>(value: string | undefined, allowed: readonly T[], fallback: T): T {
return allowed.includes(value as T) ? (value as T) : fallback;
}
export const handle: Handle = async ({ event, resolve }) => {
const theme = pick(event.cookies.get('ui-theme'), THEMES, 'glass');
const mode = pick(event.cookies.get('ui-mode'), MODES, 'light');
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%ui.theme%', theme).replace('%ui.mode%', mode)
});
};
6. Переключатель и разметка
Кнопки меняют атрибут на <html> и сохраняют выбор в cookie для следующего серверного рендера. Разметка — ровно то, что рендерят React-компоненты (Button, Badge, TextInput, Alert, Card). Без React работают только визуальные компоненты (список — в гайде HTML + CSS); разметку любого из них показывает пример «Использование» на странице компонента, когда выбран этот фреймворк.
<script lang="ts">
// The server already wrote data-theme/data-mode on <html> (hooks.server.ts);
// the buttons flip the attribute and persist the choice in a cookie for the next SSR.
function toggle(key: 'theme' | 'mode', a: string, b: string) {
const next = document.documentElement.dataset[key] === a ? b : a;
document.documentElement.dataset[key] = next;
document.cookie = `ui-${key}=${next}; path=/; max-age=31536000; samesite=lax`;
}
</script>
<main style="display: grid; justify-items: start; gap: 16px; max-width: 560px; padding: 32px">
<div style="display: flex; gap: 8px">
<button class="ui-button" data-variant="secondary" data-size="md" type="button" onclick={() => toggle('theme', 'glass', 'vercel')}>
<span class="ui-button__label">Toggle theme</span>
</button>
<button class="ui-button" data-variant="secondary" data-size="md" type="button" onclick={() => toggle('mode', 'light', 'dark')}>
<span class="ui-button__label">Toggle mode</span>
</button>
</div>
<button class="ui-button" data-variant="primary" data-size="md" type="button"><span class="ui-button__label">Deploy</span></button>
<span class="ui-badge" data-color="green" data-variant="subtle" data-size="md"><span class="ui-badge__label">Ready</span></span>
<div class="ui-field ui-text-input" data-size="md"><input aria-label="Email" placeholder="[email protected]" type="text" class="ui-field__input ui-text-input__input" /></div>
<div role="status" class="ui-alert" data-variant="info">
<span class="ui-alert__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="ui-icon"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg></span>
<div class="ui-alert__content"><div class="ui-alert__title">Heads up</div><div class="ui-alert__description">Static markup, no React at runtime.</div></div>
</div>
<div class="ui-card ui-panel" data-variant="default" data-padding="md">
<div class="ui-card__header"><h3 class="ui-card__title">Project</h3><p class="ui-card__description">Production deployment</p></div>
<div class="ui-card__content">Body</div>
<div class="ui-card__footer"><button class="ui-button" data-variant="secondary" data-size="md" type="button"><span class="ui-button__label">Open</span></button></div>
</div>
</main>
7. Соберите и запустите
С адаптером по умолчанию (adapter-auto) работает npm run preview. Для своего Node-сервера поставьте @sveltejs/adapter-node и замените импорт адаптера в vite.config.ts на import adapter from '@sveltejs/adapter-node';.
npm run check
npm run build
npm run preview
# own Node server
npm i -D @sveltejs/adapter-node
npm run build
node build
8. Интерактивные компоненты — React-островом
Select, Dialog и другие компоненты с состоянием работают только в React. Svelte-action монтирует их через createRoot только в браузере, поэтому при SSR React не рендерится. Бандл страницы с островом вырастет на React 19 (≈ +80 kB gzip), остальные страницы не меняются.
npm install react react-dom
npm install -D @types/react @types/react-dom
9. Action острова
ThemeProvider пишет тему на <html>, поэтому передавайте ему текущие значения. children у него обязательный проп — передавайте его в объекте пропсов, а не третьим аргументом createElement.
import { createElement, type ComponentType } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider, type ThemeMode, type ThemeName } from 'valyro-ui';
export interface IslandParams<P extends object> {
component: ComponentType<P>;
props: P;
theme: ThemeName;
mode: ThemeMode;
}
// Svelte action: actions run only in the browser, so React never renders during SSR.
export function reactIsland<P extends object>(node: HTMLElement, params: IslandParams<P>) {
const root = createRoot(node);
const render = ({ component, props, theme, mode }: IslandParams<P>) =>
root.render(
createElement(ThemeProvider, { theme, mode, locale: 'en-US', children: createElement(component, props) })
);
render(params);
return { update: render, destroy: () => root.unmount() };
}
10. Тема для острова
Острову нужны тема и режим ещё до монтирования. Сохраните их в locals: в handle перед return добавьте event.locals.theme = theme; и event.locals.mode = mode;, а тип опишите в app.d.ts.
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
interface Locals {
theme: 'glass' | 'vercel';
mode: 'light' | 'dark';
}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
11. Страница с островом
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = ({ locals }) => ({ theme: locals.theme, mode: locals.mode });
12. Остров в разметке
Импорт #lib/react-island.ts — с расширением: subpath-импорты не подставляют его, и без него svelte-check не найдёт модуль.
<script lang="ts">
import { Select } from 'valyro-ui';
import { reactIsland } from '#lib/react-island.ts';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let theme = $derived(data.theme);
let mode = $derived(data.mode);
function save(key: 'theme' | 'mode', value: string) {
document.documentElement.dataset[key] = value;
document.cookie = `ui-${key}=${value}; path=/; max-age=31536000; samesite=lax`;
}
const options = [
{ value: 'eu', label: 'Europe' },
{ value: 'us', label: 'United States' }
];
</script>
<main style="display: grid; gap: 16px; max-width: 640px; padding: 32px">
<div style="display: flex; gap: 8px">
<button class="ui-button" data-variant="secondary" data-size="md" type="button" onclick={() => save('theme', (theme = theme === 'glass' ? 'vercel' : 'glass'))}>
<span class="ui-button__label">Toggle theme</span>
</button>
<button class="ui-button" data-variant="secondary" data-size="md" type="button" onclick={() => save('mode', (mode = mode === 'light' ? 'dark' : 'light'))}>
<span class="ui-button__label">Toggle mode</span>
</button>
</div>
<!-- React island: a ValyroUI Select rendered by React inside a Svelte page. -->
<div use:reactIsland={{ component: Select, props: { 'aria-label': 'Region', defaultValue: 'eu', options }, theme, mode }}></div>
</main>