One thing I’ve always loved about Vue is its Single-File Component (SFC) approach. In a single .vue file, you colocate JavaScript logic, template
markup, and CSS styles. Everything that belongs to a component lives
together, which makes components easy to reason about, refactor, and
move around.
I wanted that same feeling when working with Preact.
Preact doesn’t prescribe a single-file format like Vue does, but it’s flexible enough that you can build your own conventions. This article describes my own approach to single-file Preact components, inspired by Vue SFCs, without introducing a build step, custom compilers, or framework-level abstractions.
The Goal
- Keep component logic, markup, and styles in one file
- Avoid complex tooling or build steps
- Stay close to idiomatic Preact
- Make components easy to read and move
The Core Idea
A single-file Preact component in this style contains a normal Preact function component, its markup, and its styles defined right next to each other.
The only custom utility involved is a very small csstemplate tag helper. There’s no magic beyond that.
Minimal Example
// ExampleComponent.ts
import { html } from "htm/preact"
import { useState } from "preact/hooks"
import { css } from "/utils/markup"
export function MyComponent() {
const [count, setCount] = useState(0)
const view = html`
<div data-scope="MyComponent">
<h1>Hello World</h1>
<p>Count: ${count}</p>
<button onClick=${() => setCount((c) => c + 1)}>
Increment
</button>
</div>
`
const style = css`
@scope ([data-scope="MyComponent"]) to ([data-scope]) {
h1 {
color: var( --primary-600);
}
}
`
return [view, style]
}The css Helper
The css helper is intentionally boring. It’s just a
template tag that returns a <style> node
containing raw CSS.
// /utils/css.ts
import { h, type JSX } from "preact"
export function css(
strings: TemplateStringsArray,
...values: Array<string | number | null | undefined>): JSX.Element {
let content = ""
for (let i = 0; i < strings.length; i++) {
content += strings[i]
if (i < values.length && values[i] != null) {
content += values[i]
}
}
return h("style", {
dangerouslySetInnerHTML: { __html: content.trim() }
})
}There’s no runtime, no hashing, and no transformation step. What you write is what ends up in the DOM.
Why I Like This Approach
- Colocation – logic, markup, and styles live together
- Simplicity – no custom build pipeline or compiler
- Predictability – plain Preact components
- Easy refactoring – move or delete a component as a single file
It feels very close to Vue’s SFC philosophy, but implemented entirely at the user-land level.
Closing Thoughts
Preact doesn’t force you into a specific component format — and that’s a strength. With a tiny helper and modern CSS, you can recreate much of what makes Vue SFCs enjoyable, while staying fully within the Preact ecosystem.