Two components can both call it .card

Start here·5 min· Assumes: state

Every stylesheet you have ever written was a shared room. You call something .card, someone two folders away calls something else .card, and one of you loses: usually the one who loaded first, always the one who finds out last.

Here are two components. Both use the class name card, both say what .card looks like, and neither one wins, because neither one leaks.

A <style> block written inside a component styles that component and nothing else, and no JavaScript runs to make that true.

export default function FirstCard() @{
	<article class="card">
		<style>
			.card {
				padding: 0.7em 0.9em;
				border: 2px solid rgb(28, 26, 22);
				background: rgb(198, 226, 255);
			}
			.name {
				font-weight: 700;
			}
		</style>
		<p class="name">First card</p>
	</article>
}
export default function SecondCard() @{
	<article class="card">
		<style>
			.card {
				padding: 0.7em 0.9em;
				border: 2px solid rgb(28, 26, 22);
				background: rgb(214, 245, 214);
			}
			.name {
				font-weight: 700;
			}
		</style>
		<p class="name">Second card</p>
	</article>
}

Put both on one page and the first card is blue and the second is green. Neither file mentions the other, neither one is more specific than the other, and the order you import them in does not matter.

The class name in your file is not the class name in the page

The trick is one extra class, added for you at build time. Each component module that has a <style> block gets its own scope class, and the compiler puts that class in two places: on the selectors inside the block, and on the elements the component renders.

So .card in the file becomes .card.mk-1a6qykh in the stylesheet, and the <article> goes out as class="card mk-1a6qykh". The other component gets a different hash, so its .card rule describes a class combination that the first card's markup does not have. Two rules that looked identical in your editor cannot match each other's elements.

Two details are worth knowing before you write your first block. Media queries and other at-rules are descended into, so the rules inside them are scoped like any other. Animation names are left alone, so a @keyframes you define is global and can be shared, which is almost always what you want.

Nothing runs to make it happen

There is no runtime here at all. The compiled CSS is handed to the bundler as an ordinary stylesheet, "so Vite/Rolldown's CSS pipeline owns bundling and delivery; no JavaScript runs to apply styles". The scope class is in the HTML the server sends, so a reader with JavaScript disabled still gets the styled page.

That also means the usual costs of scoping are not here. No class names are generated while the page runs, no <style> element is written into the head as a component appears, and there is no first-paint flash while a stylesheet is assembled.

A class is an expression like any other

class takes a value, and any expression will do. This is the same state you already have, so the class changes when the value changes:

import { state } from '@markless/core';

export function Classes() @{
	let active = state(false);

	<section class={active ? 'panel active' : 'panel idle'}>
		<button class={active ? 'toggle on' : 'toggle off'} onClick={() => (active = !active)}>
			Toggle
		</button>
	</section>
}

The value is a string, and you build it with the ordinary tools: a ternary, a template literal, a small function that takes a couple of booleans and returns the string. There is no class-name helper to import, because there is nothing here that a helper would be helping with.

The style attribute takes a string in the same way, and is the right choice for a value that comes from arithmetic rather than from a set of named looks:

import { state } from '@markless/core';

export function Bar() @{
	let percent = state(40);

	<div class="bar" style={`width: ${percent}%`}>{percent}</div>
}

Reach for it when the number is genuinely continuous. A dozen named states belong in classes, where the stylesheet can see them.

Coming from another framework?

This is closest to a CSS module, except that you do not import the class names and you do not rename anything: you write the plain name and the compiler adds a scope class beside it, so what you read in the file is what you read in the browser's element inspector. It is not CSS-in-JS: there is no runtime, no template literal compiled into a component, and no style element written while the page runs. Styles composed from a parent, and an escape hatch for deliberately global rules, are both named as unspecified in the host contract today, so a rule you want to be global belongs in an ordinary stylesheet you link yourself.

Try it yourself

Give a component a <style> block, then look at the element in your browser's inspector. You will see your own class name next to a short hashed one, and the rule in the stylesheet carrying both. Then copy the whole component to a second file, change one colour, and put both on a page. Watching two identical selectors not fight is the moment the idea lands.

Next: splitting a page into components.