· 8 min read Posted by Gustavo Fão Valvassori

Styling your components on Compose Web

Styling your UI is a very important part of any application. This article will explore how to style our Compose Web components.
Anna Kolosyuk - https://unsplash.com/pt-br/fotografias/tres-pinceis-de-prata-no-tecido-branco-D5nh6mCW52c
Credit: Anna Kolosyuk - https://unsplash.com/pt-br/fotografias/tres-pinceis-de-prata-no-tecido-branco-D5nh6mCW52c

Styling is an essential part of your app development. With that, you can improve the user experience and make your app look better.

For Web applications, the styling part is mainly done with CSS. CSS stands for Cascading Style Sheets, a language used to describe the presentation of a document. As Compose for Web still uses HTML for rendering the components, we will use CSS as the styling language.

Inline styling

The first way to style your components is by using inline styling. The style is passed as an HTML attribute parameter to the element. For example, if you want to style a div component, you can do it like this:

<div style="background-color: red;">Hello on red background</div>

Compose has a style method inside the attribute builder (which we saw in the last article) to support this type of customization. This method receives a lambda with the type StyleScope.() -> Unit. The StyleScope is a class that has a lot of methods to customize the style of your component. In Compose, the above example would be like this:

Div(
    attrs = {
        style { backgroundColor(Color.red) }
    }
) {
    Text("Hello on red background")
}

Inside the builder scope, you have access to many properties and methods to customize your component. If your styling method is not implemented, you can always use the property method to set a custom property. For example:

Div(
    attrs = {
        style { property("background-color", Color.red) }
    }
) {
    Text("Hello on red background")
}

Stylesheets

Inline styling is good for minor customizations on one element. But if you want to reuse them, you will need to use stylesheets. Stylesheets are a way to define a set of styles that can be applied to multiple components. You can create an object inheriting from the StyleSheet class to create a stylesheet with Compose.

object MyCSS: StyleSheet() {
    // TODO: Add your styles here
}

Then, to apply this stylesheet to your project, you must declare it on your root component. For that, you should call the Style() method with your class inside the renderComposable() method.

fun main() {
    renderComposable(rootElementId = "root") {
        Style(MyCSS)
        App()
    }
}

When you call the Style() method, it will create a <style> tag on your HTML document and add all the styles from your stylesheet to it. If you miss this step, your elements will not be styled when you set a style to them.

CSS DSL

When you create a stylesheet, you can define multiple attributes on it. Each attribute will be implemented using the by style delegate method, becoming a CSS class that can be used on your components. The value return by this delegate is a String value with the name of the class.

object MyCSS: StyleSheet() {
    val redBackground by style {
        property("background-color", Color.red)
    }
}

After creating your style, you can apply it to your component. For example:

@Composable
fun App() {
    Div(
        attrs = {
            classes(MyCSS.redBackground)
        }
    ) {
        Text("Hello on red background")
    }
}
Inside the style delegation lambda, you have access to the self selector. This selector is a reference to the current CSS class. You can use it to combine with other selectors to apply styles for specific states. We will see more about selectors later.

Element Type selector

In case you need to apply any changes to all HTML elements with a given tag, you use the type() selector function. It will return a selector instance with a style method for customizations. For example, if you want to change all table elements, you can do it like this:

object MyCSS: StyleSheet() {
    init {
        type("td") style {
            padding(0.px)
        }
    }
}

This style will be applied to all elements with the td tag, like the example below:

@Composable
fun App() {
    Table {
        Tr {
            Td { Text("0,0") }
            Td { Text("0,1") }
            Td { Text("0,2") }
        }
        Tr {
            Td { Text("1,0") }
            Td { Text("1,1") }
            Td { Text("1,2") }
        }
        Tr {
            Td { Text("2,0") }
            Td { Text("2,1") }
            Td { Text("2,2") }
        }
    }
}

Class selector

In CSS, classes are groups of elements with the same style. Its syntax is very similar to the type selector, but instead of using the type() function, you use the className() function. For example, if you want to change a class named ‘redBackground’, you can do it like this:

object MyCSS: StyleSheet() {
    init {
        className("redBackground") style {
            backgroundColor(Color.red)
        }
    }
}

This style will be applied to all elements that have the ‘redBackground’ class, like the example below:

fun App() {
    Div(
        attrs = {
            classes("redBackground")
        }
    ) {
        Text("Hello on red background")
    }
}

ID Selector

When you want to customize specific elements, you can use the ID selector. It is similar to the class selector, but instead of using the className() function, you use the id() function. It will query for the element containing the given ID. For example, if you want to change the style of an element with the ID ‘myButton’, you can do it like this:

object MyCSS: StyleSheet() {
    init {
        id("myButton") style {
            backgroundColor(Color.red)
        }
    }
}

This style will only be applied to the element with the ID ‘myButton’, like the example below:

@Composable
fun App() {
    // This one will have style
    Button(attrs = { id("myButton") }) {
        Text("Button with ID") 
    }

    // This one will not have style
    Button {
        Text("Button without ID")
    }
}

Inheritance and Sibilings

Inheritance is a significant part of CSS. It allows you to customize a component based on its parent. With it, you can change how a child, siblings, or adjacent elements are rendered. You can use the child(), sibling(), or adjacent() methods to pass the self selector as the parent.

object MyCSS: StyleSheet() {
    val myTableEntry by style {
        child(self, type("button")) style {
            backgroundColor(Color.blue)
        }
        
        child(self, type("text")) style {
            backgroundColor(Color.red)
        }
    }
}

Using their selector functions, you can also do the same without the classes, IDs, or types. For example, all buttons inside the table but without needing to create a new class:

object MyCSS: StyleSheet() {
    init {
        child(type("td"), type("button")) style {
            backgroundColor(Color.blue)
        }
    }
}

Combining styles

You can also combine styles by using the ’+’ operator. For example, if you want to apply a style to all elements with the “btn” and “warning” or “error” classes, you can do it like this:

object MyCSS : StyleSheet() {
    val btn by style {
        fontSize(20.pt)
        
        self + className("warning") style {
            backgroundColor(Color.yellow)
        }

        self + className("error") style {
            backgroundColor(Color.red)
        }
    }
}

You can also combine multiple attributes from your stylesheet object:

object MyCSS : StyleSheet() {
    val warning by style {
        // Style for warning
    }
    
    val error by style {
        // Style for error
    }

    val btn by style {
        fontSize(20.pt)
        
        self + warning style {
            backgroundColor(Color.yellow)
        }

        self + error style {
            backgroundColor(Color.red)
        }
    }
}

Pseudo selectors

In CSS, we have a lot of pseudo-selectors that can be used to apply styles to elements in specific states. For example, you can apply a style to all button elements that are being hovered by the mouse. To do that, you can use the hover pseudo selector using the compose syntax presented above:

object MyCSS : StyleSheet() {
    val myButton by style {
        backgroundColor(Color.red)
        
        self + hover style {
            backgroundColor(Color.blue)
        }
    }
}

Other pseudo selectors that are supported and you can use are: active, checked, disabled, empty, enabled, first-child, focus, nth-child, etc. You can find more information about pseudo selectors on the MDN Web Docs and the StyleSheetBuilder file from the Compose source sets.

Raw CSS Selectors

If you prefer to write selectors like in plain CSS, you can use the selector() method or just use it as a string. This is a “raw” way of doing everything we saw above. For some cases, it may be easier (like for types), but for others, it may be harder to read. For example, the following code is equivalent to all we saw above:

object MyCSS: StyleSheet() {
    init {
        "td" style {
            padding(0.px)
        }

        ".contentTable" style {
            backgroundColor(Color.red)
        }

        "#myButton" style {
            backgroundColor(Color.red)
        }

        "html, body" style {
            height(98.percent)
            width(100.percent)
            overflow("hidden")
        }

        "btn > warning" style {
            backgroundColor(Color.yellow)
        }

        "btn > error" style {
            backgroundColor(Color.red)
        }

        ".myButton:hover" style {
            backgroundColor(Color.blue)
        }
    }
}

Final Thoughts

Styling is a very important part of your application. Compose provides a ton of ways to customize your components. You can choose to use either inline styling or stylesheets. The DSL has a lot of methods to help you write your style.

This article did not mention a few topics, like media queries, animations, variables, etc. In future articles, we will explore more about styling and how to use it to make your app look better.