Building stylesheets using Leaf

CSS preprocessors are extremely popular, but in Vapor 4 we can also use the Leaf template engine to render dynamic stylesheets.


A quick CSS demo project

The very first step is to add Leaf as a dependency to your project. You should note that Leaf 4 is not released yet, that's why you need the RC flag after the version string.

// swift-tools-version:5.2
import PackageDescription

let package = Package(
    name: "myProject",
    platforms: [
    dependencies: [
        .package(url: "", from: "4.10.0"),
        .package(url: "", from: "4.0.0-rc"),
    targets: [
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "Leaf", package: "leaf"),
            swiftSettings: [
                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
        .target(name: "Run", dependencies: [.target(name: "App")]),
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "XCTVapor", package: "vapor"),

Now you are ready to import Leaf in your Swift files, but before we do so and register our routes, we have to create a new Resources/Views folder in the project root. This is where we are going to store our template files. Leaf files by default should use the .leaf file extension. You can override this behavior, but for the sake of simplicity we're going to leave it that way and we are going to create our index.leaf template inside the Views directory right away.

<!DOCTYPE html>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello world</title>
    <link rel="stylesheet" href="/style.css">

        <h1>Hello world!</h1>

Pretty basic HTML5 boilerplate code. In the head section we import our style.css stylesheet file, inside the body we print out a simple "Hello world!" text using a h1 tag. Now we should create a brand new style.leaf file next to the other template and place the following code inside of it:

* {
    margin: 0;
    padding: 0;
body {
    font-family: -apple-system, system-ui, BlinkMacSystemFont, "Helvetica", "Segoe UI", Roboto, Ubuntu;
    font-size: 16px;
    line-height: 1.4em;
    background: #(background);
h1 {
    padding: #(padding);
@media (max-width: 599px) {}
@media (min-width: 600px) {}
@media (min-width: 900px) {}
@media (min-width: 1200px) {}
@media (min-width: 1800px) {}

Since this is a leaf template file we can use the #(variable) syntax to print out values. We are going to pass the background color value and the padding as a string variable.

We are going to render the index template view and return it as a response to any incoming HTTP GET request that asks for the root path. Returning the style template is a little bit tricky, we have to pass around the parameters as the second argument to the render function, plus we have to encode the response and add the proper header values.

import Vapor
import Leaf

public func configure(_ app: Application) throws {

    app.leaf.cache.isEnabled = false

    app.get { $0.view.render("index") }

    app.get("style.css") {
        $0.view.render("style", [
            "background": "red",
            "padding": "16px"
        .encodeResponse(status: .ok, headers: ["Content-Type": "text/css"], for: $0)

We have to explicitly set the Content-Type header, so when the browser asks for this resource it'll know that it has a text/css type and the returned content of the CSS document can be used to render the HTML document with some additional style info.

You can use the same trick to return any kind of media type. It works with JavaScript files, but if we go one step further we could also generate and return dynamic images on the fly using Vapor 4.

Share this article on Twitter.
Thank you. 🙏

Picture of Tibor Bödecs

Tibor Bödecs

Creator of (weekly Swift articles), server side Swift enthusiast, full-time dad. -- Follow me & feel free to say hi. 🤘🏻 -- #iOSDev #SwiftLang

Twitter · GitHub


100% Swift news, delivered right into your mailbox

Subscribe to my monthly newsletter. On the first Monday of every month, you'll get an update about the most important Swift community news, including my articles.