svelte-css-rune
is a Svelte library that allows you to effortlessly pass styles between components by introducing a new $css
rune.
Svelte provides an elegant way to scope styles to components, but passing styles between parent and child components can be challenging. There's no built-in mechanism for this, often leading to workarounds like declaring classes as global and carefully managing potential naming conflicts. This library introduces a simple way to pass styles between components. It solves the problem of style conflicts and promotes better style encapsulation when working with nested components.
The $css
rune creates a globally unique class name for a given class, ensuring that it is unique to the file and the original class name. The style tag is modified to use the generated class name, and the class is made globally accessible with the :global
selector. If the same class is used both with the $css
rune and without it, the preprocessor respects both usages, ensuring that the styles are applied correctly.
Child.svelte
<script>
let {containerClass, buttonClass} = $props();
</script>
<div class={containerClass}>
<button class={buttonClass}></button>
</div>
Parent.svelte
<script>
import Child from "./Child.svelte"
</script>
<Child containerClass={$css("container")} buttonClass={$css("button")} />
<style>
.container{
background: rebeccapurple;
}
.button{
color: white;
}
</style>
This library was designed for component libraries. Effortlessly customize the style of your components!
<script lang="ts">
import { Button } from "bits-ui";
</script>
<Button.Root class={$css("button")}>
Unlimited
</Button.Root>
<style>
.button {
color: red;
}
</style>
Popup.svelte
<script>
let {cardClass, buttonClass} = $props();
</script>
<div class={["card",cardClass]} >
<button class={["button",containerClass]} />
</div>
<style>
.card{
// Your default styles
}
.button{
// Your default styles
}
</style>
Usage.svelte
<script>
let {containerClass, buttonClass} = $props();
</script>
<Popup containerClass={$css("container")} buttonClass={$css("button")} />
<style>
.container{
// Additional styles
}
.button{
flex-grow: 1;
}
</style>
Add svelte-css-rune
as devDependency. Use the appropriate command for your package manager:
npm install --save-dev svelte-css-rune
bun add --dev svelte-css-rune
yarn add --dev svelte-css-rune
pnpm add -D svelte-css-rune
Add the preprocessor to your Svelte configuration. This is usually in svelte.config.js
/ts
, but can also be in rollup.config.js
/ts
or vite.config.js
/ts
. SvelteKit uses a svelte.config.js
/ts
file.
import cssRune from "svelte-css-rune";
export default {
preprocess: cssRune(),
// Rest of the config
}
If you are using other preprocessors, such as svelte-preprocess
, you can pass an array of preprocessors.
The order is important: svelte-css-rune
should be the LAST one in the array."
import cssRune from "svelte-css-rune";
import preprocess from "svelte-preprocess";
export default {
preprocess: [preprocess(), cssRune()],
// Rest of the config
}
You can pass options to the preprocessor. For a list of options see the Options section.
import cssRune from "svelte-css-rune";
export default {
preprocess: cssRune({
mixedUseWarnings: true
}),
// Rest of the config
}
Use the $css
rune in your components.
See the Typescript section for typescript support. You can find a svelte kit example in the example folder.
The preprocessor can be configured with the following options:
mixedUseWarnings
(default: "use"
): Emit warnings when a class is used with the $css rune and without it. Setting this to true
will warn on mixed usage in script tags, markup and when defining mixed css rules. Setting it to "use"
will not warn when defining mixed css rules. Setting it to false
will disable all warnings.
hash
can be used to override the hash function. Expects a function that takes a string and returns a string. The default hash function is the same svelte uses.
The $css
rune is a function that takes a string literal as an argument. The rune is replaced with a unique class name that is generated by the preprocessor. This class name is unique to the file and the original name, preventing naming conflicts when passing styles between components. It modifies class names within style tags to match the generated names and utilizes the :global
selector to make these generated classes globally accessible. It only affects classes that are referenced with the $css
rune. Classes used both with the $css
rune and natively (i.e., directly within the class attribute without the rune) are duplicated. This should be avoided as it results in larger bundle sizes and can potentially cause issues. The preprocessor will warn you if such an issue ever occurs.
You can use the $css
rune inside script tags, script module tags, and within the markup. It integrates seamlessly with all Svelte style features, including the new clsx
integration. It's statically replaced with the generated class name. The content of the $css
rune must be a string literal; unquoted strings are not supported. The preprocessor will issue a warning if the $css
rune is used in an unsupported way.
<script module>
export const globalClassName = $css("my-class");
</script>
<script>
let className = $css("my-class");
let {dark, bold} = $props();
</script>
<div class={myClass}></div>
<!-- You can use it directly within the markup -->
<div class={$css("my-class")}></div>
<!-- You can combine it with native usage of the same class within the same file -->
<div class="my-class">
<!-- You can pass multiple classes at once -->
<Button class={$css("button dark")} />
<!-- Works with ternary expressions -->
<Button class={dark?$css("dark"):$css("light")}></Button>
<!-- Works with clsx syntax -->
<Button class={[dark && $css("dark"), bold && $css("bold")]}></Button>
<!-- No need to modify the style tag; it works with other preprocessors like Sass -->
<style>
.my-class{
color: red;
}
.button{
color: blue;
}
.dark{
background: black;
}
.light{
background: black;
}
.bold{
font-weight: bold;
}
</style>
This preprocessor does not interfere with or disable Svelte's unused class warnings. It will produce an error if the $css
rune is misused or references a non-existent class. Error messages are descriptive and pinpoint the exact location of the issue.
/example/Component.svelte
202| });
203|
204| const className = $css("i-dont-exist")
^^^^^^^^^^^^^^^^^^^^
class i-dont-exist is not defined
Consider a component with mixed usage like this:
<div class="outer">
<Child class={$css("inner")}/>
</div>
<Child class={$css("child")}/>
<button class="child"/>
<style>
.outer .inner{
color: red;
}
.child{
color: blue;
}
</style>
All styles just work! This is compiled to:
<div class="outer">
<Child class={"inner-1oseexr"}/>
</div>
<Child class={"child-1oseexr"}/>
<button class="child"/>
<style>
.outer :global(.inner-1oseexr){
color: red;
}
.child, :global(.child-1oseexr){
color: blue;
}
</style>
Note the random string appended to the class names. This ensures unique class names. It's generated based on the path and content of the component.
This library provides full TypeScript support. It provides a global declaration for the $css
rune. If this is not working automatically for your setup
you should reference this package in a .d.ts file in your project. The default SvelteKit app comes with src/app.d.ts.
Simply add:
/// <reference types="svelte-css-rune" />
to the top of this file.
Alternatively, you can add this package to the types
field in your tsconfig.json
, or add
import type {} from "svelte-css-rune";
to every file where the rune is used.
Edge cases will only occur when mixing $css
rune with native class usage. The preprocessor will emit a warning if mixed usage is detected. It will fail if it detects an edge case that it cannot handle.
If you adhere to this warning you will not encounter any of these issues.
The preprocessor will emit a warning if it detects mixed usage of the $css
rune and native class usage. This is not recommended, as it can lead to larger bundle sizes and potential issues. This warning can be safely ignored if you are aware of the implications and can be suppressed with the mixedUseWarnings
option.
Svelte has a limitation on global selectors. Global selectors need to be at the beginning or end of a selector list. This means that a rule like this:
// NOT OK
.used-with-rune .used-natively .used-with-rune .used-natively {
color: red;
}
will not work. The preprocessor will detect this and issue a warning. Non class selectors count as native selectors.
// NOT OK
.used-with-rune #some-id .used-with-rune div{
color: red;
}
If you use :global in the selector you might not get an error from the preprocessor, but from the svelte compiler.
// OK
.used-with-rune #some-id .used-with-rune :global(div){
color: red;
}
// NOT OK
.used-with-rune #some-id .used-with-rune :global(div) .used-natively{
color: red;
}
All combinations that respect this will compile correctly.
// OK
.used-with-rune .used-with-rune .used-natively #used-natively .used-with-rune .used-rune {
color: red;
}
The preprocessors only detects native usage of the class name if known at compile time. If you use a dynamic class name, the preprocessor will not detect it. If you use a class name dynamically and with the $css rune, the preprocessor not duplicate the rule. The dynamic class name will not work anymore.
// NOT OK
<script>
let dark = false;
let className = dark?"black":"white";
</script>
<div class={$css("black")}></div>
<div class={className}></div>
This will cause the second div to not have the correct styles. Avoid mixing dynamic class names with the $css rune. The preprocessor can handle dynamic class names if they are defined inside the element.
// OK
<script>
let dark = true;
</script>
<div class={$css("black")}></div>
<div class={dark?"black":"white"}></div>
<!-- OR -->
<div class={{black: dark, white: !dark }}></div>
You can build this library using either Node.js or Bun via the build script. It compiles to both ESM and CommonJS formats.
npm run build
bun run build
Bun is required to run the tests.
bun test
This library contains end-to-end tests that verify the functionality of the preprocessor. The transform tests executes the generated svelte components and makes sure the styles are applied correctly.
The walk tests check all stages of the preprocessor to avoid regressions.
All errors and warnings are tested.
svelte-preprocess-cssmodules
can be used to archive something similar. However, its primary goal is to provide CSS Modules support, not solely a mechanism for passing classes between components.
It generates unique class names for every class within a style tag, transforming all styles in a file to use these generated names. This involves parsing and replacing a significant portion of your code, essentially replacing Svelte's built-in style handling. It also treats the class
prop as a special, magical property, and adding other attributes requires global configuration for all components. Furthermore, it disables Svelte's unused class warnings.
svelte-css-rune
is a significantly simpler library. It only replaces the $css
rune and the referenced class with a unique class name, leaving other styles untouched.
It aims for simplicity and a seamless integration with the rest of the Svelte 5 syntax. It does not disable Svelte's unused class warnings.
svelte-preprocess-cssmodules
is a great library if you require more extensive features. This library draws significant inspiration from it.
While I initially created a pull request to add this feature to svelte-preprocess-cssmodules
, I decided to create this separate library for a more focused and simpler approach.