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"]
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, and the #Preview macro / HTMLPreview Xcode preview surface. |
SwiftHTMLClientRuntime |
Static client HTML tree and DOM host contract that can compile under standard WASM and Embedded Swift compiler profiles. |
SwiftHTMLPreview |
Compatibility re-export of SwiftHTML (the preview surface now lives in SwiftHTML). |
| 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. |
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 |
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"),
]
),
]
)#Preview ships with SwiftHTML, so any target that depends on SwiftHTML
can write previews — no separate preview product is required. SwiftHTMLPreview
remains as a compatibility re-export for existing import SwiftHTMLPreview code.
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)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.
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()
}import SwiftHTML and mark the HTML you want to inspect with #Preview. It renders the content in a WKWebView inside Xcode's canvas — a single import, no #if DEBUG guard, no SwiftUI, and nothing links into a release or WebAssembly build:
import SwiftHTML
#Preview {
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")
}
}
}
}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
}import SwiftHTML and use #Preview to inspect SwiftHTML directly inside Xcode's canvas. There is no SwiftUI dependency, and the whole surface is self-gated behind #if DEBUG && canImport(WebKit), so it never links WebKit or DeveloperToolsSupport into a release server or a WebAssembly build.
| Product | Use |
|---|---|
SwiftHTML |
HTML DSL, render artifacts, CSS, state, hydration contracts, and the #Preview macro. |
SwiftHTMLPreview |
Compatibility re-export of SwiftHTML. New code can import SwiftHTML directly. |
flowchart LR
Source["SwiftHTML content"] --> Macro["#Preview"]
Macro --> Renderer["HTMLPreviewRenderer"]
Renderer --> Document["Preview HTML document"]
Document --> WebKit["WKWebView"]
WebKit --> Canvas["Xcode canvas (PreviewRegistry)"]
A single import SwiftHTML provides both the content DSL and the #Preview macro — Xcode's canvas discovers it by name, exactly like SwiftUI's. No import DeveloperToolsSupport, no #if DEBUG, and no platform guard is needed:
import SwiftHTML
#Preview {
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")
}
}
}
}Pass a display name as the first argument, exactly like SwiftUI's #Preview. The
preview renders with a default stylesheet; apply your own CSS with SwiftHTML's
element-level .style { ... }:
import SwiftHTML
#Preview("Japanese") {
article {
h2("SwiftHTML")
p("Xcode Preview で HTML を確認できます。")
}
.style {
.padding("32px")
.font("16px -apple-system, BlinkMacSystemFont, sans-serif")
.border("1px solid color-mix(in srgb, CanvasText 16%, transparent)")
}
}For rendering SwiftHTML to a WKWebView outside a preview (custom stylesheet,
language, base URL, render options), use the HTMLPreview(...) function directly.
#Preview here is SwiftHTML's own macro. Xcode's canvas discovers previews by the
macro name Preview, so it appears in the canvas exactly like SwiftUI's #Preview.
It expands to a DeveloperToolsSupport.PreviewRegistry conformance — the same type
Apple's #Preview generates — but wrapping a WKWebView instead of a SwiftUI view.
The expansion is self-gated behind #if DEBUG && canImport(WebKit), so it is absent
from release and WebAssembly builds without any guard in your own code, and the
macro plugin is a host-only build tool not linked on non-Apple platforms.
Xcode's canvas hosts the preview off the normal on-screen window, so WebKit treats
the WKWebView as non-visible and purges its rendered content after a few seconds —
the preview renders, then goes blank (the WebContent process stays alive, so this is
not a crash). This affects any WebKit- or MapKit-backed preview, including Apple's
own #Preview { WKWebView() } and #Preview { MKMapView() }; it is not specific to
SwiftHTML, and there is no public API to prevent WebKit's non-visible purge. Use the
preview for a quick look at layout and CSS; for sustained or interactive viewing,
run on a device or Simulator, or serve the HTML in a browser.
SwiftHTML escapes text and attribute values by default:
let rendered = div(.id("root")) {
"5 > 3 & 2 < 4"
}
.render()| 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 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"
}
}
}
}
}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.
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.
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.firstState 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 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"
}
}
}
}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.
SwiftHTMLClientRuntime is the production client runtime surface. It is named by
responsibility, not by compiler profile: the same source must compile under the
standard WASM SDK and the Embedded Swift WASM SDK. It intentionally does not
include the full SwiftHTML renderer, graph, Codable manifests,
reflection-based component identity, or task-local render context.
flowchart LR
A["SwiftHTMLClientRuntime static tree"] --> B["ClientDOMHost"]
B --> C["Runtime adapter"]
C --> D["Browser DOM"]
The package includes Examples/EmbeddedWasm, which verifies the compiler
profile boundary by connecting SwiftHTMLClientRuntime 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| 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 |
| 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. |
swift build
xcodebuild test -scheme swift-html-Package -destination 'platform=macOS' -only-testing:SwiftHTMLTests
node scripts/generate-swift-html-css-properties.mjs --checkRefresh generated CSS helpers:
node scripts/generate-swift-html-css-properties.mjsThe 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.
SwiftHTML is available under the MIT license.