global-styles-inline-css takes precedence over Tailwind styles

I have a project using latest Sage (main branch GitHub) using a traditional WP setup (no Bedrock) on DevKinsta.

Problem
I’ve noticed that some rules generated by Tailwind being overridden by rules in global-inline-styles (global block styles). Tailwind’s adoption of cascade layers in V4 seems to be the culprit. Since unlayered styles take precedence over layered styles, styles in global-styles-inline-css can override styles in app.css. For example, the following selectors have the same specificity, so without layers whichever is defined last should be applied.

app.css

a {
  color: inherit;
  text-decoration: inherit;
        }
global-styles-inline-css

a:where(:not(.wp-element-button)) {
   text-decoration: underline;
}

However, since Tailwind began wrapping it’s styles in cascade layers in V4, the second rule will always win no matter the order of definition. You can also see that .no-underline is overridden as well:

Possible Solutions

  1. Important modifier
    One solution is to use the important modifier, but this would still be a breaking change for those migrating from the latest release, requiring theme devs to add the modifier to all of the clashing styles. Additionally, rules in Tailwind’s default CSS reset can still be overridden as well.

  2. Important Flag
    A more general solution would be to add the important flag, which marks all utilities as !important. While this prevents having to modify individual classes on migration, the blanket use of !important can cause unexpected issues down the line.

  3. More Layers
    My last idea is to wrap global-styles (which is generated by wp_enqueue_global_styles) in a layer that is defined prior to Tailwind’s layers. I’ve written up some preliminary code below.

Code
First, dequeue and de-register global:

app/setup.php
add_action('wp_enqueue_scripts', function () {
    wp_dequeue_style('global-styles-inline-css');
    wp_deregister_style('global-styles-inline-css');
});

This is a modified version of wp_enqueue_global_styles, which wraps the styles in a cascade layer called “wp-global-styles”

app/setup.php

add_action('wp_enqueue_scripts', function () {

    $is_block_theme   = wp_is_block_theme();

    add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );

    $stylesheet =  wp_get_global_stylesheet();

    if ( $is_block_theme ) {
        $stylesheet .= wp_get_global_stylesheet( array( 'custom-css' ) );
    }

    if ( empty( $stylesheet ) ) {
        return;
    }

    wp_register_style( 'global-styles', false );

    // wraps styles in in global-block-styles layer 
    
    wp_add_inline_style( 'global-styles', "@layer global-block-styles { $stylesheet }");
    wp_enqueue_style( 'global-styles' );

    wp_add_global_styles_for_blocks();
}, 10, 0);

Lastly, define the global-block-styles layer in app.css

@layer "global-block-styles";
@import "tailwindcss";
...

I wasn’t able to find any filters for the wp_head action, so this code generates global-styles twice - once during the wp_head action when wp_enqueue_global_styles is called and again in the above custom action. Another possible solution, which I didn’t write up, would be to manually build the head content, to prevent building global-styles twice.

Anyway, would love to hear your thoughts on this.

1 Like

This is something I struggled with as well as a total newbie to Tailwind and theme.json.

A simple solution is to add the following to the preprocessed theme.json:

  "styles": {
    "elements": {
      "link": {
        "typography": {
          "textDecoration": ""
        }
      }
    }
  }

Leaving the declaration empty seems to remove the whole a:where(:not(.wp-element-button)) from the global-styles-inline-css output completely.

2 Likes