# Compose Toolkit

Compose is a modern user-interface framework. It was originally designed by Google for Android development, and released as JetPack Compose. Kotlin, Gradle, and Jetpack Compose together represent Google's preferred toolchain for Android development.

JetBrains recently ported Jetpack Compose to desktop, and released it as Compose Multiplatform. The complete toolkit has been ported to desktop, and augmented with specific desktop composables (e.g., a window doesn't exist on Android, but does exist on desktop).

# Concepts

Compose is a declarative framework. As compared to traditional imperative toolkits, a declarative framework has two specific differences:

  1. It focuses on what you want to display on-screen, instead of how it should be created and managed. Imperative toolkits often expect the developer to use declarative syntax to describe the UI structure (e.g. Android and XML files), and then write the underlying glue-code to make that UI interactive. Declarative environments simplify this structure, so that UIs are defined completely in code. Declarative code is shorter and often simpler and easier to read.
  2. State-flow is handled differently. The framework itself manages state, and determines when and how to update and re-render components on-screen. This greatly simplifies the work that the developer has to do, and avoids a lot of the accidental complexity that creeps into large UIs.

A declarative structure is much simpler to read and maintain. Other modern toolkits like Swift UI and React are built around similar concepts, and have been extremely successful with this paradigm.

Compose is also cross-platform, so you can share code across platforms. This makes it easier to write a single application that can run on Android, iOS, desktop and even targets like WASM. Compose minimizes the amount of custom code that you need to write for each platform.

Finally, Compose is designed to be extremely fast. Under-the-covers, it uses skia, a high-performance Open Source graphics library, to draw on a native canvas on each target platform (the same library used by the Chrome browser). This enables it to achieve consistently high performance across all platforms, and makes it much "snappier" than early toolkits that relied on software renderers.

# Composable functions

A key concept in Compose is the idea of a composable function, which is a particular kind of function that describes a small portion of your UI. You build your entire interface by defining a set of composable functions that take in data and emit UI elements.

Here's an example of a simple composable function (aka composable) that takes in a String and displays it on-screen:

@Composable
fun Greeting(name: String) {
  Text("Hello $name!")
}

Here is the corresponding scene graph for this composable function:

Scene graph for our Greeting function
Scene graph for our Greeting function

Here are some characteristics of a composable:

  • The function is annotated with the @Composable annotation. All Composable functions that we write must have this annotation.
  • Composable functions will often accept parameters, which are used to format the composable before displaying it.
  • This function actually creates and displays the Text composable. We say that composables emit UI.
  • The function doesn't return anything. Compose functions that emit UI do not need to return anything, because they describe the desired screen state directly.
  • This function is fast, idempotent, and free of side effects.

# Composable scope

Let's use this code snippet to display some text in an application window.

fun main() = application {
    Window(
      title = "Hello Window", 
      onCloseRequest = ::exitApplication
    ) {
      Greeting("Compose")
    }
}

@Composable
fun Greeting(name: String) {
  Text("Hello $name!")
}

In this snippet, the application function defines a Composable Scope (think of a scope as the context in which our application runs). Within that scope, we use a Window composable to emit a window. We pass it two parameters:

  1. a title string that will be set for the window, and
  2. a lambda function that will be executed when an onCloseRequest event is received (i.e. when the window closes, we execute the build-in exitApplication function).

In this case, the Window composable calls our Greeting composable, which emits the Text composable, which in turn displays our text.

Hello Compose
Hello Compose

Let's try and add some interactivity to this application. We'll display our initial string on a button. When the user presses the button, it will change the value being displayed.

Our Button is yet-another composable. It requires us to set a parameter named onClick, which is assigned to the function (or lambda) that will be called when the button is clicked (for those familiar with other toolkits, we're assigning the onClick event handler for that button).

Let's start by just confirming that we can print something to the console when the button is pressed.

fun main() = application {
    Window(
        title = "Hello Window",
        onCloseRequest = ::exitApplication
    ) {
        Greeting("Unpressed")
    }
}

@Composable
fun Greeting(name: String) {
    Button(onClick = { println("Button pressed") }) {
        Text("Hello $name")
    }
}

Hello Kotlin Button 1
Hello Kotlin Button 1

So far it works as we'd hoped! The button displays the string passed in, and our event handler prints to the console when the button is pressed. Let's try and change our event handler so that we instead change the text on the button when it's pressed.

It's a little tricky because we cannot update the parameter directly, so we create a variable currentName to store our display value and then update that in the handler.

fun main() = application {
    Window(
        title = "Hello Window",
        onCloseRequest = ::exitApplication
    ) {
        Greeting("Unpressed")
    }
}

@Composable
fun Greeting(name: String) {
    var currentName = name
    Button(onClick = { currentName = "Pressed" }) {
        Text("Hello $currentName")
    }
}

Hello Kotlin Button 2
Hello Kotlin Button 2

Nothing changed? Why didn't that work?! It has to do with how Compose manages and reflects state changes.

# Recomposition

The declarative design of Compose means that it draws the screen when the application launches, and then only redraws elements when their state changes. Compose is effectively doing this:

  • Drawing the initial user interface.
  • Monitoring your state (aka variables) directly.
  • When a change is detected in state, the portion of the UI that relies on that state is updated.

Compose redraws affected components by calling their Composable functions. This process - detecting a change, and then redrawing the UI - is called recomposition and is the main design principle behind Compose.

Why doesn't our example work? In our example above, the onClick handler attempts to change the text property of the Button. This triggers Compose to call the Window composable, which calls the Button composable, which initializes text to it's initial value... Not what we intended.

We have 2 fundamental challenges to address:

  1. Storing state such that it is observable by Compose.
  2. Making sure that we persist state between calls to a Composable function, so that we're not just re-initializing it each time.

# Managing state

To make the state observable, we store it in instances of a MutableState class that Compose can directly monitor. In our example, we'll use the mutableStateOf wrapper function to do this:

fun main() = application {
    Window(
        title = "Hello Window",
        onCloseRequest = ::exitApplication
    ) {
        Greeting("Unpressed")
    }
}

@Composable
fun Greeting(name: String) {
    var currentName = remember { mutableStateOf(name) }
    Button(onClick = { currentName.value = "Pressed" }) {
        Text("Hello ${currentName.value}")
    }
}

It works! Compose detects when we have clicked on the Button(onClick), updates the text state (currentName.value), and then recomposes the Window and its children based on this new state.

Hello Kotlin Button 3
Hello Kotlin Button 3

Note that since we changed the type of text from a String to a MutableState<String>, we had to change all variable references to text.value to retrieve the actual value from the state (since it is a class with more properties than just it's state value).

We also added the remember { } keyword to ensure that the function remembers the state values from the last time it executed. This prevents the function from reinitializing state when it recomposes.

There are multiple classes to handle different types of State. Here's a partial list—see the Compose documentation for an exhaustive list.

Class Helper Function State that it represents
MutableState<T> mutableStateOf() Primitive <T>
MutableList<T> mutableListOf List<T>
MutableMap<K, V> mutableMapOf(K, V) Map<K, V>
WindowState rememberWindowState() Window parameters e.g. size, position
DialogState rememberDialogState Similar to WindowState

We can use these other types of state in appropriate places in our code. For instance, we can add WindowState to the Window and use that to set the window size and position.

fun main() = application {
    Window(
        title = "Hello Window",
        onCloseRequest = ::exitApplication,
        state = WindowState(width=300.dp, height=200.dp, position = WindowPosition(50.dp, 50.dp))
    ) {
        val text = remember { mutableStateOf("Press me") }
        Button(onClick = {text.value = "Pressed!"}) {
            Text(text.value)
        }
    }
}

This is a much more reasonable size!

# State hoisting

A composable that uses remember is storing the internal state within that composable, making it stateful (e.g. our Greeting composable function above).

However, storing state in a function can make it difficult to test and reuse. It's sometimes helpful to pull state out of a function into a higher-level, calling function. This process is called state hoisting.

Here's an example from the JetPack Compose documentation. In the example below, our state is the name that the user is typing in the OutlinedTextField. Instead of storing that in our HelloContent composable, we keep our state variable in the calling class HelloScreen and pass in the callback function that will set that value. This allows us to reuse HelloContent by calling it from other composable functions, and keeping the state in the calling function in each case.

fun main() = application {
    Window(
      title = "Hello Window", 
      onCloseRequest = ::exitApplication
    ) {
        HelloScreen()
    }
}

@Composable
fun HelloScreen() {
    var name by remember { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.body1
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

State hoisting
State hoisting

# Layout

Let's discuss using various @Composables that we can use to build our user interface!

For a detailed guide on layouts, refer to Compose Layouts.

There are three basic Composables that we can use to structure our UIs:

  • Column, used to arrange widget elements vertically
  • Row, used to arrange widget elements horizontally
  • Box, used to arrange objects in layers

# Column

A column is a vertical arrangement of composables.

fun main() = application {
    Window(
      title = "CS 346 Compose Layout Demo", 
      onCloseRequest = ::exitApplication
    ) {
        SimpleColumn()
    }
}

@Composable
fun SimpleColumn() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("One")
        Text("Two")
        Text("Three")
    }
}

Column demo
Column demo

# Row

A row is a horizontal arrangement of composables.

fun main() = application {
    Window(
      title = "CS 346 Compose Layout Demo", 
      onCloseRequest = ::exitApplication
    ) {
        SimpleRow()
    }
}

@Composable
fun SimpleRow() {
    Row(
        modifier = Modifier.fillMaxSize(),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        Text("One")
        Text("Two")
        Text("Three")
    }
}

Row demo
Row demo

# Box

A box is just a rectangular region. Use the composable alignment properties to place each of a Box's children within its boundaries.

fun main() = application {
    Window(
        title = "Custom Theme",
        onCloseRequest = ::exitApplication,
        state = WindowState(width = 300.dp, height = 250.dp, position = WindowPosition(50.dp, 50.dp))
    ) {
        SimpleBox()
    }
}

@Composable
fun SimpleBox() {
    Box(Modifier.fillMaxSize().padding(15.dp)) {
        Text("Drawn first", modifier = Modifier.align(Alignment.TopCenter))
        Text("Drawn second", modifier = Modifier.align(Alignment.CenterStart))
        Text("Drawn third", modifier = Modifier.align(Alignment.CenterEnd))
        FloatingActionButton(
            modifier = Modifier.align(Alignment.BottomEnd),
            onClick = {println("+ pressed")}
        ) {
            Text("+")
        }
    }
}

Box demo
Box demo

We often nest these layout composables together:

Combined layout
Combined layout

Here's the code that builds this screen. It contains a Column as the top-level composable, and a Row at the bottom that contains Text and Button composables (which is how we have the layout flowing both top-bottom and left-right).

@Composable
fun CombinedDemo(modifier:Modifier = Modifier) {
    Column(
        modifier = modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "This Window contains a Column, which in turn holds the elements below. A Column positions things top-bottom, using properties that you set. We've set this window to center content both vertically and horizontally.",
            style = MaterialTheme.typography.body1,
            modifier = modifier.width(600.dp)
        )
        Button(
            modifier = modifier,
            onClick =  { println("Button clicked.") }
        ) {
            Text(text = "This is a Button containing Text.")
        }
        Text(
            text = "A block of text. We can apply formatting, themes and so on.",
            style = MaterialTheme.typography.body1,
            modifier = modifier
        )
        Row() {
            Text(
                text = "Label",
                style = MaterialTheme.typography.body1,
                modifier = modifier.align(alignment = Alignment.CenterVertically).padding(10.dp)
            )

            Button(
                modifier = modifier,
                onClick = { println("A different button clicked.") }
            ) {
                Text("Some value")
            }
        }
    }

# Lazy Layouts

Columns and rows work fine for a small amount of data that fits on the screen. What do you do if you have large lists that might be longer or wider than the space that you have available? Ideally, we would like that content to be presented in a fixed region of the screen, and be scrollable - so that you can move up and down through the list. For performance reasons, we also want large amounts of data to be lazy loaded: only the data that is being displayed needs to be in-memory and other data is loaded only when it needs to be displayed.

Compose has a series of lazy components that work like this:

  • LazyColumn
  • LazyRow
  • LazyVerticalGrid
  • LazyHorizontalGrid

Here's an example of using a LazyRow to present contents that are spread out horizontally, and will lazy-load.

Lazy row demo
Lazy row demo

fun main() = application {
    Window(
        title = "LazyColumn",
        state = WindowState(width = 500.dp, height = 100.dp),
        onCloseRequest = ::exitApplication
    ) {
        LazyRowDemo()
    }
}

@Composable
fun LazyRowDemo(modifier: Modifier = Modifier) {
    LazyRow(
        modifier = modifier.padding(4.dp).fillMaxSize(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        items(45) {
            Button(
                onClick = { },
                modifier = Modifier
                    .size(100.dp, 50.dp)
                    .padding(4.dp)
            ) {
                Text(it.toString())
            }
        }
    }
}

We can do something similar to show a scrollable grid of data:

Lazy grid demo
Lazy grid demo

@Composable
fun AndroidLazyGrid(modifier: Modifier = Modifier) {
    LazyVerticalGrid(modifier = modifier, columns = GridCells.Fixed(5)) {
        val colors = listOf<Color>(Color.Blue, Color.Red, Color.Green)
        items(45) {
            AndroidAlien(color = colors.get(Random.nextInt(0,3)) )
        }
    }
}

# Properties

Each class has its own parameters that can be supplied to affect its appearance and behaviour.

# Arrangement

Arrangement specifies how elements are laid out by the class. e.g. Row has a horizontalArrangement since it lays elements out horizontally; Column has a verticalArrangement to control vertical layout. Arrangement must be one of these values:

  • Arrangement.SpaceEvenly: Place children such that they are spaced evenly across the main axis,
  • Arrangement.SpaceBetween: Place children such that they are spaced evenly across the main axis, without free space before the first child or after the last child.
  • Arrangement.SpaceAround: Place children such that they are spaced evenly across the main axis, including free space before the first child and after the last child, but half the amount of space existing otherwise between two consecutive children.
  • Arrangement.Center: Place children such that they are as close as possible to the middle of the main axis.
  • Arrangement.Top: Place children vertically such that they are as close as possible to the top of the main axis
  • Arrangement.Bottom: Place children vertically such that they are as close as possible to the bottom of the main axis

# Alignment

Alignment specifies how elements are aligned along the dimension of this container class. e.g. top, bottom, or center. These are orthogonal to arrangement (e.g. a Row lays out elements in a horizontal path/arrangement but aligns elements vertically in that path). Alignment must be one of these values:

  • Alignment.CenterHorizontally: Center across the horizontalAlignment (e.g. Column)
  • Alignment.CenterVertically: Center across the verticalAlignment (e.g. Row).

# Modifier

Modifier is a class that contains parameters that are commonly used across elements. This allows us to set a number of parameters within an instance of Modifier, and pass those options between functions in a hierarchy. This is very helpful when you want to set a value and have it cascade through the scene graph (e.g. set horizontalAlignment = Alignment.CenterHorizontally once and have it propagate).

You can see how this is used in the CombinedDemo below:

  • an initial modifier is passed as a parameter to the CombinedDemo composable function. If one is not provided, it uses the default Modifier.
    • fun CombinedDemo(modifier:Modifier = Modifier)
  • the Column composable uses the instance of the Modifier, and appends some new values to it. Column then inherits any values that were already set plus any new values that are initialized. In this case, we add padding(16) to the column's instance.
    • modifier = modifier.fillMaxSize().padding(16.dp)
  • Further composables that the Column calls can either use the modifier that is passed in (modifier = modifier) or add additional values (modifier = modifier.width(600.dp))

# Standard Composables

There's a very large number of widgets that you can use in Compose! Because it's cross-platform, most composables and functions exist across all supported platforms.

The Jetpack Compose reference guide is the best source of information on the Composables that are included in the material theme.

Some of the code snippets below inspired by this article: Widgets in JetPack Compose. Others are taken from the Jetpack Compose Material Components list.

# Text

A Text composable displays text.

@Composable
fun SimpleText() {
  Text(
      text = "Widget Demo",
      color = Color.Blue,
      fontSize = 30.sp,
      style = MaterialTheme.typography.h2, maxLines = 1
      )
}

Text composable
Text composable

# Image

An image composable displays an image (by default, from your Resources folder in your project).

@Composable
fun SimpleImage() {
    Image(
        painter = painterResource("credo.jpg"),
        contentDescription = null,
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .height(150.dp)
            .fillMaxWidth()
            .clip(shape = RoundedCornerShape(10.dp))
    )
}

Image composable
Image composable

# Button

There are three main types of Buttons:

  • Button: A standard button with no caption. Used for primary input.
  • OutlinedButton: A button with an outline. Intended to be used for secondary input (lesser importance).
  • TextButton: A button with a caption.

The onClick function is called when the user pressed the button.

There are also OutlinedButton and TextButton composables

fun main() {
    application{
        Window(onCloseRequest = ::exitApplication, title = "Button Demo") {
            Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
                Button(onClick = { println("Button clicked") }) { Text("Caption") }
                OutlinedButton(onClick = { println("OutlinedButton clicked") }) { Text("Caption") }
                TextButton(onClick = { println("TextButton clicked") }) { Text("Caption") }
            }
        }
    }
}

Button composable
Button composable

# Card

The Card composable is a container for parts of your user-interface, intended to hold related content. e.g. a tweet, an email message, a new story in a new application and so on. It's intended to be a smaller UI element in some larger container like a Column or Row.

Here's an example from the Card composable documentation.

Card composable
Card composable

An elevated card populated with text and icons.

@Composable
fun CardMinimalExample() {
    Card() {
        Text(text = "Hello, world!")
    }
}

# Chip

A chip compact, interactive UI element, often with both an icon and text label. These often represent selectable or boolean values.

Here's an example from the Chip composable documentation.

An example of each of the four chip components, with their unique characteristics highlighted.
An example of each of the four chip components, with their unique characteristics highlighted.

@Composable
fun AssistChipExample() {
    AssistChip(
        onClick = { Log.d("Assist chip", "hello world") },
        label = { Text("Assist chip") },
        leadingIcon = {
            Icon(
                Icons.Filled.Settings,
                contentDescription = "Localized description",
                Modifier.size(AssistChipDefaults.IconSize)
            )
        }
    )
}

# Checkbox

A checkbox is a toggleable control that presents true/false state. The OnCheckedChange function is called when the user interacts with it (and in this case, the state represented by it is stored in a MutableState variable named isChecked).

@Composable
fun SimpleCheckbox() {
    val isChecked = remember { mutableStateOf(false) }
    Checkbox(
        checked = isChecked.value ,
        enabled = true,
        onCheckedChange = {
            isChecked.value  = it
        }
    )
}

Checkbox composable
Checkbox composable

# Switch

A Switch is a toggle control similar to a checkbox, in that it represents a boolean state.

@Composable
fun SwitchMinimalExample() {
    var checked by remember { mutableStateOf(true) }

    Switch(
        checked = checked,
        onCheckedChange = {
            checked = it
        }
    )
}

Switch composable
Switch composable

# Slider

A slider lets the user make a selection from a continuous range of values. It's useful for things like adjusting volume or brightness, or choosing from a wide range of values.

Here's an example from the Slider compose documentation.

@Preview
@Composable
fun SliderMinimalExample() {
    var sliderPosition by remember { mutableFloatStateOf(0f) }
    Column {
        Slider(
            value = sliderPosition,
            onValueChange = { sliderPosition = it }
        )
        Text(text = sliderPosition.toString())
    }
}

Slider composable
Slider composable

# Spacer

A spacer just adds empty space. It's useful when you only want to force space between elements.

@Composable
fun getExtraSpace() {
      Spacer(modifier = Modifier.height(20.dp))
}

# Scaffold

A scaffold makes it easy to build an Android-style application, with a top application bar, a bottom application bar, and elements like floating action buttons. Think of it as a pre-defined layout to help you get started with a commonly used structure.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                // A surface container using theme 'background' color
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ScaffoldExample()
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScaffoldExample() {
    var presses by remember { mutableStateOf(0) }

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Top app bar") })
        },
        bottomBar = {
            BottomAppBar(
                containerColor = MaterialTheme.colorScheme.primaryContainer,
                contentColor = MaterialTheme.colorScheme.primary,
            ) {
                Text(
                    modifier = Modifier.fillMaxWidth(),
                    textAlign = TextAlign.Center,
                    text = "Bottom app bar",
                )
            }
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { presses++ }) {
                Icon(Icons.Default.Add, contentDescription = "Add")
            }
        }
    ) { innerPadding ->
        Column(
            modifier = Modifier.padding(innerPadding),
            verticalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            Text(
                modifier = Modifier.padding(8.dp),
                text =
                """
                    This is an example of a scaffold. It uses the Scaffold composable's parameters to create a screen with a simple top app bar, bottom app bar, and floating action button.

                    It also contains some basic inner content, such as this text.

                    You have pressed the floating action button $presses times.
                """.trimIndent(),
            )
        }
    }
}

Scaffold example
Scaffold example

See the App Bar documentation for examples of how to customize top and bottom app bars further.

# Using Themes

A theme is a common look-and-feel that is used when building software. Google includes their Material Design theme in Compose, and by default, composables will be drawn using the Material look-and-feel. This includes colors, opacity, shadowing and other visual elements. Apps built using the Material design system have very specific look-and-feel (example below from the Material Getting-Started documentation):

Material design
Material design

You might want to change the appearance of your application, either for product branding purposes, or to make it appear more "standard" for a specific platform. This can be done quite easily, by either extending and modifying the built-in theme, or replacing it completely.

# Customizing

To customize the default theme, we can just extend it and change its properties, and then set our application to use the modified theme. See The Color System for details on how colors are applied to Composables.

fun main() = application {
    Window(
        title = "Hello Window",
        onCloseRequest = ::exitApplication,
        state = WindowState(width=300.dp, height=250.dp, position = WindowPosition(50.dp, 50.dp))
    ) {
        CustomTheme {
            Column {
                // primary color
                val buttonText = remember { mutableStateOf("Press me") }
                Button(onClick = { buttonText.value = "Pressed!" }) {
                    Text(buttonText.value)
                }
                val outlinedButtonText = remember { mutableStateOf("Press me") }
                OutlinedButton(onClick = { outlinedButtonText.value = "Pressed!" }) {
                    Text(outlinedButtonText.value)
                }

                // secondary color
                var switchState = remember { mutableStateOf(false) }
                Switch(switchState.value, onCheckedChange = { switchState.value = !switchState.value })
            }
        }
    }
}

@Composable
fun CustomTheme(
    content: @Composable () -> Unit
) {
    MaterialTheme(
        // change main colors
        colors = MaterialTheme.colors.copy(
            primary = Color.Red,
            secondary = Color.Magenta,

        ),
        // square off corner of components
        shapes = MaterialTheme.shapes.copy(
            small = AbsoluteCutCornerShape(0.dp),
            medium = AbsoluteCutCornerShape(0.dp),
            large = AbsoluteCutCornerShape(0.dp)
        )
    ) {
        content()
    }
}

Custom theme
Custom theme

# Third-party themes

There are third-party themes that you can include to replace the Material theme completely:

  • Aurora library allows you to style Compose applications using the Ephemeral design theme.
  • JetBrains Jewel changes the look-and-feel to match IntelliJ applications. e.g. IntelliJ IDEA.
  • MacOS theme mimics the standard macOS-look-and-feel.