@teiler/svelte
v0.2.0
Published
Readme
Teiler 🪡 (ˈteɪlər - tailor)
Teiler is an open source library that simplifies the creation of stylish components for various frameworks.
Currently in the alpha phase, the library is actively being developed and improved. This package is the Svelte 5 adapter and supports both CSR (Client-Side Rendering) and SSR (Server-Side Rendering).
Join our community on our Discord Server to stay informed about the latest developments, exchange ideas, and connect with fellow developers. We are continuously working on expanding our support to include more frameworks, allowing developers to effortlessly create components across various environments.
Requirements
svelte^5.16.0- A Svelte-aware bundler (Vite with
@sveltejs/vite-plugin-svelte, SvelteKit,rollup-plugin-svelte, …)
The package ships uncompiled .svelte files through the svelte export condition, so your bundler compiles them for the right target (client or server) with the same Svelte version as your application.
Example
import { component } from '@teiler/svelte'
const Button = component.button<{
_primary: boolean
}>`
display: inline-block;
border-radius: 4px;
font-size: 0.8rem;
line-height: 1.5rem;
background: transparent;
box-shadow: 0 0 0 3px #CBCBCB inset;
${({ _primary }) =>
_primary && `
color: #fff;
box-shadow: none;
background: #CBCBCB;
`
}
`<Button _primary={primary} onclick={() => count++}>Clicked {count} times</Button>Props prefixed with _ are used only for styles and are not forwarded to the DOM. Every other prop, including event handlers (onclick, oninput, …) and class, is forwarded to the rendered element.
Migrating from Svelte 4 (@teiler/svelte 0.0.x)
- Svelte 5 is required (
svelte^5.16.0); Svelte 4 is no longer supported. - Event handlers are props:
<Button onclick={handler}>instead of<Button on:click={handler}>. - If you provide the theme through context yourself, the
THEMEcontext holds a getter (() => theme) instead of a writable store.ThemeProviderusage is unchanged. - UMD/CJS bundles are gone; the package is consumed as Svelte source by your bundler.
Keyframes
import { component, keyframes } from '@teiler/svelte'
const bouncing = keyframes`
from, 20%, 53%, 80%, to {
transform: translate3d(0,0,0);
}
40%, 43% {
transform: translate3d(0, -40px, 0);
}
70% {
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0,-4px,0);
}
`
const Button = component.button<{}>`
animation: ${bouncing} 1s ease infinite;
display: inline-block;
border-radius: 4px;
font-size: 0.8rem;
line-height: 1.5rem;
background: transparent;
box-shadow: 0 0 0 3px #CBCBCB inset;
`Global styles
global creates a component that renders nothing and adds its styles to the document. Props and the theme are interpolated the same way as in components:
import { global } from '@teiler/svelte'
const GlobalStyles = global<{ _background: string }>`
body {
margin: 0;
background: ${({ _background }) => _background};
}
`<GlobalStyles _background="#fafafa" />Styles are added when the component renders and stay in the document after it unmounts.
css
Interpolated functions can return a plain string, as in the examples above. Return css instead when the styles interpolate a component, keyframes or another function: a plain template string turns ${Icon} into [object Object] (@teiler/unplugin reports it as a warning).
import { component, css } from '@teiler/svelte'
const Icon = component.span`
color: gray;
`
const Button = component.button<{ _active: boolean; _color: string }>`
color: black;
${({ _active, _color }) =>
_active &&
css`
color: ${_color};
${Icon} {
color: ${_color};
}
`}
`A css template can also be interpolated directly, e.g. styles shared by several components: ${shared}. Functions inside it get the props of the component.
Extending
Pass an existing component to component to add styles to it. The new component keeps the element of the extended one, unless you pick another element with component.<tag>:
import { component } from '@teiler/svelte'
const Button = component.button`
display: inline-block;
border-radius: 4px;
`
// renders <button>
const PrimaryButton = component(Button)`
background: #CBCBCB;
`
// renders <a> with Button styles
const ButtonLink = component.a(Button)`
text-decoration: none;
`as prop
Use as to render a component as a different element or another component without creating a new styled component. Styles stay the same, as is not passed to the rendered element:
<script lang="ts">
import { Button } from './components'
import Link from './Link.svelte'
</script>
<Button as="a" href="/docs">Docs</Button>
<Button as={Link} to="/home">Home</Button>When as is a component, it receives class, children and every other forwarded prop. Svelte has no attribute fallthrough, so the component has to apply class to its root element:
<!-- Link.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte'
import type { ClassValue } from 'svelte/elements'
const { to, class: className, children }: { to: string; class?: ClassValue; children?: Snippet } = $props()
</script>
<a href={to} class={className}>{@render children?.()}</a>Props are typed from as: as="a" accepts anchor attributes, as={Link} accepts the props of Link and requires its required props. Without as, the component accepts the attributes of its own element. svelte-check reports an unknown tag in as and attributes the target does not accept:
<Button as="a" href="/docs">Docs</Button> <!-- ok -->
<Button href="/docs">Docs</Button> <!-- error: href is not a button attribute -->
<Button as={Link}>Home</Button> <!-- error: to is required -->[!NOTE]
mount()andrender()called from TypeScript cannot inferas, so there props are typed from the element of the styled component.
withComponent
Use withComponent to create a new styled component with the same styles that always renders another element or component. The original component stays unchanged:
import { component } from '@teiler/svelte'
import Link from './Link.svelte'
const Button = component.button`
background: #f18805;
`
const FooterLink = Button.withComponent(Link)
const ButtonAnchor = Button.withComponent('a')<FooterLink to="/home">Home</FooterLink>
<ButtonAnchor href="/docs">Docs</ButtonAnchor>Props are typed from the target, so FooterLink requires to. The target component has the same requirements as a component passed to as: it has to apply class to its root element. as still works and takes precedence over the target. Extending with component(FooterLink) keeps rendering Link, component.a(FooterLink) renders <a>.
Theme
// theme.ts
export type CustomTheme = {
fontColor: string
}<!-- App.svelte -->
<script lang="ts">
import type { CustomTheme } from './theme'
import { ThemeProvider } from '@teiler/svelte'
import { Component } from './components'
let theme: CustomTheme = $state({ fontColor: 'red' })
</script>
<ThemeProvider {theme}>
<Component>Some test text</Component>
</ThemeProvider>// components.ts
import { component } from '@teiler/svelte'
const Component = component.div`
color: ${({ theme }) => theme.fontColor};
`
export { Component }To type the theme in TypeScript, extend DefaultTheme from @teiler/svelte in a declaration file (d.ts). Do not extend @teiler/core: it is a dependency of @teiler/svelte, not of your app, so with pnpm TypeScript cannot resolve it and ignores the extension without an error.
import type { CustomTheme } from './theme'
declare module '@teiler/svelte' {
export interface DefaultTheme extends CustomTheme {}
}Server-Side Rendering
Styles are collected into a style sheet provided through the STYLE_SHEET context. Create one sheet per request, render, then put the extracted CSS into the document head.
import { createStyleSheet } from '@teiler/svelte'
import { render } from 'svelte/server'
import App from './App.svelte'
const sheet = createStyleSheet({})
const { body } = render(App, { context: new Map([['STYLE_SHEET', sheet]]) })
const { css, ids } = sheet.extract()
const styleTag = `<style data-teiler="${ids.join(' ')}">${css}</style>`Read body before calling extract(): styles are inserted while the component tree renders.
[!WARNING] Always provide a style sheet through the
STYLE_SHEETcontext on the server. Without it, every request shares one module-level style sheet, so styles accumulate across requests.
Hydration
On the client, pass the ids rendered by the server to hydrate, so styles that are already in the document are not inserted again. Provide the same style sheet to the app through context:
import { createStyleSheet } from '@teiler/svelte'
import { hydrate } from 'svelte'
import App from './App.svelte'
const sheet = createStyleSheet({})
const element = document.querySelector<HTMLStyleElement>('style[data-teiler]')
sheet.hydrate(element?.dataset.teiler?.split(' ') ?? [])
hydrate(App, { target: document.body, context: new Map([['STYLE_SHEET', sheet]]) })Content Security Policy
With a strict style-src policy, pass a nonce to the style sheet and provide it through the STYLE_SHEET context. Without an explicit nonce no attribute is set, and the default style sheet never has one.
On the server, extract returns the nonce, so it can be set on the rendered <style> tag:
const sheet = createStyleSheet({ nonce })
const { body } = render(App, { context: new Map([['STYLE_SHEET', sheet]]) })
const { css, ids } = sheet.extract()
const styleTag = `<style data-teiler="${ids.join(' ')}" nonce="${nonce}">${css}</style>`On the client, read the nonce from the server rendered tag before creating the style sheet:
const element = document.querySelector<HTMLStyleElement>('style[data-teiler]')
const sheet = createStyleSheet({ nonce: element?.nonce })[!NOTE] Use the
nonceproperty, notgetAttribute('nonce'). Browsers hide the attribute when the policy is sent in a header, sogetAttributereturns an empty string.
Build plugin
Teiler works without a build step. For smaller bundles, add @teiler/unplugin to Vite, Rollup, Rolldown, webpack, Rspack or esbuild. In production builds it minifies the CSS in templates and lets the bundler remove unused styled components. In every mode it gives components stable ids, so a ${Button} selector never matches another component with the same template strings, in development names their classes after their variables (Button-t1x2y3z), and warns about mistakes, such as CSS that is ignored at runtime. Without the plugin, set an id by hand with component.button.withConfig({ componentId: 'button' }).
// vite.config.js
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import teiler from '@teiler/unplugin/vite'
export default defineConfig({
plugins: [svelte(), teiler()],
})With SSR, use it in both the server and the client build. See the plugin README for other bundlers, options and development mode.
Sew a Pattern
This tool simplifies the creation of consistent and reusable visual styles for components across various web frameworks. It provides a pattern-based approach, where patterns serve as blueprints for defining the visual style of components.
Example
// Pattern file in ui kit library
import { pattern } from '@teiler/core'
const ButtonPattern = pattern.button`
display: inline-block;
border-radius: 4px;
font-size: 0.8rem;
line-height: 1.5rem;
background: transparent;
box-shadow: 0 0 0 3px #CBCBCB inset;
`
export default ButtonPattern
// Usage of Pattern
import { ButtonPattern } from 'some-uikit-library'
import { createComponent, sew } from '@teiler/svelte'
const Button = sew(ButtonPattern, createComponent)
export default ButtonPatterns can be extended the same way as components. pattern(ButtonPattern) keeps the element of the extended pattern, pattern.<tag>(ButtonPattern) changes it:
import { pattern } from '@teiler/core'
const PrimaryButtonPattern = pattern(ButtonPattern)`
background: #CBCBCB;
`
const ButtonLinkPattern = pattern.a(ButtonPattern)`
text-decoration: none;
`