PUBLIC OBJECT

Dynamic Tints with CSS and Kotlin/JS

I’ve been trying to build a remarkable UI for Rounds.app. One feature that turned out quite well is tinting the game name & menu bar icons when the winner changes.

In this recording you can see colors change when I toggle the win condition:

The title color changes when the winner changes

I’ve got a two CSS classes, tinted and tintedDefault for the game name. I’m using a CSS transition to animate color changes:

.tinted {
  transition: color 300ms linear;
}

.tintedDefault {
  color: #ffffff;
}

By using a CSS class, any element that’s declared as tinted will automatically receive tints. For example, the title:

  <div class="title tinted tintedDefault">Calico</div>

Next I need to dynamically add a CSS rule. I’m using Kotlin/JS so it’s easy to hook up the adoptedStyleSheets API:

private var adoptedStyleSheet: CSSStyleSheet? = null
  set(value) {
    field = value
    val array = js("[]")
    if (value != null) {
      array.push(value)
    }
    document.asDynamic().adoptedStyleSheets = array
  }

To call it, I build a stylesheet rule from a string:

val colorOrDefault = color ?: Colors.DefaultThemeColor
val cssStyleSheet: CSSStyleSheet = js("""new CSSStyleSheet()""")
cssStyleSheet.insertRule(
  rule = """
    .tinted {
      color: ${colorOrDefault.css()} !important;
      opacity: 1 !important;
    }
    """,
  index = 0,
)
adoptedStyleSheet = cssStyleSheet

Using !important is important here; it ensures the dynamic tint takes precedent over the default one.

I remove the tint after a 1,500 ms delay:

resetJob = scope.launch {
  delay(duration)
  adoptedStyleSheet = null
}

That’s enough to tint the text, but I need another trick to tint the SVG icons. The easiest way I found to recolor an SVG file was a CSS mask. Thankfully my icons are single-color.

<div 
  class="imageButton tintedBackground tintedBackgroundDefault"
  style="mask: url('/assets/bottle40x64.svg'); width: 40px; height: 64px">
</div>

Annoyingly, the dynamic color for masks is the background, so I need a second pair of CSS rules:

.tintedBackground {
  transition: background-color 300ms linear;
}

.tintedBackgroundDefault {
  background-color: #ffffff;
}

And a second dynamic CSS rule:

cssStyleSheet.insertRule(
  rule = """
    .tintedBackground {
      background-color: ${colorOrDefault.css()} !important;
      opacity: 1 !important;
    }
    """,
  index = 1,
)

The most difficult part of the whole exercise is using restraint. I’m inclined to color everything in bright colors all the time, but that yields a very ugly UI!

Rounds is my free web app for scoring in-person games. Try it out at rounds.app.