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:
- scalable - scoping CSS to particular components in one way or another is a must. We don’t want our styles to leak from widget to widget, especially if they are done by separate teams.
- flexible - our styling methodology should allow us to implement arbitrary designs made by the UI/UX team. One size fits all frameworks like Bootstrap or Bulma won’t cut it here.
- co-located - You might not like React, but back in 2013 it showed us that separation of concerns in the context of web development could mean different things, and the classic approach of having three files:
.js
,.html
,.css
for the same widget might not be the best idea for scalability and productivity. For our app, we want to have styling information kept close to the rest of the view logic. - performant - as much as possible should be done on build / compile time. Our Scala.js app with all of these wonderful libraries mentioned above could be heavy as it is. It is however a tradeoff that we are willing to make in this scenario, additional sec of initial load for better maintainability in the long term. This although doesn’t mean that we should make the situation worse by choosing a client-side heavy approach for CSS.
- unified - lastly, our approach to styling should ideally create one, unified design language and be enforced by setup. No more different interpretations of BEM(https://getbem.com/) rules.
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:
- For a pure frontend app, this setup requires you to run your Scala.js with
scalaJSUseMainModuleInitializer := true
and a task that is run after every compilation step. Although this sounds scarier than it actually is, since in practice you would need to only definesbt
command alias likeaddCommandAlias("dev", "~ fastLinkJS; frontend / run")
, in the end, it feels cumbersome. - Even though the styles are being extracted at Scala compile-time, they are left untouched in the compiled JS output since they are actually used in the runtime (the
frontend / run
from above.) This could lead to larger-than-needed JS bundles, and that’s a no-no. The solution to that was another custom piece of code, this time a Vite plugin which was removing the unnecessary code from compiled sources.
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:
- The styling logic is close to view logic. You don’t have to jump to other files to change styles. You don’t even need to import anything.
- Tailwind classes are atomic, each one is responsible for one thing. Instead of applying many style rules to a single class, you apply many classes to a single element. With this approach, you obtain style encapsulation out of the box.
- Since Tailwind classes are small, equivalent to single CSS rules, you can use them to code arbitrary designs. No more Bootstrap buttons that all look the same.
- It’s a unified and customizable design language that forces consistency. For example, there are a lot of ways to handle media queries and responsive design in your CSS, but Tailwind has already made that choice for you. A combination of
mt-4 md:mt-8
will give you amargin-top: 1rem
on mobile andmargin-top: 2rem
on bigger screens everywhere in your app, so a mobile-first approach. It’s all customizable, but good defaults are already there. - It’s not a new craze, the work on it started around 2014, but it’s still popular and has a thriving community. Heck, there are even Tailwind conferences. It all means that If you’ll encounter some issue, then there is a big chance that someone before you was in the same spot already and there are solutions.
Ok, what are the drawbacks?
- It looks weird. You might have a hard time convincing your colleagues (and yourself) to it
- It requires some learning and it can’t be escaped! Fortunately, Tailwind docs are pretty good, there is a playground in which you can quickly try out ideas. Supposedly, there is also a helpful VS Code extension that makes the learning process smoother, but it doesn’t work with Scala 🥲
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.