Skip to content
GitHub

Scala.js & CSS

TL;DR - just keep calm & use Tailwind.

Even though Scala is quite popular when it comes to languages in which you can unleash the full power of typed functional programming, it’s still a niche. Scala.js is a niche within a niche. It’s no surprise that there are not a lot of guides on how to approach CSS in Scala.js web apps.

Time to address that.


Let’s imagine the following scenario - you want to build a big and complex SPA web application heavy on domain logic. It will be opened a few times during the day and then used for many hours. It may be even packed into Electron / Tauri app. You want it to be scalable, functional and type-safe. You are fine with a slightly bigger bundle size. You already have a Scala backend and want to share some validation logic and codecs.

In that scenario, Scala.js seems like an ideal candidate.

You even have chosen your stack already. It will be a Tyrian app, powered by Cats-Effect, fs-dom and Circe. For bundling you’ll use Vite. You start building, life is great. 🤗

But soon you discover that you need to put something on the screen, and one more technical decision is needed.

What about CSS? 🙀

There are a lot of ways one could approach the CSS, in Scala.js or not.

Since we are building a new, big SPA application we want the styles to be:

Now let us walk through a couple of options.

ScalaCSS

First, let’s address the elephant in the room and talk about ScalaCSS.

ScalaCSS is an interesting and battle-tested Scala library for dealing with CSS in a type-safe matter. It brings the idea of CSS-in-JS that I’m fond of to Scala. Unfortunately, it does all of that in runtime, rather than compile-time, pushing more work on the client side, and for me, that’s a deal-breaker.

Moreover, I’m not a fan of its DSL for CSS. Type safety should allow you to bring more correctness to your program, but what does it mean to be correct when it comes to CSS? I’d argue it means that you correctly interpreted the visual requirements of your design doc in terms of HTML & CSS, and this could be only tested by the actual browser. Bespoke low-level, DSLs won’t help you here, they’ll only slow you down.

Looks like the ScalaCSS author, jagpolly has the same opinion, in his own words:

The idea of a Scala-based DSL for CSS has turned out to be a failure in my opinion

UdashCSS

If you are not afraid of ScalaCSS DSL but want to have better performance you might want to look at UdashCSS, which is a part of a full-stack Scala web framework called Udash.

It works by defining a CSS stylesheet renderer that collects styles from all over your codebase and outputs it as a vanilla CSS stylesheet through sbt task. Your styles have to be defined in shared folder for cross-compilation and the JS side only gets generated class names.

This approach requires a little more setup, but in the context of an opinionated framework, it makes sense.

Customized setup

What about a custom library then, how hard could it be? 🫠

So I started working on this thing. I thought that I could use a setup similar to UdashCSS, but without the ScalaCSS DSL and API that allows for easier co-location of styles with view logic.

The library, in combination with Tyrian could be used as follows:

object Component extends Styled:
  val color = "red"

  val header = css"""
    background-color: $color;
    margin-top: 2rem;
  """
  
  val paragraph = css"""
    padding: 1rem;
  """

  def view = div()(
    header(cls := header.className),
    p(cls := paragraph.className)("Lorem ipsum")
  )

And then, at the root of your program, you’d need to have:

@main
def run =
  JSRender.toFiles(
    directory = "frontend/styles",
    Component
  )

css is a macro that generates a unique class name for each call at compile-time. This guarantees us proper styles isolation. Styled is just a stylesheet scope, that collects results from all css invocations for further processing, and at the end, you have JSRender.toFiles which renders the styles to stylesheet(s). You can see it in action here.

This looks nice, but has some drawbacks:

Given the above, at this point, I felt that the whole setup is a little bit too fragile and experimental to use it in production. Ideally, we would be able to partially evaluate the Scala code at the compile step and get rid of all CSS strings in one go, probably through some compiler plugin. In the JS land, there are already compile-time CSS-in-JS libs like this such as Linaria. They are usually built on top of Babel, an established JS compiler, which huge ecosystem and good docs. This is the area in which Scala 3 needs to catch up.

The Others

Let’s quickly talk about some other alternatives.

CSS modules

CSS modules are an interesting approach with Scala.js and Vite out of the box… until you run tests. Node.js, which is used to run the Scala.js tests, just doesn’t understand the CSS imports like import classNames from './styles.css'. Of course, you can still run your tests through Vite with some headless browser setup, but in my opinion, it’s not worth it. 🤷

Old-school pre/postprocessors

Vanilla SCSS / LESS / PostCSS & BEM - All of those tools are mature, established and offer a nice DX. If you are disciplined about your BEM usage and don’t mind the lack of style & view logic co-location then I cannot recommend them enough. However, for a big Scala.js SPA described at the beginning of this article, this didn’t sound like a good fit.

Shadow DOM

So what about Web Components and Shadow DOM? In short, using it in raw form, in virtual DOM frameworks like Tyrian is a little bit inconvenient. You’d need to manipulate underlying DOM elements managed by virtual DOM, and every integration would need to be framework-specific. Still, this is definitely possible. React itself has many wrappers like react-shadow-root. For virtual-DOM-less libraries like Laminar or Calico it should be even easier. On a side note, lit(https://lit.dev/), a Web Components framework uses an interesting combination of Shadow DOM with runtime style generation, but with a new, more performant Constructable Stylesheets API. It could be an interesting side-quest to research Shadow DOM & Scala.js topic further, but at the moment I didn’t feel like starting another one…

Are we doomed, is there no pragmatic, battle-tested and well-supported approach to CSS for Scala.js SPAs?

Tailwind

Tailwind is a CSS library that looks ridiculous.

I mean, just look at it:

<div class="grid gap-4 col-start-1 col-end-3 row-start-1 sm:mb-6 sm:grid-cols-4 lg:gap-6 lg:col-start-2 lg:row-end-6 lg:row-span-6 lg:mb-0">
  <img src="/beach-house.jpg" alt="" class="w-full h-60 object-cover rounded-lg sm:h-52 sm:col-span-2 lg:col-span-full" loading="lazy">
  <img src="/beach-house-interior-1.jpg" alt="" class="hidden w-full h-52 object-cover rounded-lg sm:block sm:col-span-2 md:col-span-1 lg:row-start-2 lg:col-span-2 lg:h-32" loading="lazy">
  <img src="/beach-house-interior-2.jpg" alt="" class="hidden w-full h-52 object-cover rounded-lg md:block lg:row-start-2 lg:col-span-2 lg:h-32" loading="lazy">
</div>

Isn’t that just an unmaintainable mess?

For the longest time I thought so myself, but let’s look at the benefits:

Ok, what are the drawbacks?

A short Tailwind & Scala.js & Vite tutorial

Assuming that you already have a working Scala.js & Vite setup, adding a Tailwind there is pretty straightforward.

What you need to do is simply run:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

The last line will generate postcss.config.js and tailwind.config.js config files.

Tailwind works by scanning your sources for used classes and produces only the CSS which is actually used. This helps to reduce the stylesheet file size but also means that you need to manually modify content in the tailwind.config.js to include your compiled JS outputs and other sources like index.html which might contain Tailwind classes.

It might look like this:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./app/target/**/app-*/com.yourorg*.js",
    "./app/index.html",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

This spell of ./app/target/**/app-*/com.yourorg*.js tells Tailwind to look only for CSS classes inside the target directory in files starting with com.yourorg. This should force Tailwind to scan only JS files compiler from your Scala sources, not your dependencies.

Lastly, to actually include Tailwind styles, somewhere in your root index.css you need to add these:

@tailwind base;
@tailwind components;
@tailwind utilities;

More about those, and other Tailwind directives here.

If you are fortunate enough, this should be the last time you are touching a .css file in your project. 🎉

Tailwind & Scala 3 inlining

As mentioned above, Tailwind works by scanning your sources for CSS class names. It means that if you are creating your class names dynamically, like in:

val color = "blue"
val cls = s"bg-$color-500 hover:bg-$color-700"

then if that was the only usage of the above classes, Tailwind won’t be able to include it in the final CSS output during a build.

Turns out, a nice Scala 3 feature called inline might help us here.

By changing the above, to:

inline val color = "blue"
inline def cls = s"bg-$color-500 hover:bg-$color-700"

we guarantee that the string of bg-blue-500 hover:bg-blue-700 will be available in the compiled JS, which in turn would allow Tailwind to catch it as well!

You might not need it often, but it’s nice that when you need it’s possible and no weird workarounds like in JS/TS are needed.


All in all, Tailwind might not be the silver bullet but I think It’s worth a try. In Scala, or in other languages. And in Scala.js SPA land, at the moment of writing, it surprisingly might even be the most pragmatic option for authoring CSS.