More writings

HTML Interfaces For CSS Libraries

I’m using the term “CSS library” to refer to a repository that exists to provide styles for components. Perhaps “component CSS library” would be more accurate? This article is basically a grab bag of loosely held opinions presented as suggestions.

One of the big ideas behind pulling in a CSS library for your components is that the consumer of the library should be able to configure presentation of a component without touching CSS. The developer should be able to name the component configuration they want in HTML and the styling should just work. Let’s talk about some ways that API could be made more intuitive and easier to manage.

Avoid using the class attribute as API

Component configuration from the perspective of a CSS library typically consists of things like component name, variant, size, etc. These are effectively key-value pairs but I often see this type of API being set via the class attribute.

<!-- Don't do this -->
<button class="
  button--default
  button--standard
  button--medium
">

Using the class attribute for this has a number of issues:

  1. All of it is jammed into a single HTML attribute so can be difficult to differentiate one config from another.
  2. There is no indication of which config each value is setting. What are “default”, “standard”, “medium” referring to? The developer might not know unless they looked at the documentation for the library and even then it might not be obvious.
  3. No clear way to know what library is being used here. If a developer is new to maintaining a project using this CSS library, it may be difficult for them to know where to begin when looking for documentation.

A better approach is to use data-* attributes.

<!-- Do this -->
<button
  data-ln-variant="default"
  data-ln-treatment="standard"
  data-ln-size="medium"
>

Above example uses ln short for “Library Name” as a sample library specific namespace. A project like Spectrum CSS might use sp, Tailwind tw etc.

Using data-* attributes for CSS library APIs makes it clear what each config refers to because there is a clear delineation between key and value.

Use library specific namespacing

Consider adding a brief namespace unique to the library so if someone were to paste any of these API names into a search engine they would have a better chance of quickly finding a docs page for the library. This also helps to avoid potential collisions with existing code.

<!-- Don't do this -->
<button data-variant="large">Launch</button>
/* library styles */
[data-variant="large"] {
  /* These styles might collide if the project
     importing this library already styles
     a `data-variant` attribute. */
}

In the above example, if a project already styles data-variant, or imports another CSS library that does, that would collide with this library’s styles.

<!-- Do this -->
<button data-ln-variant="large">Launch</button>
/* library styles */
[data-ln-variant="large"] {
  /* The library namespacing helps ensure
     that only this CSS library will style
     this attribute. */
}

Requiring a short library-specific sigil on the custom data attributes used by the CSS library reduces the risk of code conflicts and makes the code easier to follow.

Decide if the library will style tag names or only attributes

You may be thinking that putting the data- prefix in front of all these attributes is pretty verbose. That prefix is required for custom attributes on standard HTML but not for custom HTML elements.

<!-- data prefix needed -->
<button data-ln-variant="large">Engage</button>

<!-- data prefix NOT needed -->
<button-ln variant-ln="large">
  <button>Engage</button>
</button-ln>

Above example uses ln short for “Library Name” as a sample library specific namespace. A project like Spectrum CSS might use sp, Tailwind tw etc.

While requiring a custom element for every component can be useful for consistency purposes, it will require additional complexity to do that. In the example above, the native <button> element is still desired for accessibility which makes the component a little more complex.

<!-- Native button element hidden in Shadow DOM -->
<button-ln variant-ln="large">Engage</button-ln>

<!-- Shadow DOM declaration -->
<button-ln>
  <template shadowrootmode="open">
<!-- focus management galore -->
    <button><slot></slot></button>
  </template>
</button-ln>

It is worth noting that using data-* attributes gives access to a special dataset API in JavaScript. Not of much use for a CSS library, but something to consider if that would be useful for configuring functionality in JavaScript.

Whether it’s better for a CSS library to style data-* attributes or custom elements with custom attributes may depend on the library’s goals.

  • If the CSS library exists only to provide styling for a single component library, requiring a custom tag name for every component and custom attributes for every configuration instead of using data-* attributes seems like a better fit.

  • If the CSS library exists to be more general and support multiple component libraries, using the data-* approach and never styling tag names directly seems like a better fit.

Consider making namespacing secondary to API name

Think about the developer user experience of interacting with this library’s API in the web inspector. Ideally information which is repeated should be secondary to information that is unique. To achieve that goal, consider putting API name first and namespace second.

<!-- The eye reads the less significant info first -->
<ln-popover
  ln-variant="error"
  ln-treatment="dark"
  ln-size="large"
>

</ln-popover>

<!-- The eye reads the more significant info first  -->
<popover-ln
  variant-ln="error"
  treatment-ln="dark"
  size-ln="large"
>

</popover-ln>

Above example uses ln short for “Library Name” as a sample library specific namespace. A project like Spectrum CSS might use sp, Tailwind tw etc.

Wrapping up

There’s a lot more to say on this topic, but I’m going to end here for now. Please feel free to reach out via the links in the footer. I’d love to continue the discussion!