Utilities & Testing

Responsive design, CSS-in-Kotlin, file handling, and component testing.

Responsive design

The ResponsiveController tracks the browser window size and exposes the current display type as a reactive Stream. Access it from any component:

class MyComponent(ctx: Ctx<Props>) : Component<Props>(ctx) {

    // Subscribe to the responsive controller — redraws when the breakpoint changes
    private val responsive by subscribingTo(responsiveCtrl)

    override fun VDom.render() {
        when (responsive.displayType) {
            DisplayType.Mobile -> renderMobileLayout()
            DisplayType.Tablet -> renderTabletLayout()
            DisplayType.Desktop -> renderDesktopLayout()
        }
    }
}

Custom breakpoints

By default, Kraft registers a ResponsiveController with standard breakpoints. You can override it in the app builder:

val app = kraftApp {
    responsive(
        ResponsiveController(
            breakpoints = ResponsiveController.Breakpoints(
                tablet = 600,   // default: 768
                desktop = 1024, // default: 1200
            )
        )
    )

    routing { /* ... */ }
}

Default breakpoints

If you don't configure anything, the defaults are:

Display type Window width
Mobile < 768px
Tablet 768px – 1199px
Desktop ≥ 1200px

Convenience checks on the state:

responsive.isDesktop      // true when >= 1200px
responsive.isMobile       // true when < 768px
responsive.isNotDesktop   // tablet or mobile
responsive.isNotMobile    // tablet or desktop
responsive.windowSize     // Vector2D with current width and height

CSS & Styling

Kraft supports CSS-in-Kotlin through the StyleSheet class. Define rules as delegated properties — class names are automatically mangled to avoid collisions:

object MyStyles : StyleSheet() {
    val container by rule {
        display = Display.flex
        justifyContent = JustifyContent.center
        padding = Padding(16.px)
    }

    val highlight by rule {
        backgroundColor = Color("#fff3cd")
        borderRadius = BorderRadius(4.px)
    }
}

All three stylesheet types (StyleSheet, RawStyleSheet, StyleSheetTag) auto-mount by default — they inject their CSS into the page as soon as the object is initialized. No manual StyleSheets.mount() call needed.

To opt out, pass autoMount = false and mount manually when needed:

object LazyStyles : StyleSheet(autoMount = false) {
    val card by rule { padding = Padding(12.px) }
}

// Mount later
StyleSheets.mount(LazyStyles)
// Unmount to remove
StyleSheets.unmount(LazyStyles)

Use the generated class names in your components. Each rule gets a mangled class name to avoid collisions:

override fun VDom.render() {
    div(MyStyles.container.name) {
        div(MyStyles.highlight.name) {
            +"Highlighted content"
        }
    }
}

Nested rules

Target child elements within a rule using nested CSS selectors:

object CardStyles : StyleSheet() {
    val card by rule {
        padding = Padding(12.px)

        // nested: targets h2 elements inside .card
        "h2" {
            fontSize = FontSize(1.2.em)
            fontWeight = FontWeight.bold
        }

        // nested: targets > .content children
        "> .content" {
            marginTop = 8.px
        }
    }
}

Scoped rules

Create rules scoped to a parent rule using the rule(contextRule) overload:

val card by rule {
    padding = Padding(12.px)
}

// generates: .card_abc123.cardTitle_def456
val cardTitle by rule(card) {
    fontSize = FontSize(1.2.em)
    fontWeight = FontWeight.bold
}

Raw CSS and external stylesheets

For raw CSS strings or external stylesheet links — both auto-mount by default:

// Inject a raw CSS string (auto-mounts immediately)
val rawCss = RawStyleSheet("""
    .custom-class { color: red; }
""")

// Link an external stylesheet (auto-mounts immediately)
val externalCss = StyleSheetTag {
    href = "https://cdn.example.com/styles.css"
}

File handling

Read files from a file input as Base64-encoded data:

input {
    type = InputType.file
    multiple = true
    onChange { evt ->
        launch {
            val files = evt.target.unsafeCast<HTMLInputElement>().files!!
            val loaded: List<LoadedFileBase64> = files.loadAllAsBase64()

            loaded.forEach { file ->
                println(file.file.name)      // original filename
                println(file.mimeType)       // e.g. "image/png"
                println(file.dataBase64)     // base64 content
                println(file.dataUrl)        // full data URL
            }
        }
    }
}

Responsive images

The SrcSetImage component generates srcset and sizes attributes automatically from a single image URL:

SrcSetImage(
    src = "https://cdn.example.com/photo.jpg",
    sizes = ImageSizes.default,
    alt = "A photo",
)