Skip to content

!important stripped from inlined styles when element has existing style attribute #682

@jareddellitt

Description

@jareddellitt

When an element already has an inline style attribute and CSS rules with !important are inlined onto it, the !important flags are dropped from the output. This only happens when minify_css is true (the default). Elements without pre-existing style attributes correctly preserve !important.

This is caused by two related bugs in merge_styles in css-inline/src/html/serializer.rs.

Bug 1: Property lookup uses wrong separator when minify_css is true

The find closure that searches for existing properties in the declarations buffer always compares against STYLE_SEPARATOR (": " with space):

.find(|style| {
    style.starts_with(property.as_bytes())
        && style.get(property.len()..=property.len().saturating_add(1))
            == Some(STYLE_SEPARATOR)  // always ": " (with space)
})

But when minify_css is true, buffer entries are written using STYLE_SEPARATOR_MIN (":"). The lookup never matches, so:

  • Properties are duplicated instead of merged (CSS values prepended, existing inline values appended)
  • The !important cascade logic in all four match arms is never reached for the correct case
  • Existing inline values (without !important) appear last, winning by CSS last-value-wins

Bug 2: !important stripped from output during merge (regardless of minify_css value)

When a CSS rule's value contains !important, merge_styles strips it via strip_suffix("!important") for cascade resolution but never writes it back. Both the (Some(value), Some(buffer)) and (Some(value), None) arms write the stripped value without restoring !important:

// Case 1: !important rule overrides existing
(Some(value), Some(buffer)) => {
    if !buffer.ends_with(b"!important") {
        buffer.truncate(property.len().saturating_add(STYLE_SEPARATOR.len()));
        write_declaration_value(buffer, value)?;  // value has !important stripped
        // !important is lost
    }
}
// Case 2: !important rule, no existing property
(Some(value), None) => {
    push_or_update!(..., value, ...);  // value has !important stripped
    // !important is lost
}

Reproduction

let html = r#"<html>
  <head>
    <style>
      .btn a {
        color: #fff;
        display: inline-block !important;
        font-weight: 700 !important;
        text-decoration: none !important;
      }
    </style>
  </head>
  <body>
    <table><tr><td class="btn">
      <a href="#" style="color: #fff; display: inline-block; font-weight: 700; text-decoration: none;">
        Click
      </a>
    </td></tr></table>
  </body>
</html>"#;

let inliner = CSSInliner::options().build();
let result = inliner.inline(html).unwrap();
println!("{}", result);

Current output (v0.20.0, minify_css: true):

<a href="#" style="color:#fff;display:inline-block;font-weight:700;text-decoration:none">

All !important flags are dropped.

Expected output:

<a href="#" style="color:#fff;display:inline-block !important;font-weight:700 !important;text-decoration:none !important">

Why this matters

This is critical for HTML email rendering. Email clients like Gmail add their own stylesheets (e.g., blue-link overrides via u + #body a { color: inherit !important }). Without !important on inlined button styles, these client-injected rules override button colors and break layouts.

Suggested fix

In merge_styles:

  1. Use the minification-aware separator for the property lookup:
let sep = if minify_css { STYLE_SEPARATOR_MIN } else { STYLE_SEPARATOR };
// ... use `sep` in both the find closure and the truncate call
  1. Restore !important after writing the stripped value:
// Case 1: after write_declaration_value
buffer.extend_from_slice(b" !important");

// Case 2: after push_or_update!
if let Some(buf) = declarations_buffer.get_mut(parsed_declarations_count.saturating_sub(1)) {
    buf.extend_from_slice(b" !important");
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions