DEV Community

A. Sharif
A. Sharif

Posted on

Declarative Style Definitions Pt.1

** Note: This is a first try at understanding the topic better. Not an expert in styling or design.
The following write-up is a collection of ideas from people that have been thinking about the topic for a very long time:
Brent Jackson, Adam Morse, Michael Chan, Jon Gold, Sunil Pai and many more.
**

When building UIs, we mostly have a good grip of the very initial requirements. We choose tools and concepts around these requirements and implement a UI resembling the initial definition. But overtime these requirements start to evolve. More features or changes to the initial definition require changing parts of the application, sometimes even more than just small refactorings and rewrites.
Most of the UI work tends to be a repetition of existing solutions but with explicit requirements that don't align with already existing. Often meaning starting from scratch, every time we start building a new application.
These problems might come from the way we approach UI work, mostly in an imperative style, where a declarative style might be more suited for the applications we are focused on building.

"With our current tools we’re telling the computer how to design the vision we have in our head (by tapping on our input devices for every element on the screen); in our future tools we will tell our computers what we want to see, and let them figure out how to move elements around to get there."
˘

Declarative Design Tools, Jon Gold

A declarative approach suits our need to optimize for change well. One interesting aspect is that we don't want to write more style definitions, what we want is being able to compose existing definitions in way, that anyone can use them. Adding more styles should be the exception.

"When I read about or listen to ideas on how to scale an app’s css - most of the talk is about how to write css. The real way to scale css, is to stop writing css."

CSS and Scalability, Adam Morse

So what tools and ideas do we have available to us, to making optimizing for change a feasible task?

To get a better understanding, let's take a look a Tachyons, a functional css library written by Adam Morse. By reading the class names we can understand what the styles should do. By changing a class name, we can ensure that we are not breaking another component somewhere else in the application.

This approach gives us a good idea of why composition is a better way of handling styling complexity.

With React and its "thinking in components" approach we gained another powerful concept. What if everything was a component? Even a page would just be a tree of components. React comes with composition built into the system. This is the only way you can build an application in React. It might sound counterintuitive when coming from a templates approach, but what we gain is freeing our components from the DOM and other side-effects.
We declare what we want rendered, and React takes of care of the underlaying low-level manipulations. We don't need to manually handle DOM manipulations. We might not even be rendering to a DOM in the first place.

By being able to build isolated components, that don't depend on any context, we can move these components around, like small building blocks.

This is how we already build UIs.

These paradigm helps us to optimize for change.

Let's talk about a missing aspect. Being able to declare our styling in a declarative manner.

We already talked about Tachyons, which operates on the class level, but what if could hide away the fact and enable styling on a component level?

Declarative Approach

Let's think about what a declarative styling approach might look like for a minute.
Take the following example, where we define some styling constructs and then pipe them to a specified output. This output could be a DOM element, but also a React or Preact element.

const style = (key, value) => style => `${style}${key}:${value};`;
const applyStyle = element => style => {
  element.setAttribute("style", style);
};

pipe(
  style("color", "red"),
  style("width", "4rem"),
  style("height", "2rem"),
  applyStyle(document.getElementById("main"))
);
Enter fullscreen mode Exit fullscreen mode

We can do same thing with class names or another construct. What we're mainly interested in, is being able to compose our styles as needed. In Tachyons we already have a one to one mapping between class name and style. This enables us to compose these class names at the element level. This is very useful.

There is more we can leverage when decoupling our styling definition from a specific format.
For example we can leverage types and existing language constructs.

"If you’re writing React, you have access to a more powerful styling construct than CSS class names. You have components."

“Scale” FUD and Style Components, Michael Chan

Example

It's time to see what we can gain from being able to define styles on a component level?

"Abstracting styles into components gives us a single place to change our implementation across the app."
“Scale” FUD and Style Components, Michael Chan

Let's take a look at an example. We need to display a user profile containing name, title, a short description as well as an image. Our first approach is to define a Profile component, that can show the required information for a given user.

const Profile = ({user}) => (
  <article className="mw5 center bg-white br3 pa3 pa4-ns mv3 ba b--black-10">
    <div className="tc">
      <img src={user.image} className="br-100 h4 w4 dib ba b--black-05 pa2" title={`${user.firstName} ${user.lastName}`}>
      <h1 className="f3 mb2">`${user.firstName} ${user.lastName}`</h1>
      <h2 className="f5 fw4 gray mt0">{user.role}</h2>
      <hr className="mw3 bb bw1 b--black-10">
    </div>
    <p className="lh-copy measure center f6 black-70">
      {user.description}
    </p>
  </article>
)
Enter fullscreen mode Exit fullscreen mode

The above component is based on the Tachyons cards examples. Developers can now use the the defined Profile component and integrate it into any part of the application and as needed.

<Profile user={user} />;
Enter fullscreen mode Exit fullscreen mode

The style definition is encapsulated inside the component, leaving out the possibility to adapt the look and feel. Just by looking at our component, we can see that it is very complicated to change any styling definitions, as class names are used everywhere. We can already identify some refactoring opportunities. Again, our Profile can be split up into more components. Let's start with article, which could be an Card component.

type CardProps = {
  bg: string;
  borderColor: string;
  maxWidth: number;
  children: Array<JSX.Element>;
};

const Card = ({
  bg = "white",
  borderColor = "black",
  maxWidth = 5,
  children
}: CardProps) => (
  <article
    className={`mw${maxWidth} center bg-${bg} br3 pa3 pa4-ns mv3 ba b--${borderColor}-10`}
  >
    {children}
  </article>
);
Enter fullscreen mode Exit fullscreen mode

Not only did we extract the Card component, we also defined the possibility to change the look and feel in a controlled manner. We might enable to change the background- or border-color or the max-width. As component developers, we can ensure what can be changed and how. Our Card component has become independent of the Profile component. We are free to use the Card component when building a product card instead and adapt the look and feel when suited.

While we can further break up our components, we can also improve the overall experience by adding types. Let's get back to the Card component. By adding type definitions, we can ensure than only possible values are passed in.

type MaxWidth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | "-100" | "none";
Enter fullscreen mode Exit fullscreen mode

Although it might look like a boring task, we can improve the developer experience and ensure that only correct values are passed in. We can also use propTypes when working with React, but this approach is more applicable beyond a specific framework or library.

Being able to offer type definitions is definitely another side-effect we gain by thinking in components.

But there is even more we can do to improve the overall experience and prevent incorrect style definitions from the start. We can type all possible styles, thereby enabling developers to leverage autocomplete and quick feedback cycles, f.e. receiving error messages when an incorrect value is being passed in.

enum BackgroundColor {
  White,
  Gray,
  // add all possible colors
}
Enter fullscreen mode Exit fullscreen mode

This enables us to enforce constraints on our styling.

type CardProps = {
  bg: BackgroundColor;
  borderColor: BorderColor;
  maxWidth: MaxWidth;
  children: Array<JSX.Element>;
};
Enter fullscreen mode Exit fullscreen mode

Now we can ensure that only possible types are passed in, we also control what can be overridden, while also providing defaults for using the component out of the box.

We mostly need a subset of components, and can compose more components by combining these subsets.
For example Rebass offers 8 components, that can be used as building blocks to build further components by combining these blocks. The 8 components are: Box, Flex, Text, Heading, Link, Button, Image and Card. Rebass uses styled-system for defining the styles, which follows a constraint based approach.

Once we have the low level building blocks, we can start to use them to build more specific components. We might be able to build our Profile by composing these low level building blocks.

In part 2 we will get more specific and build a fully working example taking all these ideas and see in more detail what we gain by making the low level mechanics an implementation detail.

If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Top comments (0)