Skip to content
 
 

Repository files navigation

TailwindKit Logo

TailwindKit

Swift Versions Platforms Documentation License GitHub Workflow Status Maintainability Codecov CodeFactor Grade

Type-safe Tailwind CSS v4 utility classes for Swift.


What is TailwindKit?

Server-side Swift HTML markup is full of stringly-typed CSS: class="flex items-center gap-4" is just text, so a typo is a silently broken layout that no compiler and no test will catch.

TailwindKit is a small, immutable value builder that emits Tailwind CSS v4 utility class strings from checked Swift expressions. Every utility is a property or a method, every value is a typed token, and the class string only appears at the end, via .rendered.

import TailwindKit

TW.flex.items(.center).gap(4).bg(.blue, .s500).rendered
// "flex items-center gap-4 bg-blue-500"

brightdigit.com uses TailwindKit to author its component markup.

Scope: TailwindKit is Tailwind v4 only, and its modeled utility surface is deliberately closed — a set of Swift enums and methods that grows as consuming components need new classes, not a mirror of every class Tailwind can emit. The public API accepts no free-form class name; the only caller-supplied strings go through the explicit arbitrary-value API below. For a class that isn't modeled at all, the escape hatch is your HTML library's own class API.

Installation

Add TailwindKit to your Package.swift:

dependencies: [
  .package(url: "https://github.com/brightdigit/TailwindKit.git", from: "1.0.0")
]

Then add it to a target:

.target(
  name: "MySite",
  dependencies: [.product(name: "TailwindKit", package: "TailwindKit")]
)

Usage

The builder

The core type is TailwindStyleBuilder — an immutable value builder where every member returns a new builder. Bare utilities are computed properties; parameterized utilities are methods. TW is a type alias for the builder, so a chain can start with the type name or with a leading dot.

TW.flex.flexDirection(.col).justify(.between).items(.stretch).rendered
// "flex flex-col justify-between items-stretch"

The fluent surface is organized into one public capability protocol per CSS concern — ColorStyling, SpacingStyling, SizingStyling, PositioningStyling, DisplayStyling, FlexGridStyling, TypographyStyling, BorderStyling, EffectsStyling, TransitionStyling, ListStyling, VariantStyling and ArbitraryStyling — all composed through the seam protocol TailwindStyle.

With an HTML library

TailwindKit has no dependencies — not even Foundation. TailwindStyleBuilder.rendered is just a String, so it drops into any HTML library:

Node.div(.class(TW.flex.items(.center).gap(4).rendered), .text("Hi"))

For nicer call sites, conform that library's attribute type to TailwindClassAttribute. Its single requirement is class(_:) — the factory HTML libraries already declare — so the conformance is a declaration with nothing to implement, and the .tailwind(_:) sugar comes with it:

import Plot
import TailwindKit

extension Node: TailwindClassAttribute where Context: HTMLContext {}
extension Attribute: TailwindClassAttribute where Context: HTMLContext {}

Node.div(.tailwind(.flex.items(.center).gap(4)), .text("Hi"))
// <div class="flex items-center gap-4">Hi</div>

One conditional conformance covers every context, and leading-dot inference keeps working through the protocol extension, so .tailwind(…) reads exactly like a native factory.

Types whose class assignment is an instance modifier rather than a static factory — Plot's Component, say — can't use the protocol, because Swift does not allow retroactively conforming one protocol to another. Write the one-liner directly instead:

extension Component {
  public func tailwind(
    _ style: TailwindStyleBuilder,
    replaceExisting: Bool = false
  ) -> Component {
    self.class(style.rendered, replaceExisting: replaceExisting)
  }
}

Image("logo.png").tailwind(.rounded(.lg))
// <img src="logo.png" class="rounded-lg"/>

Responsive and state variants

A variant takes a nested style and prefixes every token it produced. Prefixes stack by nesting.

TW.block.lg(.hidden).rendered              // "block lg:hidden"
TW.md(.hover(.bg(.blue, .s700))).rendered  // "md:hover:bg-blue-700"

Custom token values

The token families Tailwind v4 backs with an extensible @theme namespace — Color, Spacing, Size, MaxWidth, TextSize, FontWeight, Radius, Shadow, DropShadow, Tracking and Ease — are protocols. Conform your own type to register a value the built-ins don't cover:

struct BrandColor: Color { let token = "brand" }

TW.bg(BrandColor(), .s500).rendered   // "bg-brand-500"

The families that map to fixed CSS keywords (Shade, Position, Flex, FlexDirection, ListStyle, Align, Justify, TextAlign, VerticalAlign, ObjectFit, BorderSide) stay closed enums, because a custom value there would be meaningless.

Arbitrary values

For the occasional arbitrary value Tailwind v4 supports, the utility prefix is supplied by you — only the value is arbitrary:

TW.arbitrary("top", value: "117px").rendered         // "top-[117px]"
TW.arbitrary("bg", variable: "--brand").rendered     // "bg-(--brand)"
TW.arbitrary("grid-cols", value: "1fr 500px").rendered
// "grid-cols-[1fr_500px]"  (spaces become underscores)
TW.custom(property: "mask-type", value: "luminance").rendered
// "[mask-type:luminance]"

Each extensible token family also carries an .arbitrary(_:) static, e.g. TW.maxW(.arbitrary("48rem"))max-w-[48rem].

Testing

swift test

Tests/TailwindKitTests uses swift-testing (@Suite/@Test/#expect). The tests are offline and depend on no HTML library: they assert .rendered string equality only, e.g. TW.flex.gap(4).rendered == "flex gap-4". The TailwindClassAttribute seam is covered by conforming a local stand-in type.

Requirements

  • Swift 6.4+
  • macOS 13+, or Ubuntu 24.04 (Noble)

License

MIT © BrightDigit

About

A swift package to use Tailwind CSS inside Swift

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages