A different approach to CSS-in-JS...

If you're a web developer, and you've already used some JS frameworks (especially React), then you might be familiar with the concept of CSS-in-JS. Basically, it all boils down to creating your CSS stylesheets through JavaScript, rather than usual CSS. It's somewhat better than solutions like SCSS, because of the continuous access to all the JS goodness that it gives you. It also simplifies the management of your CSS styles and the general development experience (DX) as a whole.

Now, let's remind ourselves TypeScript - JS superset with static type system included. This one further improves DX through additional tooling, suggestions, and type-safety. So, the question should be asked - what would happen, if we mix CSS-in-JS and TypeScript together? Well, I'll tell you - TypeStyle will happen! So, bear with me for this one, as we're going to discover, what kind of goodness can such a combination provides us with, if it's worth your effort and how to use it!

The idea

First, let's take a step back and discuss exactly why anyone would ever mix TS with CSS-in-JS concept. Here, the answer is simply - because why not!? Like really, CSS-in-JS is just a general idea, obviously connected with CSS and JS, while TS is just a JS superset with easy access to all of its underlying features. That's why there's no point of not doing such a thing.

Going even further, it's the possible benefits of such a mix that make it even more interesting! CSS-in-JS concept and libraries implementing it, all aim at making CSS more... "maintainable". As you may know, many of them achieve it in different ways. Ones allow you to define your CSS classes in the form of objects, other - in form of the template literal, and some make the whole stuff even more complex by providing a Babel plugin. Don't get me wrong - all these approaches are good, depending on your use-case of course. But, they also have some more disadvantages...

One thing that almost all these libraries lack is type-safety. Of course, I mean TypeScript. Most of them are written in plain JavaScript, with only some partially-complete external typings. Such state may be the result of how hard creating a suitable, statically-typed API can be, especially for JS representation of CSS. There's just too many CSS properties and specialized rules (like @media) to do that. Yet, still - we can try!

TypeStyle

So, what is TypeStyle? By now you obviously know - it's a CSS-in-JS library, written in TypeScript. Its main goal is to make CSS maintainable and type-safe. With that said, it also comes with some pretty neat features built-in.

What differentiates TypeStyle from quite a lot of CSS-in-JS libs is that it's runtime-only. By using all CSS-related APIs (I discussed these in my previous post), it simply creates all your stylesheets with JavaScript, instead of doing any preprocessing. In this way, TypeStyle is super "portable". Because of its runtime-based model and small size (~6 KB min-zipped), you can just swap it in and you're ready to go!

The library is also framework-independent. Because of that, TypeStyle tries to mirror CSS design to a much higher degree than some libraries. Of course, this comes with some possible "drawbacks" to some, like - most notably - no auto-prefixing and other post-CSS stuff.

Of course, the biggest feature of TypeStyle is its typings. The API does a great job of allowing TS-powered autocompletion and code hints features. Maybe CSS will never be 100% type-safe, but the library does a good job of taking what we have available today to whole another level.

Basics

So, with some reasoning and introduction behind us, let's dive right into a small overview of TypeStyle API. Keep in mind that it's not really a big library, and its documentation already does its best of explaining all the stuff. With that said, go check it out, if you want to know more.

npm install typestyle

CSS classes

The most basic use of TypeStyle involves creating simple CSS classes.

import { style } from "typestyle";

const className = style({
    backgroundColor: "red",
    width: 100,
    height: 100
});

By using style() function, we're creating a new CSS class, which we can later access by the returned, hashed class name. The provided config object can be treated just like any other. This includes destructuring, Object.assign() and other cool stuff. You can do similar stuff just by supplying any number of config objects to style() function.

import { style, types } from "typestyle";

const rect: types.NestedCSSProperties = {
    width: 100,
    height: 100
};

const className = style({
    backgroundColor: "red",
    ...rect
}); // or style({backgroundColor: "red"}, rect);

The use of such patterns will result in losing the type-safety and TS support in all "components" of our style config. If you're using TS and don't want that to happen, you can specify the type for your object directly, with some help from TypeStyle-provided types, just like in the example above.

Nesting

The basic TS support for style()-like functions is present throughout multiple other CSS-in-JS libraries. What sets TypeStyle apart is the level of this integration. A great example of that is the way that TypeStyle handles pseudo-classes. Take a look:

// ...
const className = style({
    backgroundColor: "red",
    ...rect,
    $nest: {
        "&:hover": {
            backgroundColor: "green"
        }
    }
});

The library requires special nested property - $nest - in order to supply style config for different pseudo-classes and stuff. This allows TypeScript to inference proper type, and thus, provide all support it can for widely-known pseudo-classes. The $nest property can also be used for normal nested selectors. Although, keep in mind that such usage leaves you with no TS support, and a class with nested selectors that's somewhat hard to manage in most CSS-in-JS scenarios.

Helpers

Generally, the style() function is all there is to TypeStyle. It's both simple and intuitive. The rest of the library basically builds upon this functionality, with additional helper functions and other useful tools.

Media queries

Most notable examples of such helpers include media() function, used for type-safe media queries.

import { style, media } from "typestyle";
// ...
const className = style(
    rect,
    media({minWidth:0,maxWidth:600}, {backgroundColor: "red"}),
    media({minWidth:601}, {backgroundColor: "green"}),
);

The media() function is a mixin, outputting a normal style config. You can think of it as a nice replacement for $nest property.

// ...
const className = style(
    rect,
    $nest: {
        "@media only screen and (max-width: 600px)": {
            backgroundColor: "red"
        },
        // ...
    }
);

Pretty nice, huh? The $nest property might still be required for some advanced use-cases. Remember that, because we're working in JS/TS, you can always create your own mixins, in order to give some structure and look to your main style config.

Animations

Just like media queries, CSS keyframe animations are an equally "special" feature, that may be hard to use in CSS-in-JS. For that TypeStyle, again, provides nice helper function - keyframes().

import { style, keyframes } from "typestyle";
// ...
const animationName = keyframes({
  '0%': { color: 'red' },
  '100%': { color: 'green' }
})

const className = style({
    ...rect,
    animationName: animationName,
    animationDuration: '2s',
});

The function returns new, hashed name of created animation for your later use. It's this kind of intuitiveness that made me really like this library.

Concatenation

Finally, if you work with React or simple className property for that matter, you might enjoy classes() helper. It simply concatenates all supplied class names and returns the result.

import { classes } from "typestyle";
// ...
const classStr = classes(className, className2);

Raw CSS

So, as you can see from the examples above, TypeStyle provides a nice, but small set of helper functions. Like really - how much can you pack in 6 KB library? Anyway, the point is that the library doesn't provide helpers for everything. This is something that you can easily create yourself if you like, using mixins, component objects and etc.
You might guess by now that TypeStyle applies all its classes and stuff into a single stylesheet (single <style/> tag), that's created with the help of some CSS-related Web APIs. It's an important detail to remember when using TypeStyle's raw CSS functions - cssRule() and cssRaw().

import { cssRule, cssRaw } from "typestyle";
// ...
cssRule(".red-rect", {
  ...rect
  backgroundColor: "red"
});

cssRaw(`
.green-rect {
  height: 100px;
  width: 100px;
  background-color: green;
}
`);

I don't think that these functions need a deep explanation. First allows you to create a CSS rule with a custom string selector, which is still somewhat type-safe. cssRaw(), on the other hand, should be used only for loading CSS libraries, and even then - you might be better with a normal, external CSS file. It provides no type-safety at all!

Of course, such functions are extremely useful - especially when you want all your CSS to be written in CSS-in-JS fashion. Such functions can be used with e.g. @import rule, where the placement matters. That's why it's so important to understand that TypeStyle works on a single stylesheet, as for such use-cases, you should use cssRaw() before any other CSS-related call, to place your custom rule at the top of the stylesheet.

SSR

I previously mentioned that TypeStyle is runtime-only. This means that it doesn't base on any kind of Babel plugin and stuff at all by default. If you want to argue that it's not the best decision performance-wise, then think again. The performance loss is left unnoticed (for me, at least), and you just really shouldn't trade performance with maintainability. But, if you don't want to change your mind, there's another way.

TypeStyle has built-in support for Server-Side Rendering (SSR) and static page generation. Because of the use of a single stylesheet, TypeStyle provides an easy-to-use function - getStyles() - to extract all of its rules.

import { style, getStyles } from "typestyle";
// ...
const className = style({
  backgroundColor: "red"
  ...rect,
});

getStyles();
/* Example result:
hashed-class-name {
    height: 100px;
    width: 100px;
    background-color: red
}
*/

By using the getStyles() function, you can easily use all of the TypeStyle features - including hashed CSS class names, without any (even the tiniest) loss in performance. Just put the result of this call into <style/> tag of your template file, and you're ready to go! Of course, if you know how it's done, you can even create your very own Babel plugin for that very easily (most likely).

There's more!

As I don't want this post to be a documentation, rather than a simple, beginner-friendly tutorial, we'll stop here. There's still some interesting features and gotchas noted in the official docs. If you're interested in this library, I highly recommend reading the docs - they're impressively well-written! But, even so, with the set of features you learned about in this article, you should easily be able to represent most of your CSS in a type-safe, maintainable and expressive way.

Thoughts?

So, what do you think of TypeStyle? Do you like this somewhat different approach to CSS-in-JS that it represents? Let me know down in the comments section below. Also, if you like the article, consider leaving a reaction, a comment or a suggestion for future posts. For more up-to-date content, follow me on Twitter, my Facebook page or through the weekly newsletter. I hope you enjoyed this one, and have a great day!