@fibrae/semi-ui-svelte is the Svelte component library of Semi Design Next, for Svelte 5 applications; for server-side rendering see Server-side rendering.
Requirements
Peer dependency: svelte ^5.57.0.
| Item | Requirement |
|---|---|
| Browser | Chrome 120, Firefox 125, Safari 17.2 or later. Supporting Safari below 17.4 needs @fibrae/semi-polyfill; see Install below |
| Modules and syntax | ESM only; syntax and built-ins ship as ESNext, without lowering |
| Node.js | 24 or later, since server rendering runs component code; 22.17 or later when Node only runs the build plugins and semi-build-scss |
| Build plugins | Vite 8 or later, webpack 5.77 or later, rspack 2 |
Install
pnpm add svelte @fibrae/semi-ui-svelte @fibrae/semi-icons-svelte
The icon package @fibrae/semi-icons-svelte is a dependency of the component library; when your code imports it directly, declare it in your own package.json, since pnpm does not allow importing undeclared packages. The illustration package @fibrae/semi-illustrations-svelte is not installed with the library; install it when you need it.
The Svelte packages ship .svelte sources and .d.ts files, not compiled output: exports carries a svelte condition, and @sveltejs/vite-plugin-svelte (built into SvelteKit) compiles them for the app's target, DOM or server. Tools that do not recognise the svelte condition or do not compile .svelte cannot use these packages.
With npm or yarn, use the equivalent commands.
To support Safari below 17.4, also install @fibrae/semi-polyfill and import it on the first line of the app entry, before any Semi component. It only adds AbortSignal.any and Element.prototype.checkVisibility, and does nothing where the browser has them natively; to add just one of them, import @fibrae/semi-polyfill/abort-signal-any or @fibrae/semi-polyfill/check-visibility.
import '@fibrae/semi-polyfill';
Import the tokens
The components import the theme tokens (the --semi-* CSS variables) and the base rules themselves, so pages render correctly without this step. When the first component is lazy loaded, though, the tokens only apply once that component's chunk has loaded. Import the tokens entry first in your app's entry file and the tokens load with the entry's CSS, so your own styles can use the --semi-* variables from the first paint.
In a Vite + Svelte app, import it in the entry file:
// src/main.ts, before any other import
import '@fibrae/semi-ui-svelte/tokens';
SvelteKit has no such entry file; import it at the top of the root layout's script:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import '@fibrae/semi-ui-svelte/tokens';
let { children } = $props();
</script>
{@render children()}
The tokens entry is a JS module that imports the same tokens the components use, so the build output holds one copy, and build plugin options such as prefixCls apply to it as well. With omitCss it is removed along with the component styles, and the full stylesheet provides the tokens.
Use components
<script lang="ts">
import { Button, Switch, Toast } from '@fibrae/semi-ui-svelte';
let checked = $state(false);
</script>
<Button theme="solid" type="primary" onclick={() => Toast.warning({ content: 'welcome' })}>Hello Semi</Button>
<Switch bind:checked aria-label="Switch" />
Controlled values use two-way bind:checked={value}; passing only checked sets the initial value, and there are no default* props such as defaultChecked. With a one-way value, what the user changes inside the component is temporary: when the value passed by the parent changes, the parent's value wins; when the same component also takes spread props ({...rest}), replacing the spread object with a new one applies the parent's value again. To keep the user's change, use bind: or write it back to your own state in the callback (such as onChange). Content props (React's ReactNode and render functions) are snippets, written inside the component tag or defined with {#snippet}; style only accepts a string, and native events use lowercase names (onclick).
A single component can also be imported by its directory path, written down to /index, such as import { Button } from '@fibrae/semi-ui-svelte/button/index'; the component file is <directory>/<name>.svelte, such as @fibrae/semi-ui-svelte/button/button.svelte.
Styles and tokens load on demand with the components, and only the components you use are bundled. Semi ships box-sizing: border-box and the scrollbar base styles itself, so a reset stylesheet such as reset.css is not needed; an extra reset changes component sizes.
Build plugins
webpack and rspack need no build plugin when you keep the default semi prefix, layered styles and per-component style imports. The components import precompiled CSS with the semi class prefix, wrapped in @layer semi: you need a rule for .css imports, such as css-loader with style-loader (or the mini-css-extract-plugin loader) in webpack, or type: 'css' in rspack. The Vite dev server works without the plugin, but vite build needs @fibrae/semi-vite-plugin, for the reason given below. For hosts that render on the server, see Server-side rendering.
For the options below, use the plugin for your build tool: @fibrae/semi-vite-plugin for Vite, @fibrae/semi-webpack-plugin for webpack 5, and @fibrae/semi-rspack-plugin for rspack 2. The plugins apply to the component library, the icon packages and the AI packages.
| Option | Default | Description |
|---|---|---|
prefixCls | 'semi' | Class prefix, rewritten both in the component JS and in the compiled styles |
cssLayer | true | Whether the styles are wrapped in @layer semi; false outputs unlayered styles |
omitCss | false | Drops the style imports inside the Semi packages so the app loads the full stylesheet instead, see Full stylesheet |
// vite.config.ts
import { defineConfig } from 'vite';
import semi from '@fibrae/semi-vite-plugin';
export default defineConfig({
plugins: [semi({ prefixCls: 'my' })],
});
// webpack.config.mjs
import SemiWebpackPlugin from '@fibrae/semi-webpack-plugin';
export default {
plugins: [new SemiWebpackPlugin({ prefixCls: 'my' })],
};
// rspack.config.mjs; the project needs a rule that treats .scss as CSS
import { SemiRspackPlugin } from '@fibrae/semi-rspack-plugin';
export default {
module: { rules: [{ test: /\.s?css$/, type: 'css' }] },
plugins: [new SemiRspackPlugin({ prefixCls: 'my' })],
};
The webpack plugin also takes extractCssOptions (for example to use the mini-css-extract-plugin loader) and overrideStylesheetLoaders (to append your own loaders to the default chain). If class names are still semi-* in vite dev, the prebundle cache comes from the previous configuration; rebuild it with vite --force.
Svelte projects differ as follows:
- Vite only: the Svelte packages are compiled by
@sveltejs/vite-plugin-svelte(built into SvelteKit).@fibrae/semi-webpack-pluginand@fibrae/semi-rspack-plugindo not process the Svelte packages; do not use them in a Svelte project. @fibrae/semi-vite-pluginapplies to@fibrae/semi-ui-svelte,@fibrae/semi-ai-svelteand@fibrae/semi-icons-svelte, andprefixCls,cssLayerandomitCssmean the same, asplugins: [svelte(), semi()]; for SvelteKit see Server-side rendering.
The published .scss files use semi: URLs that plain Sass cannot resolve; compile them through a build plugin or @fibrae/semi-scss-compile.
Full stylesheet
To load every component's styles at once, import the full stylesheet and turn on the build plugin's omitCss; otherwise the components' own style imports are bundled again and each component's styles appear twice. The full stylesheet includes the tokens and the icon styles, uses the semi class prefix, and is wrapped in @layer semi.
import '@fibrae/semi-ui-svelte/styles.css';
// with the AI components, import theirs after it
import '@fibrae/semi-ai-svelte/styles.css';
With omitCss the plugins no longer compile styles, so cssLayer has no effect, and the webpack and rspack plugins throw when both are passed. For a full stylesheet with a custom prefix or without layers, generate one with @fibrae/semi-scss-compile: --prefix matches the plugin's prefixCls, and --no-layer outputs unlayered styles. -f and -i are optional and default to the foundation and icons-core that @fibrae/semi-scss-compile itself depends on, so its version must match the component library; when it does not, point -f and -i to the dist of the @fibrae/semi-foundation and @fibrae/semi-icons-core installed in your app, which pnpm requires as direct dependencies. --ai always needs a path, to the dist of @fibrae/semi-ai-foundation; with pnpm add it as a direct dependency at the same version as the component library.
pnpm add -D @fibrae/semi-scss-compile
pnpm exec semi-build-scss -o src/semi.css --min --prefix my
# with the AI components, generate a second file and import it after the first (also install @fibrae/semi-ai-foundation)
pnpm add -D @fibrae/semi-ai-foundation
pnpm exec semi-build-scss --ai node_modules/@fibrae/semi-ai-foundation/dist -o src/semi-ai.css --min --prefix my
Customize styles
Customizing styles needs no build plugin: override the --semi-* CSS variables in your own stylesheet to change the global colors, radii, font sizes, spacing and so on (see Design Tokens for the list, and Dark Mode for dark values). Semi's styles sit in @layer semi, so your unlayered CSS rules always win and can override a component's own styles without !important.
:root {
--semi-color-primary: var(--semi-violet-5);
}
.my-toolbar .semi-button {
border-radius: 0;
}
When using Tailwind, see Tailwind for the layer order and for using Semi tokens in utility classes.
Icons and illustrations
<script lang="ts">
import { IconHome } from '@fibrae/semi-icons-svelte';
</script>
<IconHome aria-label="Home" />
For a custom SVG, use the children snippet of the default export Icon; its argument carries fill:
<script lang="ts">
import Icon from '@fibrae/semi-icons-svelte';
</script>
<Icon size="large">
{#snippet children({ fill })}
<svg viewBox="0 0 24 24" {fill}><circle cx="12" cy="12" r="10" /></svg>
{/snippet}
</Icon>
Illustrations are imported from @fibrae/semi-illustrations-svelte, such as IllustrationNoContent.
Icon styles load with the icon components. See Icon for the icon list and usage.
AI components
The AI components (AIChatInput, AIChatDialogue, Chat, Sidebar and others) live in a separate AI package built on the component library, and their styles load with the components. When loading full stylesheets, import the AI package's after the component library's. See AI components for an overview.
pnpm add @fibrae/semi-ai-svelte
@fibrae/semi-ai-svelte currently completes only AIChatDialogue; AIChatInput, Chat and Sidebar are being ported and are not exported yet.
Server-side rendering
For server-side rendering and hydration, configure the host as the table below shows; most hosts also need the Semi build plugin. By default styles load per component: the component JS imports its own styles and you do not import styles.css. The full stylesheet is an optional route: turn on the build plugin's omitCss and import styles.css once, see Full stylesheet. Do not mix the two routes; mixed, the CSS roughly doubles.
| Host | Install | Host configuration | Styles |
|---|---|---|---|
| SvelteKit 3 | @fibrae/semi-ui-svelte, -D @fibrae/semi-vite-plugin | plugins: [sveltekit({ adapter }), semi()] | none |
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import semi from '@fibrae/semi-vite-plugin';
// replace adapter with the SvelteKit adapter you use
export default defineConfig({
plugins: [sveltekit({ adapter }), semi()],
});
Components render server HTML through render from svelte/server and in SvelteKit, and the browser hydrates with hydrate. Ids inside components come from $props.id() and match between server and browser; onMount and $effect do not run on the server, so state that depends on them (such as measured sizes) appears after hydration.
Common conventions:
- Import locales from the
locale/source/subpath of the UI package (such aslocale/source/en_US); you do not need to install@fibrae/semi-foundationdirectly. - Set
semiGlobalonly at app startup; useConfigProviderfor configuration that changes per request. - Overlays that are open by default appear after hydration.
- CodeHighlight bundles no languages: install
shikidirectly in your app (pnpm does not allow importing undeclared packages) and pass languages fromshiki/langs/*throughCodeHighlight.registerorlanguages, see CodeHighlight.