@chai-ui/svelte
v1.4.1
Published
Chai UI Svelte components — smart, drop-in UI for AI apps: Composer, Media Analyzer, Result Card, Edit Card, Side Panel, and the useComposer hook.
Downloads
1,516
Maintainers
Readme
@chai-ui/svelte
Chai UI for Svelte: drop-in components for the input and output surfaces of generative-media apps: a prompt composer, a media-to-prompt analyzer, a result card and an edit card, with the AI work built in and wired to Fal.ai, OpenRouter, Vercel AI Gateway or your own engine. Everything is controlled: you own the state, and the components render it.
These are the same components as @chai-ui/react
and @chai-ui/vue, with the
same behavior and the same prop names. All three are thin layers over
@chai-ui/core and run one shared test suite, including the WCAG 2.2 AA
checks (axe, keyboard and focus).
Building with it, or pointing a coding agent at it? Read
DESIGN.md (shipped in the package) for the usage rules,
and the docs site's llms.txt for
the API. The code boxes on chai-ui.com/docs
switch between React, Vue and Svelte.
Install
pnpm add @chai-ui/svelte @chai-ui/core @chai-ui/tokensSvelte 5, in SvelteKit or a plain Svelte app. No Chai-specific build setup:
the package ships .svelte files that your app compiles, and the
components render on the server too.
Your own font
style.css loads Roboto, Material Design 3's typeface. If your app has its
own font, import @chai-ui/svelte/style-without-fonts.css in its place (the
same styles, without Roboto) and point the components at your font:
:root { --chai-type-family-material: "Your Font", system-ui, sans-serif; }Dark theme
The components have a dark theme. It's opt-in: set data-chai-theme="dark"
on <html>, or on any element, and every component inside it switches.
<html data-chai-theme="dark">Chai doesn't follow the system setting on its own, because your app decides
its theme. Following it, overriding a single color, and the dark values
themselves are in the
@chai-ui/tokens README.
Quick start
<!-- src/routes/+layout.svelte: tokens first, then the component styles that read them -->
<script lang="ts">
import "@chai-ui/tokens/css";
import "@chai-ui/svelte/style.css";
import { ChaiProvider } from "@chai-ui/svelte";
let { children } = $props();
</script>
<ChaiProvider>{@render children()}</ChaiProvider><!-- src/routes/+page.svelte -->
<script lang="ts">
import { Composer, ResultCard, createFalEngine, useComposer } from "@chai-ui/svelte";
const composer = useComposer({ engine: createFalEngine() });
let value = $state("");
const busy = $derived(composer.run?.results.some((r) => r.status === "queued" || r.status === "running") ?? false);
async function enhance(prompt: string) {
value = await composer.enhance!(prompt);
}
</script>
<Composer
{value}
onChange={(next) => (value = next)}
onSubmit={composer.submit}
submitting={busy}
onAbort={composer.cancel}
onEnhance={composer.enhance && enhance}
enhancing={composer.enhancing}
/>
{#if composer.run}
<ResultCard results={composer.run.results} prompt={composer.run.request.prompt} onAction={() => {}} />
{/if}To try it before setting up a provider, leave out engine and
ChaiProvider: everything then runs on built-in mocks, with no network
calls and no keys.
The Svelte names
The props are React's, unchanged: value and onChange, attachments and
onAttachmentsChange, onSubmit, onAction, onEvent. Only these differ:
| React | Svelte |
|---|---|
| className, and style as an object | class, and style as a string |
| useComposer / useMediaAnalyzer return values | They return an object whose fields stay current: read composer.run, don't destructure it |
| Options that change between renders | Pass a function: useComposer(() => ({ engine, editImage })) |
| <EditCard {...edit} /> | The same: {#if composer.edit}<EditCard {...composer.edit} />{/if} |
| SidePanel's logo and footer, a Composer chip or menu item's icon | Snippets (footer also takes text) |
Call useComposer, useMediaAnalyzer and useReasoning while a component
initializes (in its <script>), as with Svelte's own getContext.
The use prefix is kept from React on purpose, so one name works across
the docs. It marks a function you call during component setup, as Svelte
libraries such as Runed and bits-ui also do, and it has nothing to do with
Svelte's use: directive.
Some controls show only when you pass their callback, as in React: the
optimize button with onEnhance, stop with onAbort, the Model menu with
onModelChange.
Docked the bar? Set menuPlacement="above" for a bar fixed to the bottom
of the screen, or "below" for one fixed to the top, so every menu opens
into the page. Left out, it's "auto": each menu goes where it fits.
What's in the package
Composer: prompt, attachments, use cases, model and aspect-ratio menus with suggested models, prompt optimization, auto-select routing, stop and retry.MediaAnalyzer+useMediaAnalyzer: media in, a generation-ready prompt out.ResultCard: one prompt's results, paged, with votes, retry, download, share, details, streaming text and quality-check tags (from your backend, or the opt-in judge:useComposer({ judgeModel, judgeChecks })).- Analytics:
onEventon every component reports what the person does and sees, oneChaiEventeach, for any analytics tool:onEvent={(e) => posthog.capture(e.name, e.properties)}. EditCard: marked regions with their own instructions, and each edit as a new version.SidePanel: a panel beside your page with a Composer and its answers, for questions about the page (opt-inpageContext, shown as a removable chip) or anything else, with prompt tips, push or overlay, and either side. Same props as React's, withlogoandfooteras snippets. See the React README's "A side panel" for the details.useComposer,ChaiProvider, and theToggle,SearchMenuandPaginationbuilding blocks.- The engines and types from
@chai-ui/core, re-exported.
Your API keys stay on your server
The engines call one route on your server, which adds the key. In SvelteKit 3, declare the keys, then add the route:
// src/env.ts: the keys the route reads. Each is optional: set the ones you use.
import { defineEnvVars } from "@sveltejs/kit/env";
const optional = (value: string | undefined) => value;
export const variables = defineEnvVars({
FAL_KEY: { schema: optional },
OPENROUTER_API_KEY: { schema: optional },
AI_GATEWAY_API_KEY: { schema: optional },
});// src/routes/api/chai/[...path]/+server.ts
import { AI_GATEWAY_API_KEY, FAL_KEY, OPENROUTER_API_KEY } from "$app/env/private";
import { createChaiHandler } from "@chai-ui/core/server";
import type { RequestHandler } from "./$types";
const chai = createChaiHandler({ falKey: FAL_KEY, openRouterKey: OPENROUTER_API_KEY, aiGatewayKey: AI_GATEWAY_API_KEY });
export const fallback: RequestHandler = ({ request }) => chai.handle(request);On SvelteKit 2, skip src/env.ts and read the keys with
import { env } from "$env/dynamic/private" (env.FAL_KEY, and so on).
Then set FAL_KEY, OPENROUTER_API_KEY or AI_GATEWAY_API_KEY in your
server's environment (.env works in development). Other servers, and
authorize and allowedModels for production, are in the
@chai-ui/core README.
Without a server route
In a desktop app (Tauri, Electron) with no server route, write an engine
that hands each run to your app's backend. Pass it to
useComposer({ engine: desktopEngine }) and to
<ChaiProvider reasoningEngine={desktopEngine}>. The
@chai-ui/core README
has a Tauri example, voice input, and what Stop does there.
