Single-File Preact Components

Published on January 11, 2026

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.