Svelte 5 components rendered to the terminal with real CSS.
Write standard Svelte components with <style> blocks. They render in the terminal with ANSI escape sequences — flexbox layout, scoped styles, CSS variables, pseudo-classes, all on a cell grid.
Early release. Svelterm requires an unmerged Svelte branch (
svelte-custom-rendererby @paoloricciuti) that adds the custom renderer API. It is not usable with mainline Svelte yet.
<script>
let count = $state(0)
</script>
<style>
.counter {
display: flex;
flex-direction: column;
border: rounded;
border-color: cyan;
padding: 1cell;
gap: 1cell;
}
.value {
color: yellow;
font-weight: bold;
}
button:focus {
color: cyan;
font-weight: bold;
}
</style>
<div class="counter">
<span>Count: <span class="value">{count}</span></span>
<button onclick={() => count++}>Increment</button>
<button onclick={() => count--}>Decrement</button>
</div>
import { run } from '@svelterm/core/app'
import { readFileSync } from 'fs'
import App from './App.svelte'
const css = readFileSync('./main.css', 'utf-8')
run(App, { css })
The same Svelte component can render in both terminal and browser. Terminal-specific CSS values (border: rounded, 1cell, opacity: dim) are naturally ignored by browsers — they're invalid CSS. Browser-specific rules go in @media (display-mode: screen).
<style>
.greeting {
border: rounded;
border-color: cyan;
padding: 1cell;
}
@media (display-mode: screen) {
.greeting {
border: 2px solid #00b4d8;
border-radius: 8px;
padding: 1rem;
}
}
</style>
To build for each target, use separate Vite configs — one with customRenderer: '@svelterm/core' for terminal, one without for browser. The component source is the same.
Standard CSS works as expected. These are the terminal-specific additions:
| Feature | Terminal | Browser |
|---|---|---|
| Borders | single, double, rounded, heavy (box-drawing characters) |
Ignored (invalid values) |
| Units | cell — one monospace character position |
Ignored (unknown unit) |
| Opacity | dim — terminal dim attribute |
Ignored (invalid value) |
| Colors | ANSI names, 256-color, truecolor hex, CSS named colors | Standard CSS colors |
| Media | @media (display-mode: terminal) |
@media (display-mode: screen) |
var(), calc(), @media, @keyframesflex-direction, justify-content, align-items, flex-grow, flex-shrink, gap, flex-wrap:focus and :hover pseudo-classes<input> and <textarea> with readline-like editingprefers-color-scheme detection via terminal queriesSvelterm requires the experimental custom renderer API, available on the svelte-custom-renderer branch:
# Clone the branch
git clone -b svelte-custom-renderer https://github.com/paoloricciuti/svelte.git svelte-fork
cd svelte-fork
pnpm install
pnpm -C packages/svelte build
Then reference it in your project's package.json:
{
"peerDependencies": {
"svelte": "file:../svelte-fork/packages/svelte"
}
}
Configure the Svelte compiler to use svelterm as the custom renderer:
// vite.config.ts
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
plugins: [
svelte({
compilerOptions: {
experimental: {
customRenderer: '@svelterm/core',
},
css: 'external',
},
}),
],
build: {
target: 'node22',
rollupOptions: {
external: ['svelte', 'svelte/renderer', 'svelte/internal',
'svelte/internal/client', 'ws', 'http', 'crypto'],
},
},
})
run(component, options?)Start an interactive terminal application.
import { run } from '@svelterm/core/app'
const stop = run(App, {
css, // Extracted CSS string
fullscreen: true, // Use alternate screen buffer (default: true)
mouse: true, // Enable mouse input (default: true)
props: { name: 'world' },
})
// Call stop() to shut down and restore terminal
Returns a function that stops the application, unmounts the component, and restores the terminal.
MIT