· 6 min read Posted by Gustavo Fão Valvassori

Exploring Compose Web

Compose for HTML has different components than Compose for SKIA. In this article, we will explore and learn how to configure a few of them.
Joshua Earle - https://unsplash.com/pt-br/fotografias/silhueta-de-homem-de-pe-na-colina-durante-a-noite-estrelada-C6duwascOEA
Credit: Joshua Earle - https://unsplash.com/pt-br/fotografias/silhueta-de-homem-de-pe-na-colina-durante-a-noite-estrelada-C6duwascOEA

Now that we know how to create a compose application, we can start exploring how to render composable components and combine them all. In this article, we will explore a few basic components, how they appear on web, and how to customize them.

Basic elements

If you already have Compose experience, this one should not be a big mystery. You call a Composable Function, like Text(), send data as an argument, like a string, and it renders on the screen.

@Composable
fun MyTextComponents() {
    Text("This is a text")
}

This code will result in the following UI and HTML elements:

Text Element

But, if you look closer, it only renders plain text, with no formatting tags. With that in mind, you are probably asking questions like “How do I change the font size or make the text bold?”. And the answer is much easier than you expect: you use a composable component.

If you already have some HTML experience, you know how to apply changes to your text:

  • <span> for some inline text;
  • <p> if you need a paragraph;
  • <h1> to <h6> if you need a title;
  • And so on…

And the way you use it with Compose is pretty similar. But in this case, you will use the Compose syntax instead of HTML:

@Composable
fun MyTextComponents() {
    Span { Text("Some inline text") }

    P { Text("This is a new paragraph") }

    H1 { Text("This is a title") }
    H2 { Text("This is a title") }
    H6 { Text("This is a title") }
}

And as you can see bellow, it resulted in the following HTML elements:

Text Elements

Attributes

Now that we know the basics let’s move forward. The next thing you should probably be asking is about the HTML Tag attributes. How do I add a class to my element? How do I add an ID?

Let’s use a simple form to explain how to do things. We can learn how to set element attributes and listen for events with it.

@Composable
fun MyFormComponent() {
    Form {
        Label { Text("Email") }
        Input(type = InputType.Email)
        Button { Text("Submit") }
    }
}

This code is a simple form with a label, an input, and a button. It has no actions implemented yet. So, nothing happens if you click the label or the button, change the text, etc. All you have are UI elements like the screenshot below:

Empty form with no configuration

Now, we need to set the attributes for those elements so they can behave appropriately. For that, we need to:

  1. Set the ID of the input so the label can focus on it when clicked;
  2. Set the for attribute on the label so it can focus on the input;
  3. Set the type of the button so it can submit the form;
  4. Store the email on a state variable;
  5. Add a listener to the form submit event so we can handle the form submission;

If you are familiar with Compose, you already know that you can do most of the customizations on components using the Modifier. We don’t have it on the HTML version of compose, but we have a similar solution. Here, we use the Attribute Builder syntax. Instead of an object that we mutate and pass to the component, we use a lambda that receives all modifications and applies to the element.

@Composable
fun MyFormComponent() {
    var email by remember { mutableStateOf("") }

    Form(
        attrs = {
            // 5. Add the listener
            onSubmit { event ->
                event.preventDefault()
                println("Email: $email")
            }
        },
    ) {
        // 2. Setting the 'for' attribute
        Label(forId = "email-input") {
            Text("Email")
        }
        Input(
            type = InputType.Email,
            attrs = {
                // 1. Setting the input id
                id("email-input")
                
                // 4. Listening for changes and storing the value
                onChange { email = it.value }
            }
        )
        Button(
            attrs = {
                // 3. Setting button type            
                type(ButtonType.Submit)
            },
        ) { Text("Submit") }
    }
}

In this example, you can better understand how to set attributes on the elements. You can also see how to listen to events and store the state of the form. It is worth noticing that some elements have helper properties, so you don’t need to implement the attrs lambda. For example, the Label component has a forId property that will automatically set the for attribute on the element.

If you don’t see the property method you need on the component, you can always use the attr function and set it manually.

@Composable
fun CustomAttributeExample(ariaLabel: String) {
    Button(attrs = {
        attr("aria-label", ariaLabel)
    })
}

If this becomes a regular thing on your project, you can extract it into an extension function:

@Composable
fun CustomAttributeExample(ariaLabel: String) {
    Button(attrs = {
        ariaLabel(ariaLabel)
    })
}

fun AttrsScope<HTMLElement>.ariaLabel(label: String) {
    attr("aria-label", label)
}

Slot-based components

Slot-based components is a way of providing flexible and customizable components. This pattern relies on having composable functions as arguments so you can customize the component.

On Compose for JS, it provides a custom type used on most of the slot-based components.

@Composable
fun Div(
    attrs: AttrBuilderContext<HTMLDivElement>? = null,
    content: ContentBuilder<HTMLDivElement>? = null
) {
    TagElement(
        elementBuilder = Div,
        applyAttrs = attrs,
        content = content
    )
}

As you can see, it uses two types of lambdas: AttrBuilderContext and ContentBuilder. The first is used to customize the attributes of the element (as previously discussed), while the second is used to customize the element’s content. In other words, the second one is the slot parameter.

If you check the type implementation, you can see that it’s a lambda type for the TagElement you are using:

typealias AttrBuilderContext<T> = AttrsScope<T>.() -> Unit
typealias ContentBuilder<T> = @Composable ElementScope<T>.() -> Unit

Using them, you have better support from Compose as you have the element reference and other useful methods. But, in most cases, you may not need it. Mainly because of the following reasons:

  1. A simple composable function is enough for most cases;
  2. Most HTML elements are already supported out of the box, so you may not need to create your own;
  3. In case you need to create your own, you may need to use a considerable amount of time;
  4. In case you are creating wrapper components, you can use the original type as arguments;

Just in case you need to create a wrapper component and want to know how to do it, here is a basic example:

@Composable
fun Container(
    attrs: AttrBuilderContext<HTMLDivElement>? = null,
    content: @Composable  ElementScope<HTMLDivElement>.() -> Unit,
) {
    Div({
        attrs?.invoke(this)
        classes("my-container")
    }) {
        content()
    }
}

This can be useful for an internal design system library or even create types for CSS libraries, like Bootstrap.

Final Thoughts

In this article, we learned how to use the basic elements of Compose for HTML. How to set element attributes, and how slot-based components work on Compose JS.

In the following article, we will learn how to change component styles and use CSS Libraries.