One Off the Slack: How Do We Tie Theme Colors Together?

Lilly asked:

I would like to have the TextFieldColors to apply the color for onSecondary instead of onPrimary. I tried with TextStyle.color or wrapping it in a Surface and setting contentColor but nothing works. The text/label/indicator color is always black. My onSecodary color is white.

Column(modifier = Modifier.padding(8.dp)) {
            TextField(
                modifier = Modifier.fillMaxWidth(),
                value = password,
                onValueChange = { password = it },
                label = { Text("Password") },
                visualTransformation = PasswordVisualTransformation(),
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
           )
}

Bradleycorn offered up one solution:

In our app, we use custom colors on our TextFields as well. I wrote a wrapper composable that sets the colors on the TextField like this:

@Composable
fun ThemedTextField(
        label: String,
        modifier: Modifier = Modifier,
        textFilter: (String, String) -> String = {_, new -> new },
        backgroundColor: Color = MaterialTheme.colors.primary,
        keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
        visualTransformation: VisualTransformation = VisualTransformation.None,
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }) {

    val contentColor = contentColorFor(backgroundColor = backgroundColor)
    
    // Many of these values are the same as the default, just using contentColor instead 
    // of the default text field color. 
    val colors = TextFieldDefaults.textFieldColors(
        textColor = contentColor,
        backgroundColor = backgroundColor,
        cursorColor = contentColor,
        focusedIndicatorColor = contentColor.copy(alpha = ContentAlpha.high),
        unfocusedIndicatorColor = contentColor.copy(alpha = TextFieldDefaults.UnfocusedIndicatorLineOpacity),
        leadingIconColor = contentColor.copy(alpha = TextFieldDefaults.IconOpacity),
        trailingIconColor = contentColor.copy(alpha = TextFieldDefaults.IconOpacity),
        focusedLabelColor = contentColor.copy(alpha = ContentAlpha.high),
        unfocusedLabelColor = contentColor.copy(alpha = ContentAlpha.medium),
        placeholderColor = contentColor.copy(alpha = ContentAlpha.medium)
    )

    TextField(
            label = { Text(text = label, style = MaterialTheme.typography.body1, maxLines = 1) },
            maxLines = maxLines,
            singleLine = maxLines == 1,
            shape = RectangleShape,
            colors = colors,
            interactionSource = interactionSource,
            keyboardOptions = keyboardOptions,
            visualTransformation = visualTransformation,
            modifier = modifier
    )
}

Lilly… was not excited by the prospects offered by that solution:

It would be unfortunate if setting all/or some of the colors manually would be the only solution. I expected to find something more elegant, something like a switch to mimic that the background is in secondary color and so the content color too

Bradleycorn replied:

Well, you don’t have to set them all …. if you only want to set a few, and let the rest use their defaults, you could do that:

// Just set the text and cursor color, and use material defaults for everything else
val myColors = TextFieldDefaults.textFieldColor(textColor = Color.Red, cursorColor = Color.Blue)

TextField(colors = myColors)

Lilly responded:

Anyway, I would expect that this works:

MyTheme() {
    Surface(
        color = MaterialTheme.colors.secondary,
        contentColor = MaterialTheme.colors.contentColorFor(MaterialTheme.colors.secondary)
    ) {

        var text by remember { mutableStateOf("") }

        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = text,
            onValueChange = { text = it },
            label = { Text("Username") }
        )
    }
}

It seems the TextField composable is immune against theming. I might file a bug?

As Bradleycorn points out, Material Design is a rather opinionated framework:

I think it’s just that the Material Text Field has very specific guidelines, that include using certain colors from the theme.

My guess is that the Dev team would say, “if you want a customized text field, use the BasicTextField composable”

In our case, we want everything the material text field provides, just with some different coloring, so it was easiest to just create and pass in the color scheme that we wanted.

Also, as you can see from my initial example, there are a lot of colors in a TextField .. if it was going to use the LocalContentColor to build itself, which color does it use for that? The text? the cursor? the indicator (outline/underline)? Whichever color it uses LocalContentColor for, what does it set the OTHER colors to? There’s no way to know.

So, they probably just said, “let’s stick with the material guideline colors”, and if the dev wants to use different colors, we’ll expose a colors property they can set.

After examining the TextField() code, Lilly was hoping that there was simply one place to make the color change, rather than overwriting lots of colors. Bradleycorn suggested DIY:

You could write your own function to do that. It could be useful if you need to show textfields on different backgrounds and use a consistent coloring scheme.

Could even make it an extension on the Material Theme if you want…

@Composable
fun MaterialTheme.textFieldColors(baseColor: Color): TextFieldColors = TextFieldDefaults.textFieldColors( 
   .. set the colors you want here ... 
)

Colton Idle chimed in at the end:

Whenever I have one offs like this I jsut create a one off theme and then everything typically works. Much less code. It’s what one of the official compose samples uses. I think @nickbutcher mentioned in a video that you should consider it like a theme overlay in legacy android.


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!