Styling-API Reinvented

by Michal Raczkowski - 12 Jul 2018

Decoupled styling in UI components

Styling isolation

Styling isolation achieved via CSS-modules, various CSS-in-JS solutions or Shadow-DOM simulation is already a commonly used and embraced pattern. This important step in CSS evolution was really necessary for UI components to be used with more confidence. No more global scope causing name conflicts and CSS leaking in and out! The entire component across HTML/JS/CSS is encapsulated.

Styling API - exploration

I expect CSS technology to offer much more in the future. The encapsulation usually comes hand in hand with the interface, for accessing what was hidden in an organised way. There are different ways to provide styling-APIs, for customising the component CSS from the outside.

One of the simplest methods is to support modifiers; flags for the component, used to change appearance, behavior or state:

<MyComponent type="large" />

This is convenient if there are a few predefined modifiers. But what if the number of different use cases grows? The number of modifiers could easily go off the scale if we combined many factors, especially for non-enum values like "width" or "height".

Instead we could expose separate properties that provide a certain level of flexibility:

<MyComponent color="red" border="2" borderColor="black" />

In such cases different modifiers can simply be constructed by users of the component. But what if the number of CSS properties is large? This solution also doesn't scale nicely. Another con is that any modification of the component's styles will likely force us to change the API as well.

Another solution is to expose the class that will be attached to the root element (let’s assume it's not a global class and proper CSS isolation technique is in place):

<MyComponent className="my-component-position" />

Attaching a class from the outside will effectively overwrite the root element CSS. This is very convenient for positioning the component, with such CSS properties as: "position," "top," "left," "z-index," "width," and "flex.” Positioning of the component is rarely the responsibility of the component itself. In most cases it is expected to be provided from outside. This solution is very convenient and more flexible than former proposals. But it’s limited to setting the CSS only for the component's root element.

The combination of the above solutions would likely allow us to address many use cases, but is not perfect, especially for component libraries, where simple, generic and consistent API is very important.

Decoupled styling

I'd like to take a step back and rethink the whole idea of styling-API for components. The native HTML elements come with minimal CSS, enough to make the elements usable. The users are expected to style them themselves. We are not talking about "customisation", as there is basically no inherent styling in place to "customise". Users inject styling, via a “class” attribute or “className” property:

<button class="fashion-store-button" />

In latest browsers like Chrome, we can also set the styling for more complex HTML5 elements like video elements:

<video class="fashion-store-video" />

.fashion-store-video::-webkit-media-controls-panel {
 background-color: white;
}

Thanks to Shadow DOM and webkit-pseudo-elements users can set the styles not only for the root element, but also for important inner parts of the video component. However webkit pseudo-elements are poorly documented and seem to be unstable. It’s even worse for custom elements, because currently it’s not possible to customise the inner parts of elements (::shadow and /deep/ have been deprecated). However, there are other proposals that will likely fill the gap:

Let's summarise the native approach, which I call "decoupled styling":

  1. A component is responsible only for its functionality (and API) and comes with minimal or no styling
  2. A component styling is expected to be injected from outside
  3. There is styling-API in place to style the inner parts


Benefits

The nature of styling is change, the nature of functionality (and API) is stability. It makes perfect sense to decouple both. Decoupled styling actually solves many issues that UI-component library developers and users are facing:

  • styling couples components together
  • same changelog for styling and functionality/API causes upgrading issues (e.g. forced migrations)
  • limited resilience - changes in styling propagate to all parts of the frontend project
  • costs of rewriting components to implement a new design
  • costs of rewriting/abandoning projects, because of outdated components
  • limitations of styling-API to address different use cases
  • bottleneck of component library constantly adjusted for different use cases


API proposal

In the world of custom UI components, many components are constructed from other components. Contrary to native HTML/CSS implementation with injecting a single class name, here we need API for accessing the nested components. Let’s look at the following proposal for the API.

Imagine a “Dialog” component that contains two instances of a “Button” component (“OK” and “Cancel” buttons). The Dialog component wants to set the styling for OK button but leave the styling for the Cancel button unchanged (default):

<Button classes={icon: "button-icon", text: "button-text"}>OK</Button>
<Button>Cancel</Button>

We used “classes” property to inject the CSS classes for two of Button’s internal elements; the icon and the text elements. All properties are optional. It’s up to component itself to define its styling-API (set of class names referencing their child elements).

To use Dialog with its default, minimal styling:

<Dialog />

But for cases where we want to adjust the styles, we will inject it:

<Dialog classes={root: "dialog-position"} />

We injected a class that will be attached to the root element. But we can do much more:

<Dialog classes={
 root: "dialog-position",
 okButton: {
   icon: "dialog-ok-button-icon",
   text: "dialog-ok-button-text"
 }
} />

The example above shows how we can access every level of nested components structure in the Dialog. We’ve set the CSS classes for the root element and OK button. By doing that we will effectively overwrite the styling for the OK button, that is preset inside Dialog.

In the same way we will be able to set the styling for components that contain Dialogs, and farther up, to the highest level of the application. On the root level of the application, defining the styles will practically mean defining the application theme.


Implementation

I implemented two examples using React and TypeScript, first with CSS Modules and second with Emotion (CSS-in-JS library). Both are based on the same concept:

  • default, minimal styling for components is predefined as an isolated set of classes
  • styling-API (set of class names) is defined using TypeScript interface, with all properties optional
  • components allow injection of class names object (via “classes” parameter) which is “deeply-merged” with default class names object, overwriting the styles

React, TypeScript, CSS Modules: https://github.com/mrac/decoupled-styling-css-modules
React, TypeScript, Emotion: https://github.com/mrac/decoupled-styling-css-in-js

Conclusion

Decoupling styling from UI components may be a step towards making them really reusable, drawing from the original idea behind Cascade Style Sheets to separate the presentation layer from UI logic and markup. Defining boundaries between UI logic and markup on one side and styling on the other side would likely change the way UX designers collaborate with engineers.  Here designers would style components based on API provided by engineers. It would be easier now to specify what constitutes a breaking-change within that contract. Putting an ever-changing skin on top of what is stable would likely save costs, friction and contribute to software quality.

Similar blog posts