Add a style utility API

Created on 3 April 2025, 19 days ago

Problem/Motivation

After SDC in Drupal 10.1 (July 2023), and the Icon API in Drupal 11.1 (Dec 2025), let’s continue to implement design systems API in Core, in order to be able to build business agnostic, shareable, Drupal themes, providing design implementations which can be leveraged by display building tools.

Styles utilities are a common artefact of design systems. Examples:

Each style is a set of mutually exclusive, self-descriptive, single-purpose, universal, CSS classes. Examples: Typography, orders, Colors, Spacing, Elevation....

Analysis of the current solutions in the contrib space

Like SDC and Icon API, we believe it must be front-dev friendly, UI logic focused, YAML plugin declaration available in Drupal theme. So, let's have a look on contrib modules are already covering this scope.

UI Styles (usage: 800)

https://www.drupal.org/project/ui_styles

Discovery: {provider}.ui_styles.yml in modules and themes.

Minimal example (Bootstrap 5 background color utility):

background_color:
  label: Background color
  options:
    bg-primary: Primary
    bg-secondary: Secondary
    bg-success: Success
    bg-danger: Danger
    bg-warning: Warning
    bg-info: Info

The option key is the CSS class.

With some metadata:

background_color:
  label: Background color
  description: Similar to the contextual text color classes, set the background of an element to any contextual class.
  options:
    bg-primary:
      label: Primary
      description: The color displayed most frequently across your app's screens and components. 
    bg-secondary: Secondary
    bg-success: Success
    bg-danger: Danger
    bg-warning: Warning
    bg-info: Info

Personal opinion:

I am maintainer of this module and I participated to define this format, so I may be biased, but I am confident about this format which has already been battle-tested with many design systems implementations and display building tools (layout builder views, page layout, ckeditor5, theme settings...), successfully. And the team is currently working on advanced issues like Support styles which are not HTML classes Active .

Style options (usage: 1700)

https://www.drupal.org/project/style_options

More precisely, the CssClass plugin, because this module do a bit more than style utilities.

They are not really Drupal plugins, but it uses YAML discovery anyway: {provider}.style_options.yml in modules and themes.

Example:

 background_color:
    plugin: css_class
    label: Background color
    multiple: false
    required: true
    default: 1
    options:
      - label: Primary
        class: bg-primary
      - label: Secondary
        class: bg-secondary
      - label: Success
        class: bg-success
      - label: Danger
        class: bg-danger
      - label: Warning
        class: bg-warning
      - label: Info
        class: bg-info

Careful: default value is an integer index with this syntax.

Same example with the keyed syntax:

 background_color:
    plugin: css_class
    label: Background color
    multiple: false
    required: true
    default: primary
    options:
      primary:
        label: Primary
        class: bg-primary
      secondary:
        label: Secondary
        class: bg-secondary
      success:
        label: Success
        class: bg-success
      danger:
        label: Danger
        class: bg-danger
      warning:
        label: Warning
        class: bg-warning
      info:
        label: Info
        class: bg-info

Personal opinion:

Very similar to UI styles, but with a syntax a bit more complicated. Maybe because this module do more than style utilities. Also, do we really need multiple and required key?

Block Style Plugins (usage: 400)

https://www.drupal.org/project/block_style_plugins

Discovery: {provider}.blockstyle.yml in modules and themes.

Example:

colors:
  label: Colors
  form:
    background_color:
      '#type': 'select'
      '#title': 'Background color'
      '#options':
        bg-primary: Primary
        bg-secondary: Secondary
        bg-success: Success
        bg-danger: Danger
        bg-warning: Warning
        bg-info: Info

Note: The first level is a group of styles, the second level (in form) are styles.

Personal opinion:

Maybe too complex and "drupally" for front-dev with this explicit usage of the Form API in the YAML.

Layout Builder style (usage: 23K)

https://www.drupal.org/project/layout_builder_styles

A bit out of scope because styles are config entities instead of plugins, and because it works only with Layout Builder. But it is a popular module so let’s have a look.

There are 2 config entity types:

  • One config entity by group (so by “utility”)
  • And one config entity by style in the group (so by “option”)

Example (with usual config entities properties removed)

id: background_color
label: 'Background color'
multiselect: single
form_type: checkboxes
required: false
id: primary
label: Primary
classes: bg-primary
type: component
group: background_color
block_restrictions: {  }
layout_restrictions: {  }
id: secondary
label: Secondary
classes: bg-secondary
type: component
group: background_color
block_restrictions: {  }
layout_restrictions: {  }

Proposed resolution

Definition & discovery

Based on the analysis below, with some discussions:

  • Required? Multiple? I am afraid we are losing the point of style utilities by introducing those.
  • Support styles which are not HTML classes Active
  • Do we also add metadata for the previews in library pages like UI Styles is doing? Or do we let contrib modules do their own stuff?
  • ...

In the renderer service

Once contrib or custom modules will leverage this API, they can add styles classes in $build["#attributes"]["class"].

This is causing a few issues:

  • The syntax is verbose and error prone
  • Styles classes are mixed with other classes
  • There is no possibility to add checks about the existence of a style option, or the mutual exclusivity of style options.

So, it would be better to introduce #styles universal property, which can be added to every renderables already accepting an #attributes property:

  • ['#type' => 'html_tag']
  • ['#type' => 'component']
  • Most of #theme and most of render elements

This is excluding #markup, #plain_text and maybe some #theme and some render elements.

So the renderer service to process this:

  if (isset($elements['#styles'])) {
	$elements["#attributes"] = AttributeHelper::mergeCollections(
  	$elements["#attributes"],
  	[
    	  'class' => $elements['#styles']
  	]
	);
	unset($elements['#styles']);
  }

Do we also add checks about the existence of a style option, or the mutual exclusivity of style options, here?

This is a big move to a new Render API based on design systems concept. This #styles render property will fit well alongside ['#type' => “component”] and [“#type” => “icon”] renderables.

Remaining tasks

Let's start by contributing the maintainer of the contrib modules to ask them if they want to participate.

User interface changes

No

Introduced terminology

"Style", "Utility", "Option"... The terminology used in this issue summary is challengeable.

API changes

No, only additions.

Data model changes

No.

Feature request
Status

Active

Version

11.0 🔥

Component

theme system

Created by

🇫🇷France pdureau Paris

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @pdureau
  • 🇬🇧United Kingdom catch

    How does this integrate (or not) with asset loading?

    I've been trying to get rid of as much of the system/base library as possible, and this has exposed various more or less undocumented 'utility classes' in core that have been around for years - clearfix, container-inline etc. that are loaded on every page despite in some cases only being used on a single or handful of admin pages in core.

    Would the styles plugins also come with a .css file, or something else?

  • 🇫🇷France Grimreaper France 🇫🇷

    Hi,

    @catch, this is something I have been thinking about too to avoid loading unused CSS on all pages.

    - Add Bootswatch themes Active
    - 📌 Starterkit: split Bootstrap CSS per component Active

    Like with UI Icons or UI Skins, adding a "library" key to a style declaration could be possible. Then is a style of this declaration is used, the library could be automatically attached.

    Problem would be in some "free" areas not connected to UI Styles like WYSIWYG, or link class attributes, (same problem regarding components BTW), to detect automatically that a class is used then search for matching style and so library... This can quickly become tricky. Especially if sometimes the same CSS class is declared in multiple style declarations.

    Funny that you mention "clearfix, container-inline", those classes are also in Bootstrap, and in UI Suite Bootstrap I have removed them:

    libraries-override:
      ...
      system/base:
        css:
          component:
            css/components/clearfix.module.css: false
            css/components/container-inline.module.css: false
            ...
    

    And the case you mentioned, would this mean that Core should have declared libraries with one small CSS file only and properly attach only when needed?

  • 🇬🇧United Kingdom catch

    @grimreaper I've been slowly doing that in 📌 Refactor system/base library Needs work although mostly for larger files used in less places. However pretty much all the use cases even for these tiny utility declarations are on admin pages so overall I think we should be doing the same.

    We're approaching the point where the standard profile + stark loads about 1kb of CSS on a page instead of 7kb a year ago, this could be zero if we keep going. It opens up possibilities like inlining CSS for anonymous users without massive customisation etc. But ofc the last 1kb is the hardest - although stable9 means it's not a BC break for themes using that as a base.

  • 🇫🇷France Grimreaper France 🇫🇷
Production build 0.71.5 2024