/scalawind

Scalawind - Zero-Runtime Typesafe TailwindCSS in Scala

Primary LanguageJavaScriptMIT LicenseMIT

Write typesafe Tailwindcss with Scalawind


Scalawind is a Zero-Runtime Typesafe Tailwindcss in Scala

scalawind-demo.mp4

Features

  • ⚡️ Write faster with Fluent API
  • 🚀 Thanks Scala 3 macros, we can completely eliminate runtime cost. Only strings present in the final compiled code. No function calls. No overhead.
  • 🦄 Works with every UI libraries in ScalaJS ecosystem
  • 🎨 Customizable via user's tailwind config
  • 💪 Typesafe tailwindcss classes and autocomplete (via codegen)

Supported Tailwind Features

  • Normal, like flex items-center justify-center
  • Arbitrary values, like bg-[#de3423] text-[#380d09] h-[100px]
  • Normal modifiers, like hover:bg-red-500 or md:text-xs
  • Important modifiers, like !text-red-500
  • Color opacity, like bg-blue-500/25 or bg-black/[.05]
  • Raw, an escape hatch to pass in raw tailwind utility classes
  • Arbitrary variants, like [&:nth-child(3)]:text-red-500
  • Negative values, like -top-1
  • Percentage values, like w-1/2
  • Dot values, like w-1.5

You can have a quick check for how far Scalawind can contruct Tailwind classes by looking at the test cases at: scalawind.test.scala

Scalawind - Zero-Runtime Typesafe TailwindCSS in Scala

Although this library is still very early, the actual code is just a single file generated and output directly into your project. You can twist it, fix it, make changes to it however you like.

Integration Testing

The idea is very simple, you can generate typesafe scala code from tailwind config file and use tailwind utility classes in a typesafe way.

import scalawind.*

val styles: String = tw.bg_black.text_white.hover(tw.bg_white.text_black)

// ↓ ↓ ↓ ↓ ↓ ↓

val styles: String = "bg-black text-white hover:bg-white hover:text-black

We use fluent syntax to type our tailwind classes. These classes will be compiled at compile-time so there's no runtime cost for this.

If you're using Laminar or Scalajs-React, you should use the -f flag while generating scalawind, which accepts values: laminar, scalajs-react or both, it will generate some implicit conversion codes which will allow you to use scalawind directly in your UI code:

div(
  tw.flex.items_center.justify_center,
  div(
    tw.text_red_500.bg_black,
    "Hello world"
  ),
)

Quickstart

You can use degit to clone the vite example that's already setup everything for you to get started.

$ npx degit nguyenyou/scalawind/examples/vite-app my-scalawind-app
quickstart.mp4

There many examples for you to choose, you can take about at the examples folder. Pick one and replace EXAMPLE_NAME_HERE following the syntax:

$ npx degit nguyenyou/scalawind/examples/EXAMPLE_NAME_HERE my-scalawind-app

How to use

Install the CLI, using any node package manager that you prefer:

$ npm install scalawind --save-dev

Then, add the postinstall script to your package.json to make sure the code will automatically run after install:

"scripts": {
  "postinstall": "scalawind generate",
}

After generating, you will have a scalawind.scala file at the root of your project. You can move it to your preferred location and rename the package however you like.

Then, import scalawind.* and you're ready to go.

import scalawind.*

button(
  cls := tw.bg_blue_500
          .hover(tw.bg_blue_600).first_letter(tw.text_red_500.font_bold)
          .text_white.rounded.py_3.px_4.md(tw.py_4.px_5)
          .dark(tw.bg_sky_900.hover(tw.bg_sky_800)).css,
  "Click me"
)

// ↓ ↓ ↓ ↓ ↓ ↓

<button class="bg-blue-500 hover:bg-blue-600 first-letter:text-red-500 first-letter:font-bold text-white rounded py-3 px-4 md:py-4 md:px-5 dark:bg-sky-900 dark:hover:bg-sky-800">
  Click Me
</button>

That's it.

Customize Generated Code

The Scalawind CLI supports -o to specify the output path and -p to specify the generated package name. For example:

$ scalawind generate -o ./src/main/scala/myapp/scalawind.scala -p scalawind

Show Compiled Class On Mouse Hover

You can turn on the show compiled class on mouse hover feature by adding the --preview-compiled-result or -pcr to the command. For example:

$ scalawind generate -pcr -o ./src/main/scala/myapp/scalawind.scala 

Normal Usage

Fluent API

Scalawind uses Fluent Syntax which can help us type faster and still benefit from type safety.

tw.bg_blue_500.text_white.rounded.py_3.px_4

// ↓ ↓ ↓ ↓ ↓ ↓

"bg-blue-500 text-white rounded py-3 px-4"

Negative value

To use classes which start with negative values like -left-1, just replace - with _ underscore. In this case, you can type: tw._left_1

Percentage value

For utilities like w-1/2, we have to call the method in backticks. Example usage:

val styles: String = tw.`w_1/2`

// ↓ ↓ ↓ ↓ ↓ ↓

val styles: String = "w-1/2"

Dot value

For utilities like w-1.5, we have to call the method in backticks. Example usage:

val styles: String = tw.`w_1.5`

// ↓ ↓ ↓ ↓ ↓ ↓

val styles: String = "w-1.5"

Color opacity

You can write color opacity class, by writing the color class name follow by $ dollar sign and invoke function call with a opacity value in number:

val styles: String = tw.text_red_500$("25").bg_black$("[.05]")

// ↓ ↓ ↓ ↓ ↓ ↓

val styles: String = "text-red-500/25 bg-black/[.05]"

Modifiers

Ordering stacked modifiers

tw.dark(tw.groupHover(tw.focus(tw.bg_black)))

// ↓ ↓ ↓ ↓ ↓ ↓

"dark:group-hover:focus:bg-black"

This behavior is the same as tailwindcss ordering stacked modifiers behavior

Important modifier

To specify a class to be important, you can wrap it inside the tw.important() modifier.

button(cls := tw.important(tw.text_black).hover(tw.important(tw.text_blue_700)).css, "Click me")

// ↓ ↓ ↓ ↓ ↓ ↓

<button class="!text-black hover:!text-blue-700">Click me</button>

Arbitrary values

This feature is not stable yet, it works but very limited

We have support for arbitrary values with quite similar signature, instead of wrapping your arbitrary value in square brackets, you now use function call. For example:

val styles: String = tw.bg_("#bada55").text_("22px")

// ↓ ↓ ↓ ↓ ↓ ↓

val styles: String = "bg-[#bada55] text-[22px]"

Arbitrary variants

We have support for arbitrary variants feature.

Arbitrary variants are like arbitrary values but for doing on-the-fly selector modification, like you can with built-in pseudo-class variants like hover:{utility} or responsive variants like md:{utility} but using square bracket notation directly in your HTML.

val styles: String = tw.variant("&:nth-child(3)", tw.text_red_500.bg_black)

// ↓ ↓ ↓ ↓ ↓ ↓

val styles: String = "[&:nth-child(3)]:text-red-500 [&:nth-child(3)]:bg-black"

Escape Hatches

There're cases you need some Tailwind classes that Scalawind currently doesn't support, you can use the raw method to directly write the utilities that you need, for example:

tw.raw("some-very-special-class")

Of course, this method can be chain in the fluent style like any other methods:

val styles = tw.text_black.bg_white.hover(tw.raw("text-white bg-black")).css

// ↓ ↓ ↓ ↓ ↓ ↓

val styles = "text-black bg-white hover:text-white hover:bg-black"

Classes Validation

Check Duplication

Passing the flag -cd or --check-duplication to enable this feature.

When writing a long list of utility classes, it's sometime necessary to check if we accidentally duplicate our class, with class validation feature enabled, we check it for you:

Screenshot 2024-06-08 at 13 41 44

Usage Optimization

Passing the flag -co or --check-optimization to enable this feature.

In Tailwind, we have margin and padding classes that can be used in three different fashions:

  • One-direction: mt-2, mb-2, ml-2 and mr-2
  • Two-directions: my-2 and mx-2
  • Four-directions: m-2

It makes sense that we provide a check for efficient usage, such as, we should use m-2 instead of combination of my-2 and mx-2 or we should use mx-2 instead of combination of mr-2 and ml-2.

Screenshot 2024-06-08 at 13 40 57

Advanced Usage

Implicit Conversion to Laminar Modifier

Using the -f laminar flag when generating scalawind code, it will allow you code like this:

div(
  cls := tw.text_red_500.bg_black.css,
  "Hello, world"
)

// ↓ ↓ ↓ ↓ ↓ ↓

div(
  tw.text_red_500.bg_black,
  "Hello, world"
)

Yes! You can omit the cls := and .css parts.

Use -f both will generate code for both Laminar and Scalajs-React code.

Implicit Conversion to Scalajs-React TagMode

Using the -f scalajs-react flag when generating scalawind code, it will allow you code like this:

Then, you can write like this:

<.div(
  ^.cls := tw.text_red_500.bg_black.css,
  "Hello, world"
)

// ↓ ↓ ↓ ↓ ↓ ↓

<.div(
  tw.text_red_500.bg_black,
  "Hello, world"
)

Yes! You can omit the ^.cls := and .css parts.

Use -f both will generate code for both Laminar and Scalajs-React code.

Slinky

In slinky, we can skip the css method, like this:

className := tw.flex.items_center.justify_center

Reducing Generated Code Size

Colors

By default, TailwindCSS includes all of their colorset which make the generated scala code has to cover all the usages of these colors.

You can pick some of them to use by overriding the config, like this:

const colors = require("tailwindcss/colors");

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: {
    files: ["./index.html", "./scalajs-modules/**/*.js"],
  },
  theme: {
    colors: {
      transparent: "transparent",
      current: "currentColor",
      black: colors.black,
      white: colors.white,
      red: colors.red,
    },
  },
};

Core Plugins

TailwindCSS by default includes all their core plugins for you, this will cause the generated scala code has to cover all the core plugins, you can pick only the plugins that you use:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: {
    files: ['./index.html', './scalajs-modules/**/*.js'],
  },
  corePlugins: [
    'display',
    'textColor',
    'width',
    'height',
    'alignItems',
    'justifyContent'
  ]
};

Acknowledgement

This project is inspired by https://github.com/mokshit06/typewind. Thank you a lot for making the library.

License

MIT License © 2024-Present You Nguyen