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: 880)

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 #3517009: POC: Core styles API.

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.
  • #3517009: POC: Core styles API
  • 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 contacting the maintainer of the contrib modules to ask them if they want to participate.

TODO:
- finish event subscriber for html/body
- add helper/refactor how to apply styles. Put logic into Style definition to avoid duplication between renderer and Attribute?
- item_attributes? When adding a style, add optional parameter to specify attribute key?
- ensure big_pipe compatibility? not sure if relevant that a style alters body/html attributes asynchronously after page had been rendered.
- drop short syntax for options.
- description of option.
- add lifecycle/deprecation properties on style and/or on style options
- remove item_attributes
- apply in Core: search where Core could declare style utilities with dedicated library and apply it.
- support of prefers-color-scheme or light-dark()??? No news or reaction, postponed

To test:
- no more option short syntax
- description on option
- target property is now an enum
- enums
- StyleDefinition applyOrBubbleOnAttribute
- StyleDefinition applyOnAttribute
- StyleDefinition applyOrBubbleOnArray
- Attribute object addStyle and changes
- Attribute helper changes
- bubbleablemetadata change
- Renderer changes

User interface changes

No. API Only.

Introduced terminology

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

API changes

No, only additions.
Change record: https://www.drupal.org/node/3586264
Need complete examples in documentation pages.

Data model changes

No.

Issue fork drupal-3517033

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

pdureau created an issue. See original summary.

pdureau’s picture

Title: Add a style utilty API » Add a style utility API
catch’s picture

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?

grimreaper’s picture

Hi,

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

- #3478113: [5.2.x] Add Bootswatch themes
- #3372792: Starterkit: split Bootstrap CSS per component

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?

catch’s picture

@grimreaper I've been slowly doing that in #2880237: [meta] Refactor system/base library 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.

grimreaper’s picture

Assigned: Unassigned » grimreaper
grimreaper’s picture

@catch, Thanks! This looks very nice to go down to this few CSS by default!

pdureau’s picture

Let's start with a MR inspired from UI Styles (as a first POC, we are still open to other projects point-of-views).

Proposed scope:

  • Plugin definition (more flexible, to cover exotic notations or with the library key proposed by @catch for example)
  • PluginManager with YAML discovery
  • #styles "universal" render property (available on any renderable with and #attributes render property)
  • Something to replace the "drilling" because we have now access to the RenderAPI
  • A minimal form element with select & checkboxes

Out of scope:

  • Style library pages
  • StylesheetController, StyleSheetGenerator with Saberworm dependency
  • The full FormElement with Widget/Source Plugins
  • Metadata in plugin definition related to library pages or the full widget (preview_with, icon....)
  • Integration with Drupal API (ckeditor, layouts, blocks...)
  • No extra
grimreaper’s picture

Current Style definition in UI Styles:

  protected array $definition = [
    'id' => '',
    'enabled' => TRUE,
    'label' => '',
    'description' => '',
    'links' => [],
    'category' => '',
    'options' => [],
    'empty_option' => '- None -',
    'previewed_with' => [],
    'previewed_as' => 'inside',
    'icon' => '',
    'weight' => 0,
    'additional' => [],
    'provider' => '',
  ];

Style definition proposal:

  protected array $definition = [
    'id' => '',
    'enabled' => TRUE,
    'label' => '',
    'description' => '',
    'links' => [],
    'category' => '',
    'options' => [],
    'weight' => 0, // to keep?
    'library' => '',
    'class' => '', // for #3469785: Add HTML class property for styles?
    'additional' => [],
    'provider' => '',
  ];

#3469785: Add HTML class property for styles?
Need to address: #3517009: POC: Core styles API:

'target' => 'body'/'attributes'/'foo',
'key' => 'class',

like in https://git.drupalcode.org/project/ui_skins/-/blob/1.1.x/src/Definition/... ?

pdureau’s picture

The contrib modules studied in the description have been reached:

pdureau’s picture

Because our proposal is also covering theme/mode switching, let's also support of prefers-color-scheme or light-dark()

pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
pdureau’s picture

So, we are heading to a "Style API" covering both style utilities and theme/mode switching, in order to:

  • Implement those 2 parts of design system with a single API, because it appears they share the exact same definition format
  • Allow local application of a theme/mode, useful when a design system allow dark mode or specific branding yo be applied in a specific part of the page
  • #3517009: POC: Core styles API

So, we need to think about the way of applying a style globally. What do we do? We extend #attached render property with a styles keyword? We extends our own #styles render property? We add an other universal render property?

Let's check what is decided on #3531854: Add a Design Tokens & CSS variables API to be consistent.

pdureau’s picture

Today discussion with @grtimreaper.

YAML definition format

For style utilities

In {provider}.utilities.yml, an example of a style utility with different options:

size:
  label: "Text size"
  # kind: utility/theme. Magically added by the discovery
  attribute: class # Overridable at the option level.
  options: 
    small: 
       label: Small
      value: 'text-sm'
    medium:
      label: Medium
      attribute: style
      value: 'font-size: 15px'
    large:
       label: "Large"
      attribute: cds-text
      # value is optional when the same as the option key
  # library: "" # A SDC like asset library declaration

Notes:

  • class is attribute default value
  • option value is optional when the same as the option key

For themes/modes

In {provider}.modes.yml, the same but we have a concept of "global" style:

default:
  label: Light/Dark
  # kind: utility/theme. Magically added by the discovery
  attribute: data-fr-scheme # overridable at option level
  target: html # overridable at option level. if present, the mode is global only. Not available for styles.
  options:
     light:
        label: "Light"
       value: light
     dark: "Dark" # Shorthand if value == option key
  # library: '' # A SDC like asset library declaration
  
mourning:
  label: "Mourning"
  attribute: data-fr-mourning
  target: html
  options:
    mourning:
      label: Mourning
      value: ""

Use in Render API

A #styles render property with an associative array where:

  • keys are style plugins IDs as string
  • values are style option value as sting

No need to extend #attached render property with a styles keyword.

Example:

[
  '#type' => 'component',
  '#component' => 'foo:card',
  '#styles' => [
    'size' => 'small',
  ],
  '#slots' => [],
]

Do we prefix plugin IDs with provider Id like SDC ?

[
  '#type' => 'component',
  '#component' => 'foo:card',
  '#styles' => [
    'foo:size' => 'small',
  ],
  '#slots' => [],
]

Rendering

Property Syle plugin level Option level Example Rendered result
attribute No No
size:
  label: "Text size"
  options: 
    small:
       label: Small
       value: 'text-sm'
class="text-sm" because class is the default attribute
attribute Yes No
size:
  label: "Text size"
  attribute: foo
  options: 
    small:
       label: Small
       value: 'text-sm'
foo="text-sm"
attribute No Yes
size:
  label: "Text size"
  options: 
    small:
       label: Small
        attribute: bar
       value: 'text-sm'
bar="text-sm"
attribute Yes Yes
size:
  label: "Text size"
  attribute: foo
  options: 
    small:
       label: Small
        attribute: bar
       value: 'text-sm'
bar="text-sm" because option level wins
value No No
size:
  label: "Text size"
  options: 
    small:
       label: Small
        attribute: bar
bar="small" because we take the option's key by default
value Yes No
size:
  label: "Text size"
  value: "hello"
  options: 
    small:
       label: Small
       attribute: bar
bar="small hello" because we add both the style value and the option key
value No Yes
size:
  label: "Text size"
  options: 
    small:
       label: Small
       value: "baz"
class="baz" because we have only the option value
value Yes Yes
size:
  label: "Text size"
  value: bar
  options: 
    small:
       label: Small
       value: "baz"
class="bar baz" because we add both values
grimreaper’s picture

Do we prefix plugin IDs with provider Id like SDC ?

In this case it would require to put in place a "replaces" mechanism like SDC too?

Currently override (in sub themes) of plugin ID are used to disable style plugin as a whole or to add/remove options.

Should it be handled like SDC only allowing adding new options to props, new props/slots but not removing ones?

This impact the subject of value validation.

I would say let's not namespace to allow override. but when processing if a style plugin is disabled (we have not put this key in the proposition by the way..) or option is missing, ignore and log.

grimreaper’s picture

Discussed with @pdureau, like with Icon API and SDC.

Try to validate style definition against JSON schema.

grimreaper’s picture

For attribute object core/lib/Drupal/Core/Template/Attribute.php, add a "addStyle" method.

grimreaper’s picture

Status: Active » Needs work
grimreaper’s picture

Discussed with @pdureau, splitting into modes.yml and utilities.yml, would require to create some additional YamlDiscovery classes to inject dynamilcally the "kind" also there is a risk of plugin ID collision and we want to avoid namespacing so let's have only one file.

TODO:
- Merge YAML files into styles.yml
- kind default to utility, can be manually set to kind.
- JSON schema: only allow utility and kind value for this property.
- Plugin manager provides method to get only plugins by a certain kind.
- Plugin manager getDefinitionsForTheme not tested yet.

- Form element
- Renderer to introduce #styles
- Attribute: add addStyle method.

grimreaper’s picture

TODO:
- Merge YAML files into styles.yml
- kind default to utility, can be manually set to mode.
- JSON schema: only allow utility and kind value for this property.
- Plugin manager provides method to get only plugins by a certain kind.
- Plugin manager getDefinitionsForTheme not tested yet.
- Form element
- Renderer to introduce #styles
- Attribute: add addStyle method.

grimreaper’s picture

TODO:
- Plugin manager getDefinitionsForTheme not tested yet.
- Renderer to introduce #styles
- Attribute: add addStyle method.

grimreaper’s picture

In UI Styles, sometimes there is "#item_attributes" instead of "#attributes" for image, responsive image.

  public const array THEME_WITH_ITEM_ATTRIBUTES = [
    'image_formatter',
    'responsive_image_formatter',
  ];

Should we handle that too?

grimreaper’s picture

I don't think it will be possible to add a method to attribute object because it know nothing about libraries.

So I think we will need to add a Twig function:

{% set attributes = addStyle(attributes, 'my_style', 'my_value') %}

So that it can alter the attributes object, but I am not sure that mechanism like attach_library could be triggered:

  public function attachLibrary($library) {
    assert(is_string($library), 'Argument must be a string.');

    // Use Renderer::render() on a temporary render array to get additional
    // bubbleable metadata on the render stack.
    $template_attached = ['#attached' => ['library' => [$library]]];
    $this->renderer->render($template_attached);
  }
grimreaper’s picture

TODO:
- Plugin manager getDefinitionsForTheme not tested yet.
- Update test target enum
- Test Attribute
- Test Attribute helper
- special bubbleable/attached for html/body?
- check target for mode only.
- put logic into Style definition to avoid duplication between renderer and Attribute + tests
- item_attributes?

I don't think we're going to get around the HTML preprocess, in fact, we won't be able to.
The core has a preprocess_html in app/core/lib/Drupal/Core/Theme/ThemePreprocess.php

Except we go through it before going through app/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php, which handles attachments.

So even introducing a new type of #attached wouldn't work.

Or, we could create an event subscriber like core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php
which will manipulate the response's HTML and access the response's attachments.

grimreaper’s picture

Before forgetting, discussion with @pdureau, for modes, light and dark is special because tight with browser and OS features of light/dark.

We should try to handle that too. So that it can prepare to provide a live theme switcher block like on design system documentation pages.

pdureau’s picture

Before forgetting, discussion with @pdureau, for modes, light and dark is special because tight with browser and OS features of light/dark.

Indeed, let's also support of prefers-color-scheme or light-dark()

grimreaper’s picture

utility: use attribute only
If mode: look at target.

Twig attributes: new method addStyle
- add attribute:
-- empty target: OK
-- meta:
-- html/body:
- attached library: OK

render array:
- add attribute:
-- empty target: OK
-- meta: OK
-- html/body: Ok via attached.
- attached library: OK

Preprocess callback: #styles not usable.
- add attribute:
-- empty target: ?
-- meta: ?
-- html/body: ?
- attached library: ?

new attached key: styles:
- target:
-- attribute: value

$element['#attached']['styles'] = [
  'html' => [
    'class' => [
      'foo',
    ],
  ],
  'body' => [
    'data-bs-theme' => 'light',
  ],
];

OR

- target:
-- plugin_id: plugin_option

$element['#attached']['styles'] = [
  'html' => [
    'colors_background_color' => 'bg_primary',
  ],
  'body' => [
    'color_mode' => 'light',
  ],
];

With second option, possible during resolution to make usage of Attributes object so we can benefit from AttributeHelper::matchCollectionsTypes

UI Skins:
- preprocess HTML to set global theme: to replace with event subscriber executed before the one in Core handling html_attributes and body attributes, so able to set attachments.
- theme form settings alter: to put in UI Styles as now modes will be styles managed.
-- 2 styles form elements: one for html, one for body. Need to filter out styles by target (not just kind)
-- that way possible to handle #3485599: Handle styles on body tag

UI Styles Block:
- preprocess block: need to make it work preprocess callback

UI Styles CKE5:
- need to pass to a system of custom attributes to able to create a filter plugin that will be able to add attributes and attach library.
- on-the-fly style generation wrapped in .ck-content may be simplified to only load the library of the enabled style plugins. No more need to load all the libraries with parent theme recursively.

UI Styles Entity Status:
- hook entity view: should be unaffected or worst case, fall into same as preprocess.

UI Styles Layout Builder:
- preprocess block: need to make it work preprocess callback
- entity view alter: (to convert into hook entity view?)
- event subscriber

UI Styles Page:
- preprocess region: need to make it work preprocess callback

UI Styles UI Patterns:
- handling Attribute object: so should be ok

UI Styles Views:
- hook_preprocess_views_view: need to make it work preprocess callback

grimreaper’s picture

Discussed with @pdureau.

So, everything is OK:
- attributes.addStyle
- renderer / render array #styles
- preprocess callback (will alter directly $variables['#attached'] and $variables['#attributes'])

In each case:
- #attributes for kind: utility and kind: mode with empty target
- #attached html_head for kind: mode with target: meta
- #attached styles for kind: mode with target: html/body, then an event subscriber will put in html attributes or body attributes.
- #attached library for the library of the style plugin.

To add style utilities to html attributes or body attributes, use preprocess_html callback, (or force putting a style utility in #attached styles and default to body attributes).

$element['#attached']['styles'] = [
  'color_mode' => 'light',
];

Problem to solve is compatibility with big_pipe for #attached styles, and not sur if needed to be solved as for html and body tags big pipe should not intervene for those render element.

Also discussed about options:
- drop short syntax
- allow description per option

grimreaper’s picture

Idea of the night.

Should we add lifecycle/deprecation properties on style and/or on style options?

grimreaper’s picture

TODO:
- finish event subscriber for html/body
- add helper/refactor how to apply styles. Put logic into Style definition to avoid duplication between renderer and Attribute?
- drop short syntax for options.
- description of option.
- item_attributes? When adding a style, add optional parameter to specify attribute key?
- add lifecycle/deprecation properties on style and/or on style options?
- ensure big_pipe compatibility? not sure if relevant that a style alters body/html attributes asynchronously after page had been rendered.
- support of prefers-color-scheme or light-dark()

To test:
- Plugin manager getDefinitionsForTheme.
- Form element with theme.
- target property is now an enum
- Attribute object
- Attribute helper
- no more option short syntax
- description on option
- style application logic
- event subscriber

grimreaper’s picture

I will start/continue to update contrib modules to test with the new API.

Most of the stuff remaining here are to write tests.

If some review could be done to indicate if there is a blocker or an architecture problem before writing tests on stuff that will require rework it would be nice.

Thanks!

TODO:
- finish event subscriber for html/body
- add helper/refactor how to apply styles. Put logic into Style definition to avoid duplication between renderer and Attribute?
- item_attributes? When adding a style, add optional parameter to specify attribute key?
- ensure big_pipe compatibility? not sure if relevant that a style alters body/html attributes asynchronously after page had been rendered.
- drop short syntax for options.
- description of option.
- add lifecycle/deprecation properties on style and/or on style options
- support of prefers-color-scheme or light-dark()

To test:
- no more option short syntax
- description on option
- target property is now an enum
- StyleDefinition applyOrBubbleOnAttribute
- StyleDefinition applyOnAttribute
- StyleDefinition applyOrBubbleOnArray
- Plugin manager getDefinitionsForTheme.
- Form element with theme.
- Attribute object
- Attribute helper
- style application logic
- event subscriber
- bubbleablemetadata change

grimreaper’s picture

Issue summary: View changes
johnpitcairn’s picture

Coming in a bit late here sorry - I have been using a custom style_options plugin to allow editors to apply a css property value to an element via an inline style, ie:

<div style="--my-prop-name: 5">

I find this has advantages over simple css utility classes or data attributes, especially for use in unitless calculations.

Will a technique like this be supported?

grimreaper’s picture

Hello,

It is in the scope (see comment 16) and already implemented in the MR.

johnpitcairn’s picture

Thanks! I see it now.

The syntax for a simple range of values will be quite verbose, requiring repetition of the property name and manually specifying each individual value and unit.

As a future extension, I would like to allow for the possibility of a yaml definition something like:

type: style
element: range #specify the form widget
unit: rem #omit or '' for unitless
min: 0
max: 8
step: 1
property: '--my-prop-name'

A similar syntax could also be used with data attributes.

Given I had written a custom style_options plugin to support this, I'd be happy enough doing the same for this API as long as the underlying support and swappability is there.

grimreaper’s picture

Hello,

Thanks for your suggestion.

In short, I would say yes it will still be possible in extension as JSON schema allow additional unknown properties. But that's not the direction we want to promote.

I understand the feeling of code duplication. Know that with UI Suite Bootstrap, I went that way :D

The problem with such syntax is that it ties the style declaration to a form element and with our experience in UI Suite (UI Patterns, UI Styles, UI Skins, etc.) is that we want to decouple the declaration of the design system artifact (component, style, design token) to the UI forms (data can come from something else than a form).

1: because it is the front dev who declares the styles and how it will be applied, not how it will be configured. The style definition should know nothing (or as less as possible, maybe only suggest stuff) about Drupal Form API, where it will be configured. This would introduce drupalism into the declaration
2: having such link between style declaration and form element, would prevent (or make it harder) other contrib or custom alteration to choose something else.
3: concrete example of styles with Bootstrap:

flex_order:
  category: "Flex"
  label: "Flex order"
  description: "Change the visual order of specific flex items with a handful of order utilities. We only provide options for making an item first or last, as well as a reset to use the DOM order. As order takes any integer value from 0 to 5, add custom CSS for any additional values needed. Additionally there are also responsive order-first and order-last classes that change the order of an element by applying order: -1 and order: 6, respectively."
  links:
    - 'https://getbootstrap.com/docs/5.3/utilities/flex/#order'
  options:
    order_first:
      label: "First"
      value: "order-first"
    order_last:
      label: "Last"
      value: "order-last"
    order_0:
      label: "Order 0"
      value: "order-0"
    order_1:
      label: "Order 1"
      value: "order-1"
    order_2:
      label: "Order 2"
      value: "order-2"
    order_3:
      label: "Order 3"
      value: "order-3"
    order_4:
      label: "Order 4"
      value: "order-4"
    order_5:
      label: "Order 5"
      value: "order-5"

spacing_margin:
  category: "Spacing"
  label: "Margin"
  links:
    - 'https://getbootstrap.com/docs/5.3/utilities/spacing/#notation'
  options:
    m_0:
      label: "0"
      value: "m-0"
    m_1:
      label: "1"
      value: "m-1"
    m_2:
      label: "2"
      value: "m-2"
    m_3:
      label: "3"
      value: "m-3"
    m_4:
      label: "4"
      value: "m-4"
    m_5:
      label: "5"
      value: "m-5"
    m_auto:
      label: "Auto"
      value: "m-auto"

You will regularly have some styles with options mixing number and string so no possible to get a range.
4: Form alteration is possible (I am doing it in UI Styles with a different form element extending the one provided in this MR). But style application is currently done by the style definition (or at least providing helper in it), maybe that's not the correct approach, which will check options and not other properties.

nod_’s picture

MR looks good, a bit worried about adding more drupal specific logic to the attributes object, and more drupalism to SDC but that's a tradeoff that looks worth it

pdureau’s picture

adding more drupal specific logic to the attributes object, and more drupalism to SDC

In my opinion, the addition of \Drupal\Core\Template\Attribute::addStyle() is one of the key features of this MR. It is one of the 2 ways for developers to use the API with #style render property.

Using it instead of Attribute::addClass() or Attribute::setAttribute() will allow to:

  • check if a style exist
  • guarantee there is no options of the same styles (options are mutually exclusive)
  • attach asset libraries when they are

It is not a addition to SDC but also usable from any templates with an attribute object, like the SDC ones. I hope we will get rid of this object one day #3457874: HTML attributes as Twig mappings instead of PHP objects but it is another goal.

pdureau’s picture

Talked with the Mercury & Canvas teams today at Vienna.

The asked if this is related to CVA: https://cva.style

cva("base", options);

with:

  • base: the base class name (string, string[])
  • options (optional):
    • variants: your variants schema
    • compoundVariants: variants based on a combination of previously defined variants
    • defaultVariants: set default values for previously defined variants
      note: these default values can be removed completely by setting the variant as null

Variant schema is a mapping where key is a variant ID and value is a list of style utilities classes. Example:

{
    intent: {
      primary: ["bg-blue-500", "text-white", "border-transparent"],
      secondary: ["bg-white", "text-gray-800", "border-gray-400"],
    },
    size: {
      small: ["text-sm", "py-1", "px-2"],
      medium: ["text-base", "py-2", "px-4"],
    },
    disabled: {
      false: null,
      true: ["opacity-50", "cursor-not-allowed"],
    },
  }

So, variants are like "pattern presets" (see what Canvas and Display Builder are doing) but for style utilities instead of SDC.

Like pattern presets, it would be perfect as config entities, managed by site builders by assembling style plugins.

CVA has also a Twig function : html_cva https://twig.symfony.com/doc/3.x/functions/html_cva.html

{% set alert = html_cva(
    base: 'alert',
    variants: {
        color: {
            blue: 'bg-blue',
            red: 'bg-red',
            green: 'bg-green',
        },
        size: {
            sm: 'text-sm',
            md: 'text-md',
            lg: 'text-lg',
        }
    }
) %}

The main purpose of this function may not be useful for SDC because we don't extends/inherits component classes.
So we will need to be able to inject a toArray() of those config entities and use them there.

larowlan’s picture

Can we get an issue summary update here - what's the use case for this - why do we need it etc?
@grimreaper asked me to review it but I'm missing the context.

It looks like you can put #styles on any element and they bubble up with attachments and then get replaced.

I'm not super keen on the HTML rewriting - can we do that with placeholders like we do for scripts/css in \template_preprocess_html?

nod_’s picture

re #41, sounds good. +1 to adding to the API of the attribute object

pdureau’s picture

Here is an explanation of each added/modified files (outside tests).

The API itself:

  • JSON schema: core/assets/schemas/v1/style.schema.json
  • Form element: core/lib/Drupal/Core/Render/Element/Styles.php
  • A new plugin type:
    • core/lib/Drupal/Core/Theme/Style/Exception/StyleDefinitionErrorException.php
    • core/lib/Drupal/Core/Theme/Style/StyleDefinition.php
    • core/lib/Drupal/Core/Theme/Style/StyleDefinitionInterface.php
    • core/lib/Drupal/Core/Theme/Style/StylePluginManager.php
    • core/lib/Drupal/Core/Theme/Style/StylePluginManagerInterface.php
    • core/core.services.yml
  • Asset libraries handling:
    • core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
    • core/lib/Drupal/Core/Render/BubbleableMetadata.php

Mechanisms for local application:

  • Via a render property: core/lib/Drupal/Core/Render/Renderer.php
  • Via a new Drupal\Core\Template\Attribute::addStyle() method: core/lib/Drupal/Core/Template/Attribute.php
  • With: core/lib/Drupal/Core/Template/AttributeHelper.php

Mechanism for page wide application:

  • core/lib/Drupal/Core/EventSubscriber/HtmlStylesResponseFilter.php

Add a new #accept_attributes render property to be used in ElementInterface::getInfo() to know in which render element the #attributes object can be added/modified:

  • core/lib/Drupal/Core/Render/Element/InlineTemplate.php
  • core/modules/filter/src/Element/ProcessedText.php
  • as a complementary task, normalize the #item_attributes render property to #attributes:
    • core/modules/image/src/Hook/ImageThemeHooks.php
    • core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
    • core/modules/image/image.module
    • core/modules/responsive_image/src/Hook/ResponsiveImageThemeHooks.php
    • core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php

Use the new API with core_resize library to address #2880237: [meta] Refactor system/base library so we have an use case i nCore and we can see the benfit of the API:

  • core/modules/system/system.styles.yml
  • core/lib/Drupal/Core/Form/FormPreprocess.php
  • core/lib/Drupal/Core/Render/Element/Textarea.php
  • core/themes/claro/templates/form/textarea.html.twig
  • core/themes/starterkit_theme/templates/form/textarea.html.twig
  • core/profiles/demo_umami/themes/umami/templates/classy/form/textarea.html.twig
pdureau’s picture

Can we get an issue summary update here - what's the use case for this - why do we need it etc?

We need it as we need SDC, the Icon API and #3531854: Add a Design Tokens & CSS variables API:

  • for frontdevs: it will allow to implement designs in sharable, business agnostic, UI logic focused, Drupal theme
  • for back devs: it will allow to use the design implementation easier (see what we have done with core_resize in this MR)
  • for site builders: it will be usable by the new generation of display building tools like Display Builder or Canvas

It looks like you can put #styles on any element and they bubble up with attachments and then get replaced.

I'm not super keen on the HTML rewriting - can we do that with placeholders like we do for scripts/css in \template_preprocess_html?

So, with the visibility shared in #45, does that means Mechanism for page wide application would need some more discussion and/or work? The API is already very valuable without this mechanism, because local applcitaion is the most common use case. So we are OK to remove core/lib/Drupal/Core/EventSubscriber/HtmlStylesResponseFilter.php from the MR.

pdureau’s picture

Canvas project is also expecting this feature :)

pdureau’s picture

Usages to check the validity of this proposal:

larowlan’s picture

So, with the visibility shared in #45, does that means Mechanism for page wide application would need some more discussion and/or work? The API is already very valuable without this mechanism, because local applcitaion is the most common use case. So we are OK to remove core/lib/Drupal/Core/EventSubscriber/HtmlStylesResponseFilter.php from the MR.

In slack during discussion with @grimreaper I posted

If the bubbling to the body stuff isn't the 80% use case my advice would be to remove it. We should focus on MVP for core

It sounds like this bubbling isn't the main use case so I think we're in agreement there - thanks!

grimreaper’s picture

Issue summary: View changes

Before removing, I will do a test with @nod_ suggested usage of DOM. Then will remove.

And about remaining todo:

- update form element to be able to filter by target, provider, not just theme and kind.

I got an idea, I will isolate in a protected method the gathering of definitions, so that it will be easier to only override this part for contrib needs.

cedric_a’s picture

Hello, started at DrupalCon Vienna last friday, I took the time to finish my little simple test of the new API, I got 2 difficulties, due to the documentation composed from the comments above, so here are my take aways :
- the final filename is NOT my_theme.utilities.yml BUT my_theme.styles.yml (thank you Florent for the clarification)
- to add your styles to the render array in a preprocess, DON'T USE $variables['#styles']['background_color'] BUT $variables['#attached']['styles']['background_color']

Here is my complete test implementation, the idea is to provide a setting (background color) in the theme (named 'dcwien25'), the options of this setting are loaded from the yaml file, and finally the selected options is adding its corresponding css class to the html body.

dcwien25.styles.yml

background_color:
  label: "Background color"
  attribute: class # Overridable at the option level.
  options:
    light:
      label: Light
      value: 'bg-light'
    dark:
      label: Dark
      value: 'bg-dark'
    blue:
      label: Blue
      value: 'bg-info'

theme-settings.php

use Drupal\Core\Form\FormState;

/**
 * Implements hook_form_system_theme_settings_alter().
 */
function dcwien25_form_system_theme_settings_alter(array &$form, FormState $form_state): void {
  $style_manager = \Drupal::service('plugin.manager.style');
  /** @var \Drupal\Core\Theme\Style\StyleDefinitionInterface|null $definition */
  $definition = $style_manager->getDefinition('background_color', FALSE);

  $options = [];
  if ($definition) {
    $options = $definition->getOptionsAsOptions();
  }

  $form['dcwien25'] = [
    '#type' => 'details',
    '#title' => t('Settings available thank\'s to styles API'),
    '#open' => TRUE,
  ];

  $form['dcwien25']['bgcolor'] = [
    '#type' => 'select',
    '#title' => t('Background color'),
    '#default_value' => theme_get_setting('bgcolor'),
    '#options' => $options,
  ];
}

dcwien25.theme

/**
 * Implements hook_preprocess_HOOK() for html.html.twig.
 */
function dcwien25_preprocess_html(array &$variables): void {
  $bgcolor = theme_get_setting('bgcolor');
  if ($bgcolor) {
    $variables['#attached']['styles']['background_color'] = $bgcolor;
  }
}

My thoughts : this is pretty straightforward and easy to use, my next use case would be to add styling options in blocks or paragraphs (until they are replaced with Canvas !!)

grimreaper’s picture

Thanks @cedric_a for the tests.

About styles.yml it will be specified in the change record.

About #styles not working, it is because you have tested in a preprocess and potentially put #styles NOT on a render element. If you put #styles on a render element it will work:

$build['test'] = [
  '#type' => 'container',
  '#styles' => [
    'background_color' => 'dark',
  ],
];
grimreaper’s picture

Hi,

Form element updated for easier override of definitions obtention logic.

I will complete the tests during the coming days.

In the meantime if people can give reviews and feedbacks to ensure architecture.

I will appreciate to not write tests on stuff which would potentially be removed or reworked ;)

Thanks!

grimreaper’s picture

Status: Needs work » Needs review
grimreaper’s picture

Assigned: grimreaper » Unassigned
grimreaper’s picture

Reworked how Attribute object handle attachments to avoid side effects during rendering process and fix existing tests.

Only remaining existing test not passing is core/modules/syslog/tests/src/Kernel/SyslogTest.php

Because syslog config is null in the service during execution. I think it is due to the logger factory service added to the renderer service. So during test execution, the logger service is not recreated with the module's config imported.

But I had not the time to ensure that and figure out a fix yet.

grimreaper’s picture

Pipeline is green now!

Adding new tests.

Removing tag "Needs issue summary update" per comment 45.

grimreaper’s picture

Issue summary: View changes

Updating remaining tests to write.

To test:
- no more option short syntax
- description on option
- target property is now an enum
- enums
- StyleDefinition applyOrBubbleOnAttribute
- StyleDefinition applyOnAttribute
- StyleDefinition applyOrBubbleOnArray
- Attribute object addStyle and changes
- Attribute object addStyle through Twig
- Attribute helper changes
- style application logic
- bubbleablemetadata change
- unit or kernel or functional test on renderer change, aka apply on render array

grimreaper’s picture

Issue summary: View changes

I have completed the tests:

- Attribute object
- Attribute helper
- renderer
- bubbleablemetadata

I have not found an existing test for the one line addition of HtmlResponseAttachmentsProcessor

I started to write kernel test for the renderer to see resulting render array after processing, but this was equivalent to unit tests already done.

Same for calling Attribute in Twig, with the unit tests on it PHP side I think it is ok.

Now waiting for reviews and feedbacks!

Thanks!

grimreaper’s picture

Discussed with @pdureau,

Changes related to removing special case of #item_attributes to use #attributes instead moved to a dedicated issue #3554447: Use #attributes instead of #item_attributes to simplify the MR of style API issue.

pdureau’s picture

There are now 4 usages of this API.

As a style provider:

As a style plugin consumer:

pdureau’s picture

The MR has been simplified since comment #45. Here is the content (tests excluded).

The API itself:

  • JSON schema: core/assets/schemas/v1/style.schema.json
  • Form element: core/lib/Drupal/Core/Render/Element/Styles.php
  • A new plugin type:
    • core/lib/Drupal/Core/Theme/Style/Exception/StyleDefinitionErrorException.php
    • core/lib/Drupal/Core/Theme/Style/StyleDefinition.php
    • core/lib/Drupal/Core/Theme/Style/StyleDefinitionInterface.php
    • core/lib/Drupal/Core/Theme/Style/StylePluginManager.php
    • core/lib/Drupal/Core/Theme/Style/StylePluginManagerInterface.php
    • core/core.services.yml
  • Asset libraries handling:
    • core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
    • core/lib/Drupal/Core/Render/BubbleableMetadata.php

Mechanisms for local application:

  • Via a render property: core/lib/Drupal/Core/Render/Renderer.php
  • Via a new Drupal\Core\Template\Attribute::addStyle() method: core/lib/Drupal/Core/Template/Attribute.php
  • With: core/lib/Drupal/Core/Template/AttributeHelper.php

Add a new #accept_attributes render property to be used in ElementInterface::getInfo() to know in which render element the #attributes object can be added/modified:

  • core/lib/Drupal/Core/Render/Element/InlineTemplate.php
  • core/modules/filter/src/Element/ProcessedText.php

Use the new API with core_resize library to address #2880237: [meta] Refactor system/base library so we have an use case in Core and we can see the benefit of the API:

  • core/modules/system/system.styles.yml
  • core/lib/Drupal/Core/Form/FormPreprocess.php
  • core/lib/Drupal/Core/Render/Element/Textarea.php
  • core/themes/claro/templates/form/textarea.html.twig
  • core/themes/starterkit_theme/templates/form/textarea.html.twig
  • core/profiles/demo_umami/themes/umami/templates/classy/form/textarea.html.twig
pdureau’s picture

nicxvan’s picture

Did a really high level review, didn't review all of the test coverage.

I'm not super sure but this feels analogous to libraries and I wonder if info alter hooks should be executed somewhere: https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Co...

grimreaper’s picture

Hi,

Thanks for the reivew!

Did an update regarding small changes. Waiting for discussion approval for the remaining points.

For comment 64, I think you are right, I have not tested the hook_info_alter for styles. Looking at DefaultPluginManager:

protected function alterDefinitions(&$definitions) {
    if ($this->alterHook) {
      $this->moduleHandler->alter($this->alterHook, $definitions);
    }
  }

So the alterDefinition of the stylePluginManager should be changed as???:

  protected function alterDefinitions(&$definitions) {
    parent::alterDefinitions($definitions);
    if ($this->alterHook) {
      $this->themeManager->alter($this->alterHook, $definitions);
    }

    /** @var \Drupal\Core\Theme\Style\StyleDefinitionInterface[] $definitions */
    foreach ($definitions as $definition_key => $definition) {
      if (!$definition->isEnabled()) {
        unset($definitions[$definition_key]);
      }
    }
  }
nicxvan’s picture

I'm honestly not sure what the established pattern is, I just know we want to alter themes after modules.

Maybe asking one of the plugin subsystem maintainers would be helpful.

grimreaper’s picture

Assigned: Unassigned » grimreaper
grimreaper’s picture

Assigned: grimreaper » Unassigned

Hi,

All code review threads had been fixed.

Issue still in needs review if other feedback are needed.

nicxvan’s picture

I think all of my feedback has been addressed, I don't think I can RTBC as I haven't reviewed it deep enough, but I think it's ready for that deep review.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

grimreaper’s picture

Status: Needs work » Needs review

MR was not mentioning git conflict and tests are still green after rebase.

Back to needs review.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

grimreaper’s picture

Hum, I am not sure how to fix pipeline after rebase.

https://git.drupalcode.org/issue/drupal-3517033/-/jobs/8345516

The "Drupal\Core\Theme\Style\StylePluginManagerInterface::getGroupedDefinitions()" method will require a new "string $label_key" argument in the next major version of its interface "Drupal\Component\Plugin\CategorizingPluginManagerInterface", not defining it is deprecated.

In the latest commit I have added this argument, but CI is still not happy.

I don't get the deprecation when executing the tests locally.

grimreaper’s picture

I forgot a method, misreading the error message.

Thanks godotislate on Slack!

https://drupal.slack.com/archives/C079NQPQUEN/p1770144037257769

pdureau’s picture

pdureau’s picture

Talked with the Mercury & Canvas teams today at Vienna.

The asked if this is related to CVA: https://cva.style

Once the Style Utility API is here, CVA can easily being added as a config entity on the top of style plugins, in the same way:

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

grimreaper’s picture

Assigned: Unassigned » grimreaper
grimreaper’s picture

Assigned: grimreaper » Unassigned
Status: Needs work » Needs review

The Functional JS error is unrelated.

Back to needs review after rebased.

I pushed a separated branch with the commits history before I squashed it to ease current and future rebases.

catch’s picture

The issue summary could use an update here, mostly an example of what the proposed API looks like.

grimreaper’s picture

Assigned: Unassigned » grimreaper

Good suggestion!

I am preparing that as a change record and also updating IS.

grimreaper’s picture

Assigned: grimreaper » Unassigned
Issue summary: View changes

CR created: https://www.drupal.org/node/3586264

Should documentation pages be created before merge? In case someone reads a documentation page for an API not merged yet...

grimreaper’s picture

Issue tags: +DevDaysAthens2026
needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new1.88 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

kentr’s picture

Issue tags: +Accessibility

Accessibility tag for dark mode support.

Edit: And contrast, forced-colors, or anything else related to accessibility.

mgifford’s picture

There are some interesting ideas here. I'd love classes to be more than just random names.

I was thinking about this a bit last week. Not this specifically, but how do we add structure to how we define style guides. As sites get more complicated we have to start understanding why.

https://mgifford.github.io/ACCESSIBILITY.md/examples/MODERN_CSS_THEME_AR...
https://mgifford.github.io/ACCESSIBILITY.md/examples/COLOR_CONTRAST_ACCE...

Having more of an organizing logic around these styles is important.