swift-html

0.7.1

Framework-neutral declarative HTML engine for Swift applications
1amageek/swift-html

What's New

0.7.1

2026-06-25T07:09:29Z

Included

  • Adds typed @container stylesheet items through StylesheetItem.container and the container(_:) stylesheet builder helper.
  • Adds regression coverage for nested typed container rules.

SwiftHTML

SwiftHTML is a low-level declarative HTML engine for Swift applications.

It provides a typed HTML DSL, component model, renderer, internal graph diffing, typed CSS helpers, hydration metadata, client event bindings, split-loading contracts, and browser-neutral runtime contracts. It is deliberately framework-neutral: it does not depend on Vapor, JavaScriptKit, SwiftWebUI, server routing, or a concrete WebAssembly bootstrap.

flowchart LR
  Tree["Component tree"] --> Graph["internal HTMLGraph"]
  Graph --> Artifact["RenderArtifact facade"]
  Graph --> Patch["HTMLPatch"]
  Artifact --> HTML["SSR HTML"]
  Artifact --> Hydration["Hydration manifest"]
  Hydration --> Runtime["Runtime package"]
  Runtime --> Host["Browser host"]
Loading

Status

SwiftHTML is an early pre-1.0 package extracted from SwiftWeb. The public API is intended to be small and framework-neutral, but runtime and hydration contracts may still evolve before 1.0.

This README describes the current main branch. Use the README from a matching Git tag when depending on a tagged release.

Package Role
SwiftHTML HTML DSL, rendering, diffing, state, environment, CSS, hydration contracts, browser command contracts.
SwiftHTMLEmbedded Experimental static HTML tree and DOM host contract that can compile with Embedded Swift for small client WASM bundles.
SwiftHTMLPreview Xcode #Preview bridge, SwiftUI host view, and WebKit-backed HTML preview surface.
Higher-level server package HTTP routing, request/response integration, security middleware, server action gateway.
Higher-level UI package Design-system components, visual defaults, JavaScriptKit adapter, WASM bootstrap.

Requirements

SwiftHTML currently requires Swift 6.3 and Apple platform SDKs that provide Synchronization.Mutex.

Platform Minimum
macOS 15
iOS 18
tvOS 18
watchOS 11
visionOS 2

Installation

Add SwiftHTML to a Swift Package. The examples in this README use the current main branch API:

// swift-tools-version: 6.3
import PackageDescription

let package = Package(
    dependencies: [
        .package(url: "https://github.com/1amageek/swift-html.git", branch: "main"),
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "SwiftHTML", package: "swift-html"),
            ]
        ),
        .target(
            name: "AppPreviews",
            dependencies: [
                .product(name: "SwiftHTMLPreview", package: "swift-html"),
            ]
        ),
    ]
)

Quick Start

import SwiftHTML

struct HomePage: Component {
    var body: some HTML {
        document {
            html {
                head {
                    meta(.charset("utf-8"))
                    title("SwiftHTML")
                }
                SwiftHTML.body {
                    main(.class("page")) {
                        h1("SwiftHTML")
                        p("Typed HTML rendered from Swift values.")
                        a(.href("/docs")) {
                            "Read the docs"
                        }
                    }
                }
            }
        }
    }
}

let html = HomePage().render()
print(html)

Copyable Snippets

The snippets below are intentionally complete enough to paste into a Swift file. They include imports, model values, components, and the render or preview entry point. The examples use fenced Markdown code blocks so documentation surfaces can expose their normal copy action.

Server-Rendered Page

import SwiftHTML

struct ArticleSummary: Sendable {
    let id: String
    let title: String
    let excerpt: String
    let href: String
}

struct ArticleListPage: Component, Sendable {
    let articles: [ArticleSummary]

    var body: some HTML {
        document {
            html {
                head {
                    meta(.charset("utf-8"))
                    title("Latest Articles")
                }
                SwiftHTML.body {
                    main(.class("article-list")) {
                        h1("Latest Articles")
                        p(.class("lead"), text: "Rendered on the server with typed SwiftHTML components.")

                        section(.aria("label", "Articles")) {
                            ForEach(articles, id: \.id) { summary in
                                articleCard(summary)
                            }
                        }
                    }
                    .style {
                        .maxWidth("720px")
                        .margin("0 auto")
                        .padding("32px")
                        .font("16px -apple-system, BlinkMacSystemFont, sans-serif")
                    }
                }
            }
        }
    }

    private func articleCard(_ summary: ArticleSummary) -> some HTML {
        article(.class("article-card")) {
            h2 {
                a(.href(summary.href)) {
                    summary.title
                }
            }
            p(summary.excerpt)
        }
        .style {
            .padding("16px 0")
            .border("0 solid color-mix(in srgb, CanvasText 16%, transparent)")
            .custom("border-bottom-width", "1px")
        }
    }
}

func renderArticleListPage() -> String {
    ArticleListPage(
        articles: [
            ArticleSummary(
                id: "swift-html",
                title: "Typed HTML in Swift",
                excerpt: "Use lowercase tags, typed attributes, and components to build HTML documents.",
                href: "/articles/swift-html"
            ),
            ArticleSummary(
                id: "hydration",
                title: "Hydration Contracts",
                excerpt: "Render artifacts carry state, event, and browser-neutral runtime metadata.",
                href: "/articles/hydration"
            ),
        ]
    )
    .render()
}

Xcode Preview

import SwiftHTMLPreview

#Preview("Release Dashboard", traits: .fixedLayout(width: 520, height: 360)) {
    HTMLPreview {
        main(.class("dashboard-shell")) {
            header(.class("dashboard-header")) {
                p(.class("eyebrow"), text: "SwiftHTML Preview")
                h1("Release Operations")
                p("Inspect layout, copy, and CSS directly in Xcode.")
            }

            section(.class("metric-grid"), .aria("label", "Release metrics")) {
                article(.class("metric-card")) {
                    p(.class("metric-label"), text: "Tests")
                    strong("108")
                    span(.class("metric-trend"), text: "passing")
                }

                article(.class("metric-card")) {
                    p(.class("metric-label"), text: "Preview")
                    strong("Ready")
                    span(.class("metric-trend"), text: "WebKit")
                }
            }
        }
    }
    .style {
        rule("body") {
            .margin("0")
            .padding("24px")
            .font("16px -apple-system, BlinkMacSystemFont, sans-serif")
            .background("Canvas")
            .color("CanvasText")
        }

        rule(".dashboard-shell") {
            .display("grid")
            .gap("16px")
        }

        rule("h1, p") {
            .margin("0")
        }

        rule(".dashboard-header") {
            .display("grid")
            .gap("8px")
        }

        rule(".eyebrow, .metric-label, .metric-trend") {
            .color("color-mix(in srgb, CanvasText 68%, transparent)")
        }

        rule(".metric-grid") {
            .display("grid")
            .gridTemplateColumns("repeat(2, minmax(0, 1fr))")
            .gap("12px")
        }

        rule(".metric-card") {
            .display("grid")
            .gap("6px")
            .border("1px solid color-mix(in srgb, CanvasText 16%, transparent)")
            .borderRadius("8px")
            .padding("12px")
        }
    }
}

Stateful Runtime Check

import SwiftHTML

struct InlineCounter: ClientComponent, Sendable {
    @State private var count = 0

    var body: some HTML {
        button(.type(ButtonType.button), .onClick {
            count += 1
        }) {
            "Count \(count)"
        }
    }
}

func renderCounterAfterOneClick() throws -> String {
    var runtime = try BrowserHydrationRuntime(
        root: InlineCounter(),
        host: BrowserDOMCommandBuffer(),
        stateStore: StateStore()
    )

    guard let handler = runtime.session.artifact.clientHandlers.handlers.first else {
        return runtime.session.artifact.html
    }

    let update = try runtime.invoke(handlerID: handler.id)
    return update.html
}

Xcode Preview

Use SwiftHTMLPreview when you want to inspect SwiftHTML directly inside Xcode previews. Keep production targets depending on SwiftHTML, and add SwiftHTMLPreview only to preview or development-only targets.

Product Use
SwiftHTML HTML DSL, render artifacts, CSS, state, hydration contracts.
SwiftHTMLPreview HTMLPreview, SwiftUI preview host, WebKit-backed rendering.
flowchart LR
  Source["SwiftHTML component"] --> View["HTMLPreview"]
  Preview["SwiftUI #Preview"] --> View
  View --> Host["HTMLPreviewHost"]
  Host --> WebKit["WKWebView"]
Loading

Basic Preview

Import SwiftHTMLPreview and put the SwiftHTML you want to inspect directly inside HTMLPreview:

import SwiftHTMLPreview

#Preview("Release Dashboard", traits: .fixedLayout(width: 520, height: 360)) {
    HTMLPreview {
        main(.class("dashboard-shell")) {
            header(.class("dashboard-header")) {
                p(.class("eyebrow"), text: "SwiftHTML Preview")
                h1("Release Operations")
                p("Inspect layout, copy, and CSS directly in Xcode.")
            }

            section(.class("metric-grid"), .aria("label", "Release metrics")) {
                article(.class("metric-card")) {
                    p(.class("metric-label"), text: "Tests")
                    strong("108")
                    span(.class("metric-trend"), text: "passing")
                }

                article(.class("metric-card")) {
                    p(.class("metric-label"), text: "Preview")
                    strong("Ready")
                    span(.class("metric-trend"), text: "WebKit")
                }
            }
        }
    }
    .style {
        rule("body") {
            .margin("0")
            .padding("24px")
            .font("16px -apple-system, BlinkMacSystemFont, sans-serif")
            .background("Canvas")
            .color("CanvasText")
        }

        rule(".dashboard-shell") {
            .display("grid")
            .gap("16px")
        }

        rule("h1, p") {
            .margin("0")
        }

        rule(".dashboard-header") {
            .display("grid")
            .gap("8px")
        }

        rule(".eyebrow, .metric-label, .metric-trend") {
            .color("color-mix(in srgb, CanvasText 68%, transparent)")
        }

        rule(".metric-grid") {
            .display("grid")
            .gridTemplateColumns("repeat(2, minmax(0, 1fr))")
            .gap("12px")
        }

        rule(".metric-card") {
            .display("grid")
            .gap("6px")
            .border("1px solid color-mix(in srgb, CanvasText 16%, transparent)")
            .borderRadius("8px")
            .padding("12px")
        }
    }
}

Fixed Size Previews

Use SwiftUI preview traits for fixed layouts:

import SwiftHTMLPreview

#Preview("Mobile", traits: .fixedLayout(width: 390, height: 844)) {
    HTMLPreview {
        main(.class("page")) {
            h1("Mobile Preview")
            p("This document is rendered inside a fixed preview surface.")
        }
    }
}

You can also use regular SwiftUI view modifiers around the preview host:

import SwiftHTMLPreview

#Preview("Fixed Host") {
    HTMLPreview {
        main {
            h1("Fixed Host")
        }
    }
    .frame(width: 390, height: 844)
}

HTML-Specific Modifiers

Use short modifiers on HTMLPreview for document-level settings that SwiftUI Preview does not own.

Modifier Purpose
.language(_:) Sets the document <html lang="..."> value.
.style { ... } Injects a preview-only Stylesheet into the generated document.
.baseURL(_:) Resolves relative URLs inside the WKWebView.
.renderOptions(_:) Controls SwiftHTML render diagnostics and runtime metadata.
import SwiftHTMLPreview

#Preview("Japanese", traits: .fixedLayout(width: 390, height: 240)) {
    HTMLPreview {
        article(.class("card")) {
            h2("SwiftHTML")
            p("Xcode Preview で HTML を確認できます。")
        }
    }
    .style {
        rule("body") {
            .padding("32px")
            .font("16px -apple-system, BlinkMacSystemFont, sans-serif")
        }

        rule(".card") {
            .border("1px solid color-mix(in srgb, CanvasText 16%, transparent)")
            .padding("16px")
        }
    }
    .language("ja")
}

Build Behavior

HTMLPreview is a SwiftUI view. Put it inside #Preview so Xcode's preview discovery and build behavior stay exactly aligned with SwiftUI previews.

SwiftHTMLPreview is intentionally separate from SwiftHTML so the core HTML engine does not depend on SwiftUI, WebKit, or macro implementation details.

SwiftHTML escapes text and attribute values by default:

let rendered = div(.id("root")) {
    "5 > 3 & 2 < 4"
}
.render()

Core Concepts

Concept API Notes
HTML primitive div, span, input, text, rawHTML, Element Lowercase types map to DOM tags.
Component Component A value that returns body.
Server-owned component ServerComponent SSR/default ownership boundary.
Client-owned component ClientComponent Owns @State, event closures, and hydration metadata.
Render result RenderArtifact Public facade for HTML, diagnostics, manifests, handlers, and snapshots.
Runtime state StateStore Component-scoped state slots used during render and hydration.
Runtime state snapshot StateStoreSnapshot Codable state payload guarded by a state schema hash for HMR and WASM runtime swaps.
Runtime schema StateSchema Stable hash derived from state slots, value types, and source locations.

SwiftHTML keeps the raw render graph internal. Public code should use RenderArtifact, HTMLDOMSnapshot, hydration indexes, diagnostics, and patch/runtime records instead of constructing graph nodes.

HTML DSL

HTML tags are lowercase Swift types. Text can be written directly inside builders, or through text initializer shortcuts:

section(.id("intro")) {
    h2("Client Counter")
    p(.class("lead"), text: "State can belong to a ClientComponent.")
    input(
        .type(InputType.email),
        .name("email"),
        .placeholder("hello@example.com"),
        .required
    )
}

Attributes are typed where it matters and still allow escape hatches:

a(
    .href("/account"),
    .data("tracking-id", "account-link"),
    .aria("label", "Open account")
) {
    "Account"
}

Element("custom-element", attributes: [
    .attribute("part", "label")
]) {
    "Custom element content"
}

Builder control flow works with if, switch, for, and ForEach:

struct Menu: Component {
    let items: [String]
    let isSignedIn: Bool

    var body: some HTML {
        nav {
            ul {
                ForEach(items, id: \.self) { item in
                    li {
                        a(.href("/\(item)")) {
                            item
                        }
                    }
                }
            }

            if isSignedIn {
                button(.type(ButtonType.button)) {
                    "Sign out"
                }
            }
        }
    }
}

Rendering

Use render() when only the HTML string is needed:

let html = HomePage().render()

Use renderArtifact() when a server or runtime needs diagnostics, hydration metadata, event handlers, or a DOM snapshot:

let artifact = HomePage().renderArtifact()

print(artifact.html)
print(artifact.diagnostics)
print(artifact.hydration.components)
print(artifact.browserHydrationIndex())

HTMLRenderOptions controls diagnostic capture, handler closure capture, browser hydration markers, and component environment overrides.

CSS

Inline styles use Style and @StyleBuilder:

div {
    "Panel"
}
.style {
    .display("grid")
    .gridTemplateColumns("1fr auto")
    .gap("12px")
    .whiteSpace("nowrap")
    .custom("--panel-tone", "muted")
}

Stylesheets use Stylesheet, CSSRule, and @StylesheetBuilder:

let stylesheet = Stylesheet {
    rule(".panel") {
        .minHeight("36px")
        .background("var(--panel-background)")
        .borderRadius("8px")
    }

    rule(".panel[data-active=\"true\"]") {
        .outline("2px solid var(--accent)")
    }
}

print(stylesheet.cssText)

The generated CSS property surface is based on @mdn/browser-compat-data. Standard-track, non-deprecated, non-vendor properties are exposed as Style helpers so editors can autocomplete the CSS property surface.

Style.custom(_:_:), dynamic CSS members, CSSSelector, CSSRule, and raw style attributes serialize values as authored. Do not pass untrusted external input directly into those APIs.

State And Hydration

ClientComponent can own @State and event closures:

struct Counter: ClientComponent, Sendable {
    @State private var count = 0

    var body: some HTML {
        button(.type(ButtonType.button), .onClick {
            count += 1
        }) {
            "Count \(count)"
        }
    }
}

Rendering records state slots and event bindings in the artifact:

let store = StateStore()
let artifact = Counter().renderArtifact(stateStore: store)

let component = artifact.hydration.components.first
let handler = artifact.clientHandlers.handlers.first

State snapshots are explicit runtime data. A host can preserve client state during HMR or a component WASM swap only when the rendered state schema matches:

let schemaHash = artifact.hydration.stateSchemaHash
let snapshot = try store.snapshot(schemaHash: schemaHash)

let nextStore = StateStore()
nextStore.restore(snapshot)

Only values that can be encoded by the runtime are included in the snapshot. Non-encodable state falls back to the component initializer on restore.

The in-package hydration runtime can be used by tests or host adapters:

let host = BrowserDOMCommandBuffer()
var runtime = try BrowserHydrationRuntime(
    root: Counter(),
    host: host,
    stateStore: StateStore()
)

let handlerID = runtime.session.artifact.clientHandlers.handlers[0].id
let update = try runtime.invoke(handlerID: handlerID)

print(update.commands)

HydrationRuntimeSession.flush() tracks dirty components, then currently performs a whole-root re-render and graph diff. Scoped subtree diffing is a runtime optimization boundary, not part of the current correctness contract.

Environment

Environment values can be defined with EnvironmentKey:

struct LocaleKey: EnvironmentKey {
    static let defaultValue = "en"
}

extension EnvironmentValues {
    var locale: String {
        get { self[LocaleKey.self] }
        set { self[LocaleKey.self] = newValue }
    }
}

struct LocaleLabel: Component {
    @Environment(\.locale) private var locale

    var body: some HTML {
        span {
            locale
        }
    }
}

Type-based environment reads are optional:

struct LibraryReader: Component {
    @Environment(Library.self) private var library: Library?

    var body: some HTML {
        if let library {
            span {
                library.title
            }
        } else {
            span {
                "Library unavailable"
            }
        }
    }
}

Actions

SwiftHTML defines transport-neutral action contracts. It can render an action target and hidden fields, but it does not dispatch HTTP requests or invoke server actors.

struct SaveAction: ActionRepresentable {
    let path = "/actions/save"
    let method = FormMethod.post
    let fields = [
        ActionField("scope", "profile")
    ]
}

Higher-level packages can map ActionRepresentable to forms, buttons, fetch requests, server action gateways, or actor invocation.

Embedded WASM

SwiftHTMLEmbedded is a separate product for production client code that must compile with Embedded Swift. It intentionally does not include the full SwiftHTML renderer, graph, Codable manifests, reflection-based component identity, or task-local render context.

flowchart LR
  A["SwiftHTMLEmbedded static tree"] --> B["EmbeddedDOMHost"]
  B --> C["Runtime adapter"]
  C --> D["Browser DOM"]
Loading

The package includes Examples/EmbeddedWasm, which connects SwiftHTMLEmbedded to JavaScriptKit in an example package. JavaScriptKit remains outside the core SwiftHTML target.

Measured with Swift 6.3.1 and no wasm-opt:

Encoding Standard WASM Embedded WASM Reduction
raw 9,027,908 bytes 859,116 bytes 90.5%
gzip -9 2,423,702 bytes 283,656 bytes 88.3%
brotli -q 11 1,741,470 bytes 230,400 bytes 86.8%

Run the measurement locally:

cd Examples/EmbeddedWasm
export SWIFT_BIN="/Users/1amageek/Library/Developer/Toolchains/swift-6.3.1-RELEASE.xctoolchain/usr/bin/swift"
./measure-size.sh
npm install
npm run test:browser

What SwiftHTML Does Not Own

Concern Expected owner
HTTP routing Server framework package
Request and response objects Server framework package
CSRF, CORS, Origin, Redirect policy Server framework package
Server action dispatch Server framework package
Distributed actor registry Server framework package
Design-system components UI package
JavaScriptKit DOM adapter Runtime package
WASM bootstrap script Runtime package

Safety Notes

Surface Behavior
Text nodes Escaped by default.
Attribute values Escaped and validated by attribute kind.
URL attributes Reject unsafe JavaScript URLs in typed URL attributes.
rawHTML Emits authored HTML; use only with trusted content.
CSS selectors and values Serialized as authored; validate untrusted input before passing it to CSS APIs.
Event closures Captured only in render artifacts for client-owned components and runtime adapters.

Development

swift build
xcodebuild test -scheme swift-html-Package -destination 'platform=macOS' -only-testing:SwiftHTMLTests
node scripts/generate-swift-html-css-properties.mjs --check

Refresh generated CSS helpers:

node scripts/generate-swift-html-css-properties.mjs

Documentation

The DocC catalog lives in Sources/SwiftHTML/SwiftHTML.docc. Build it with:

xcodebuild docbuild -scheme swift-html-Package -destination 'generic/platform=macOS'

The longer design notes live in docs/SwiftHTML.md.

License

SwiftHTML is available under the MIT license.

Description

  • Swift Tools 6.3.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Jun 28 2026 03:53:58 GMT-0900 (Hawaii-Aleutian Daylight Time)