Designing a new style system to replace CSS

UIScript DevBlog, Part 3

Bence Meszaros
5 min readMay 21, 2024
Front end development

Intro

Last week I introduced some basic Views in UIScript and discussed my implementation of a view tree that would eliminate the need for HTML in websites/web apps.

This week our victim is CSS.

CSS-in-JavaScript

To put it simply, UIScript is a CSS-in-JavaScript solution. Similar to how the DOM API can be used to migrate the functionality of HTML into JavaScript we can do the same with CSS. I have two reasons for this.

First, using proper programming concepts such as variables, operators, conditionals, loops and functions is a much more powerful way to style and build layouts. With only a handful of tools we can achieve virtually anything we want. These tools are ubiquitous to the point where even CSS began to introduce them, although in a subpar way. This begs the question: why bother when JavaScript already had these tools right from the start and with a much better implementation?

Second, several concepts in CSS are badly designed making it very hard to understand, learn and use them in a productive way. This abstraction would present an opportunity to redesign how styling and layout building works. For this, I usually use four categories:

  • concepts that are good enough to keep as is
  • concepts that are good but need to be renamed
  • concepts that are bad and should be redesigned
  • concepts that are bad but we cannot redesign them

So let’s get started.

The fugly way

To kick things off we will violate the “best” practice of not targeting Views directly. I find it quite amusing how desperately CSS is trying to avoid this, even though this is the most intuitive thing to do. Here are some of our current options:

  • using inline styles:
<p style="font-family: Helvetica; color: black; opacity: 0.5">Hello World</p>
  • using external styles:
<p id="foo">Hello World</p>
#foo {
font-family: Helvetica;
color: black;
opacity: 0.5
}
  • using styles in JavaScript:
<p id="foo">Hello World</p>
const foo = document.getElementById("foo");
foo.style.fontFamily = "Helvetica";
foo.style.color = "black";
foo.style.opacity = 0.5;

It is actually pretty impressive how something this simple can look this bad. This is literally the most basic thing anyone will try when learning front end development and you are wondering why CSS has a steep learning curve.

The main problems with the current browser API

Setting styles in JavaScript can happen in two ways: we can set individual properties or we can add/remove classes from our Views. Creating classes on the fly is not very pretty so I wanted to focus on the first option.

Since our Views are HTMLElements, we can already access and set their style properties. This is the basic option in JavaScript but it has several issues that I don’t really like:

  • style properties are properties of the style object and not the View itself which adds boilerplate and cumbersome to use
  • several property names and property values are bad, inconsistent or nonsensical
  • type juggling and unit handling is pretty bad even compared to the dynamic nature of JavaScript

So simply adding style properties to our Views wasn’t going to work, I wanted a style system that could solve the rest of the issues as well.

//this isn't much better
const text = Text("style me");
text.fontFamily = "Helvetica";
text.color = "black";
text.opacity = 0.5;

Getter/setter

Another common approach is to use getter/setter methods to process values before getting or setting them. We can achieve this in several ways:

  • using accessor descriptors with regular properties
  • using Proxy objects
  • using getter/setter method pairs

All of these options have significant downsides: using regular properties is still verbose, adding a Proxy seems overkill (and wrapping HTML elements is something I definitely want to avoid for now anyway) and defining and using every single property with two methods is bloated. I wanted something much better, much more compact and of course compatible with my new view hierarchy.

The beauty is the beast

It took some experimenting and iteration but I’ve settled on a combined getter/setter method syntax. Here is the same example using UIScript:

Text("Hello World")
.fontFamily("Helvetica")
.fontColor("black")
.opacity(0.5);

This approach has several benefits. It is short, concise and uniform, it can handle busywork under the hood and it is compatible with our declarative view tree using method chaining.

Each property is defined using a single, combined getter/setter method that acts as a setter if there is an argument and a getter if there is none. This is usually done by function overloading which is not supported in JavaScript but we can fake it with a simple conditional that checks the number of arguments inside our methods.

The beauty is that setters can also return a value, specifically the View itself, enabling method chaining.

So instead of this:

const text = Text("Hello World");
text.setFontFamily("Helvetica");
text.getFontFamily();

we can do this:

Text("Hello World")
.fontFamily("Helvetica")
.fontFamily(); //gets the current font family

These methods can also have better names than their CSS counterparts, better values, better value types and even better arguments (better number and/or better order of arguments) to further increase code clarity.

This is also valid JavaScript, so there is absolutely no need to compile, transpile, build or otherwise modify this code before running it in a browser.

Pedal to the metal

Just like with our view tree, as we move over to a scripting language we unlock virtually endless new possibilities. Just to give you a sneak peek here are some ideas of what UIScript can do:

  • use proper variables and constants to store values
const color = "darkgray";

Stack(
Text("Hello World")
.fontColor(color),

Text("Hola mundo")
.fontColor(color)
);
  • use operators to evaluate values
let darkMode = true;

Text("Hello World")
.fontColor(darkMode ? "white" : "black");
  • reference properties on any Views
const catImage = Image("cat.jpeg")
.width(100)
.height(300);

const dogImage = Image("dog.jpeg")
.width(catImage.width())
.height(catImage.height());
  • use callbacks to define even more complex logic
Image("hero.jpeg")
.width(() => {
if (device === "mobile") {
return 400;
}

if (device === "tablet") {
return 600;
}

if (device === "desktop") {
return 1200;
}
});
  • use custom classes to create components and centralize style management
function Button(label) {
return Text(label)
.width("content")
.height("content")
.padding(12, 40, 12, 40)
.cornerRadius(50)
.backgroundColor("lightblue");
}

Stack(
Button("Log in"),
Button("Subscribe")
);
  • use State values (aka. Signals) to achieve reactive programming
Image("welcome.png")
.width(viewport.width) //recalculated whenever viewport.width changes
.height(viewport.height); //recalculated whenever viewport.height changes

These are just a few ideas that are already available in UIScript and there are many more to come and even more that I am experimenting with.

That’s a wrap

There are many more features I want to show you but for now I think this is enough food for thought. In the coming weeks I want to show you how UIScript can simplify layout building, how it can handle interactivity, how I experiment with different State/Signal implementations, and how I fixed several additional HTML and CSS concepts to make front end development much better.

So stay tuned and if you like UIScript please consider clapping, commenting and sharing my stuff with others.

Thanks and have a great week.

⬅️ Part 2, <rip>HTML</rip>

--

--

Bence Meszaros
Bence Meszaros

Written by Bence Meszaros

Lead Software Engineer, Fillun & Decketts

Responses (4)