Skip to content

Latest commit

Β 

History

134 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ElementaryTailwind: Type-safe Tailwind CSS in Swift

Type-safe Tailwind CSS utilities for Elementary β€” write Tailwind classes as typed Swift methods, not raw strings.

Compatibility

ElementaryTailwind Elementary TailwindCSS
0.3.xxx 0.8.0 4.3.3
0.2.xxx 0.8.0 4.3.3

Caution

DO NOT USE 0.1.xxx TAGS There was some mismatches in that versions

import Elementary
import ElementaryTailwind

struct ProductPage: HTMLDocument {
    var title: String { "Featured product" }

    var body: some HTML {
        main(
            .maxWidth(.xxl),
            .marginX(.auto),
            .padding(.size(8))
        ) {
            div(
                .display(.flex), .flexDirection(.column), .gap(.size(4)),
                .backgroundColor(.white), .borderWidth(.size(1)),
                .borderColor(.gray.shade(200)), .borderRadius(.lg), .p(8)
            ) {
                h1(.fontSize(.xxxl), .fontWeight(.bold), .textColor(.gray.shade(900))) {
                    "Featured product"
                }
                p(.fontSize(.base), .textColor(.gray.shade(500))) {
                    "A short description of the product."
                }
                button(
                    .backgroundColor(.blue), .textColor(.white),
                    .padding(.x(4), .y(2)), .borderRadius(.md),
                    .fontWeight(.medium), .fontSize(.sm)
                ) {
                    "Add to cart"
                }
            }
        }
    }
}

Generated HTML:

<main class="max-w-2xl mx-auto p-8">
  <div class="flex flex-col gap-4 bg-white border border-gray-200 rounded-lg p-8">
    <h1 class="text-3xl font-bold text-gray-900">Featured product</h1>
    <p class="text-base text-gray-500">A short description of the product.</p>
    <button class="bg-blue-500 text-white px-4 py-2 rounded-md font-medium text-sm">Add to cart</button>
  </div>
</main>

Use it

Add elementary-tailwind to your Package.swift dependencies:

// swift-tools-version: 6.1
import PackageDescription

let package = Package(
    name: "MyApp",
    dependencies: [
        .package(url: "https://github.com/amirsaam/elementary-tailwind.git", from: "0.1.100"),
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "ElementaryTailwind", package: "elementary-tailwind"),
            ]
        ),
    ]
)

ElementaryTailwind depends on Elementary. Swift Package Manager resolves this transitively β€” no need to declare it as a direct dependency.

This package requires Swift 6.1 with StrictConcurrency=complete and targets macOS v14, iOS v15, tvOS v17, watchOS v10.

Quick tour

import Elementary
import ElementaryTailwind

var head: some HTML {
    meta(.charset(.utf8))
    setupTailwind()  // emits <script src="https://cdn.tailwindcss.com/4.3.3" defer>
}
// every Tailwind utility is a typed static method on MarkupAttribute
div(.display(.flex), .items(.center), .gap(.size(4)), .p(8)) {
    p(.textColor(.blue), .fontSize(.lg)) { "Hello" }
}
// layout β€” display, position, inset, z-index, order
div(.display(.flex), .position(.absolute)) { ... }
div(.inset(.size(4), negative: true)) { ... }  // -> -inset-4
div(.zIndex(.number(10), negative: true)) { ... }  // -> -z-10
div(.order(.number(3), negative: true)) { ... }  // -> -order-3

// variants β€” hover, focus, responsive, dark mode, container queries
button(.backgroundColor(.blue, variants: [.hover])) { "Hover me" }
div(.display(.grid, variants: [.md, .lg])) { ... }
div(.backgroundColor(.gray.shade(900), variants: [.dark])) { ... }
div(.display(.flex, variants: [.namedContainerQuery("sidebar")])) { ... }
// colors β€” full Tailwind color palette with shade and opacity support
p(.textColor(.red)) { "Red" }
p(.textColor(.red.shade(500))) { "Red 500" }
p(.textColor(.blue, opacity: 70)) { "Blue 70%" }
div(.backgroundColor(.gray.shade(900), variants: [.dark])) { ... }
// spacing β€” fractional values, directional, arbitrary
div(.margin(.top(4)), .padding(.x(1.5))) { ... }
div(.margin(.left(.arbitrary("20px")))) { ... }

// negative values β€” prepends `-` to the class
div(.margin(.size(4), negative: true)) { ... }  // -> -m-4
div(.marginX(.size(4), negative: true)) { ... }  // -> -mx-4
// gradients β€” direction + color stops with optional opacity
div(.gradientToDirection(.br), .gradientFromColor(.blue), .gradientToColor(.purple)) { ... }  // bg-linear-to-br
div(.gradientToDirection(.r), .gradientFromColor(.red, opacity: 50)) { ... }                  // bg-linear-to-r
div(.gradientToDirection(.arbitrary("65deg"))) { ... }                                        // bg-linear-[65deg]
// filters
div(.blur(.md), .brightness(125), .grayscale(50)) { ... }
div(.backdropBlur(.lg), .backdropBrightness(75)) { ... }
// transforms β€” scale, rotate, translate, skew, perspective, 3D
div(.scale(.all(110)), .rotate(.z(45))) { ... }
div(.transform(.gpu), .perspective(.value(500)), .rotate(.x(15))) { ... }

// negative values β€” prepends `-` to the class
div(.scale(.all(50), negative: true)) { ... }      // -> -scale-50
div(.translate(.x("4"), negative: true)) { ... }    // -> -translate-x-4
// interactivity β€” cursor, scroll snap, scroll margin/padding
div(.cursor(.pointer), .scrollSnapAlign(.start)) { ... }

// negative values for scroll-margin/padding β€” prepends `-` to the class
div(.scrollMargin(.value(4), negative: true)) { ... }    // -> -scroll-m-4
div(.scrollPadding(.value(4), negative: true)) { ... }   // -> -scroll-p-4
// SVG fill β€” color, none, or keyword (currentColor, inherit, transparent)
svg(.fill(.blue)) { ... }             // fill-blue-500
svg(.fillNone()) { ... }              // fill-none
svg(.fillCurrent()) { ... }           // fill-current
svg(.fillInherit()) { ... }           // fill-inherit
svg(.fillTransparent()) { ... }       // fill-transparent
// border-radius β€” uniform or directional
div(.borderRadius(.lg)) { ... }
div(.borderRadius(.topLeft(.lg), .topRight(.lg))) { ... }
// arbitrary values β€” typed .arbitrary(String) on ~70 token types, or raw .class()
div(.margin(.left(.arbitrary("20px")))) { ... }                       // ml-[20px]
div(.gridTemplateColumns(.arbitrary("200px_minmax(900px,1fr)_100px"))) { ... }
div(.scale(.arbitrary("1.7"))) { ... }                                // scale-[1.7]
div(.backgroundColor(.arbitrary("#0f172a"))) { ... }                  // bg-[#0f172a]
div(.class("bg-(--my-color)")) { ... }
// mix typed and raw β€” .class() with variant support
div(.display(.flex), .class("custom-class")) { ... }
div(.class("shadow-outline", variants: [.focus])) { ... }
// string extraction β€” capture modifier output outside an HTML builder
let classes = twValue(
    .translate(.y("10"), negative: true),
    .margin(.size(4)),
    .text(.lg, variants: [.sm])
)
// β†’ "-translate-y-10 m-4 sm:text-lg"

Utilities

All 220+ token types across 16 Tailwind CSS categories:

Category Methods Examples
Layout .display, .position, .inset, .insetTop, .insetRight, .insetBottom, .insetLeft, .insetX, .insetY, .zIndex, .overflow, .overflowX, .overflowY, .overscrollBehavior, .overscrollBehaviorX, .overscrollBehaviorY, .visibility, .float, .clear, .isolation, .columns, .breakAfter, .breakBefore, .breakInside, .boxSizing, .boxDecorationBreak, .objectFit, .objectPosition, .aspect .display(.flex), .position(.absolute), .zIndex(.number(10)), .inset(.fraction("1/2"))
Flexbox & Grid .flexDirection, .flexWrap, .flex, .flexGrow, .flexShrink, .flexBasis, .items, .justify, .placeContent, .placeItems, .placeSelf, .alignContent, .alignSelf, .justifyItems, .justifySelf, .order, .gap, .gapX, .gapY, .gridTemplate*, .gridColumn, .gridRow, .gridAuto* .flexDirection(.column), .items(.center), .gap(.size(4))
Spacing .padding, .paddingX, .paddingY, .paddingTop, .paddingRight, .paddingBottom, .paddingLeft, .margin, .marginX, .marginY, .marginTop, .marginRight, .marginBottom, .marginLeft, .gap, .spaceX, .spaceY .p(8), .padding(.x(4), .y(2)), .mt(4), .mx(.auto)
Sizing .width, .minWidth, .maxWidth, .height, .minHeight, .maxHeight, .size, .aspect .width(.full), .height(.screen), .size(.size(4))
Typography .fontFamily, .fontSize, .fontWeight, .fontStyle, .fontSmoothing, .fontStretch, .fontVariantNumeric, .fontFeatureSettings, .letterSpacing, .lineClamp, .lineHeight, .textAlign, .textColor, .textDecoration, .textDecorationColor, .textDecorationStyle, .textDecorationThickness, .underlineOffset, .textTransform, .textOverflow, .textWrap, .textIndent, .verticalAlign, .whitespace, .wordBreak, .overflowWrap, .hyphens, .tabSize, .listStyle, .listStylePosition, .listStyleImage, .content .fontSize(.lg), .textColor(.blue), .fontWeight(.bold), .overflowWrap(.breakWord)
Backgrounds .backgroundColor, .backgroundAttachment, .backgroundClip, .backgroundImage, .backgroundOrigin, .backgroundPosition, .backgroundRepeat, .backgroundSize, .backgroundBlendMode .backgroundColor(.blue), .backgroundSize(.cover)
Gradients .gradientToDirection, .gradientFromColor, .gradientViaColor, .gradientToColor .gradientFromColor(.blue, opacity: 50)
Borders .borderWidth, .borderColor, .borderStyle, .borderRadius, .outlineWidth, .outlineColor, .outlineStyle, .outlineOffset, .ringWidth, .ringColor, .ringOffsetWidth, .ringOffsetColor, .boxShadow, .boxShadowColor, .divideX, .divideY, .divideColor, .divideStyle .borderRadius(.lg), .borderRadius(.topLeft(.md)), .borderColor(.t, .gray.shade(200)), .divideY(.size(2))
Effects .opacity, .textShadow, .mixBlendMode, .backgroundBlendMode, .boxShadow, .boxShadowColor, .insetShadow .opacity(50), .textShadow(.lg), .insetShadow(.sm)
Masks .maskClip, .maskComposite, .maskImage, .maskMode, .maskOrigin, .maskPosition, .maskRepeat, .maskSize, .maskType .maskClip(.border), .maskSize(.cover)
Filters .blur, .brightness, .contrast, .dropShadow, .grayscale, .hueRotate, .invert, .saturate, .sepia, .backdropBlur, .backdropBrightness, .backdropContrast, .backdropGrayscale, .backdropHueRotate, .backdropInvert, .backdropOpacity, .backdropSaturate, .backdropSepia .blur(.md), .backdropBrightness(75)
Tables .borderCollapse, .borderSpacing, .tableLayout, .captionSide .borderCollapse(.collapse)
Transitions .transition, .transitionBehavior, .transitionDuration, .transitionTimingFunction, .transitionDelay .transition(.colors), .transitionDuration(.ms(150))
Animation .animation .animation(.spin), .animation(.pulse)
Transforms .transform, .scale, .rotate, .translate, .skew, .transformOrigin, .perspective, .perspectiveOrigin, .backfaceVisibility, .transformStyle, .zoom .transform(.gpu), .rotate(.z(45))
Interactivity .cursor, .pointerEvents, .resize, .userSelect, .scrollBehavior, .scrollSnap*, .scrollMargin, .scrollPadding, .scrollbarWidth, .scrollbarColor, .scrollbarGutter, .touchAction, .accentColor, .appearance, .caretColor, .colorScheme, .fieldSizing, .willChange .cursor(.pointer), .scrollSnapAlign(.start)
SVG .fill, .fillNone, .fillCurrent, .fillInherit, .fillTransparent, .stroke, .strokeNone, .strokeWidth .fill(.blue), .fillCurrent(), .strokeWidth(.value(2))
Accessibility .screenReader, .forcedColorAdjust .screenReader(.only)

Variants

Every utility method accepts an optional variants: parameter:

// pseudo-classes
div(.backgroundColor(.blue, variants: [.hover])) { ... }
div(.ringWidth(.size(2), variants: [.focus])) { ... }

// responsive
div(.display(.flex, variants: [.md])) { ... }
div(.display(.grid, variants: [.lg])) { ... }

// dark mode
div(.backgroundColor(.gray.shade(900), variants: [.dark])) { ... }

// container queries
div(.display(.grid, variants: [.containerQuery])) { ... }
div(.display(.grid, variants: [.namedContainerQuery("sidebar")])) { ... }

// combined - example generates `md:hover:flex` <- order of variants does matter
div(.display(.flex, variants: [.hover, .md])) { ... }

Available variants:

Category Variants
Pseudo-classes .hover, .focus, .focusWithin, .focusVisible, .active, .visited, .disabled, .invalid, .valid, .readOnly, .checked, .indeterminate, .required, .empty
Pseudo-elements .first, .last, .odd, .even, .placeholder, .before, .after, .file, .marker, .selection
Responsive .sm, .md, .lg, .xl, .xxl
Max-width responsive .maxSm, .maxMd, .maxLg, .maxXl, .maxXxl
Media .dark, .print, .containerQuery, .namedContainerQuery(String)
Group .groupHover, .groupFocus, .groupChecked, .groupDisabled, .groupInvalid, .groupValid, .groupOpen, .groupAutofill, .groupRequired, .groupVisited, .groupPlaceholder, .groupTarget
Peer .peerHover, .peerFocus, .peerChecked, .peerInvalid, .peerValid, .peerOpen, .peerAutofill, .peerRequired, .peerVisited, .peerPlaceholder, .peerTarget
Markers .group(.bare), .group(.named("item")), .peer(.bare), .peer(.named("email"))
Custom .arbitrary(String)

Mark group/peer elements with .group()/.peer() so group-*/peer-* variants on children or siblings can target them:

div(.group(.bare)) { ... }                                  // class="group"
li(.group(.named("item"))) { ... }                          // class="group/item"
span(.opacity(.value(100), variants: [.groupHover])) { ... }

Setup

The setupTailwind() helper generates the <script> tag needed to install Tailwind CSS from a CDN:

var head: some HTML {
    meta(.charset(.utf8))
    setupTailwind()            // defaults to v4.3.3
    setupTailwind(version: "4.3.3")  // pin a specific version
}

Generated HTML:

<script src="https://cdn.tailwindcss.com/4.3.3" defer></script>

If you need to host Tailwind CSS yourself or use a different CDN, write the <script> tag directly:

var head: some HTML {
    meta(.charset(.utf8))
    script(.src("/tailwind.min.js"), .defer) {}
}

Custom values

Note

Arbitrary values are supported via typed .arbitrary(String) on ~70 token types across every utility category β€” spacing, sizing, colors, grids, filters, transforms, transitions, typography, and more. The token wraps the value in Tailwind's bracket syntax (scale-[1.7], bg-[#0f172a], grid-cols-[200px_minmax(900px,1fr)_100px]). CSS variable syntax ((<property>)) and uncommon utility combinations fall back to raw .class().

Most value-based utilities accept typed arbitrary values:

// typed arbitrary values β€” every utility that documents UsingACustomValue
div(.scale(.arbitrary("1.7"))) { ... }                                      // scale-[1.7]
div(.gridTemplateColumns(.arbitrary("200px_minmax(900px,1fr)_100px"))) { ... }
div(.backgroundColor(.arbitrary("#0f172a"))) { ... }                        // bg-[#0f172a]
div(.animation(.arbitrary("wiggle_1s_ease-in-out_infinite"))) { ... }
div(.textShadow(.arbitrary("0_35px_35px_rgb(0_0_0_/_0.25)"))) { ... }

// colors β€” TWColor.arbitrary works for all 9 color utilities
div(.textColor(.arbitrary("#f00"))) { ... }                                // text-[#f00]
div(.borderColor(.arbitrary("var(--brand)"))) { ... }                      // border-[var(--brand)]

For utilities not covered by typed tokens, use the raw .class() modifier (followings are just examples):

// arbitrary value
div(.class("grid-cols-[1fr_2fr_1fr]")) { ... }

// CSS variable
div(.class("bg-(--my-color)")) { ... }

// mix typed and raw
div(.display(.flex), .class("custom-class")) { ... }

Documentation

The full API is documented in source β€” every public type and function has doc comments. For architecture details, see AGENTS.md.

The full test suite (237 snapshot tests across 17 suites) lives in Tests/ElementaryTailwindTests/.

Future directions

  • All Tailwind CSS v4 utility categories are implemented (220+ token types, 100% docs coverage).

If you think something is missing, feel free to open an issue but PRs are always welcomed.

License

MIT

About

TailwindCSS + Elementary: Type-safe rapid UI development in Swift

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages