DEV Community

Cover image for Am I the only one seeing infinite possibilities in the HTML style attribute?
Nobuo Nakayama
Nobuo Nakayama

Posted on

Am I the only one seeing infinite possibilities in the HTML style attribute?

Hi everyone,

I’ve been spending a lot of time researching the inline style attribute - the one we’ve all been told to avoid because it traditionally ruins CSS cascading and can't be overridden without using !important.

I wanted to share my findings and see what you think.

The Catalyst

I’ve been around since the XHTML days, so I used to be deeply obsessed with validation-clean, "perfect" HTML. I made every effort to strip away presentational elements and attributes to keep my markup pure. Using the style attribute was completely out of the question.

However, looking at modern project layouts, I noticed that class attributes are now cluttered with presentational values:

has-background, has-text-color, row, col, text-center, text-wrap, …

Then one day, I encountered a UI state that couldn't be easily solved by pre-defined components alone:

<div class="animation" style="--time: 8s; --easing: ease-in-out;"></div>
Enter fullscreen mode Exit fullscreen mode

By combining the style attribute with CSS custom properties, I realized I could control components with much more flexibility than static utility classes allowed.

Custom Properties as Arguments

This sparked an idea: what if we treated custom properties inside a style attribute like function arguments for UI components?

<p><button class="button" style="--color: var(--red);"></button></p>
<p><button class="button" style="--background: var(--blue); --color: var(--white);"></button></p>
<p><button class="button" style="--border-width: var(--line_x-fine);"></button></p>
<p><button class="button" style="--border-color: var(--green);"></button></p>
<p><button class="button" style="--border-radius: 1e6px;"></button></p>
<p><button class="button" style="--font-size: var(--em_large);"></button></p>
<p><button class="button" style="--inline-size: 100%;"></button></p>
Enter fullscreen mode Exit fullscreen mode

Suddenly, a single component could yield an almost infinite number of variations.

More importantly, all presentational class names completely vanished from the class attribute. Looking at the native roles of HTML attributes, this felt like the correct allocation of responsibilities.

Eliminating !important

While building out these buttons, I stumbled upon another crucial realization: when leveraging custom properties this way, you can easily mute or override them from your global stylesheet without ever using !important.

.button {
    /* Arguments */
    --background: transparent;
    --border-width: var(--line_x-fine);
    --border-color: currentcolor;
    --border-radius: 0;
    --color: currentcolor;
    --font-size: 1em;
    --inline-size: auto;

    /* Processing */
    background: var(--background);
    border: solid var(--border-width) var(--border-color);
    border-radius: var(--border-radius);
    color: var(--color);
    font-size: var(--font-size);
    inline-size: var(--inline-size);
    padding-block: calc(0.5lh - 0.5em);
    padding-inline: calc(2lh - 2em);

    /* Condition */
    &:where([style~="--background:"]) {
        border-color: transparent; /* '!important' is not used. */
    }
}
Enter fullscreen mode Exit fullscreen mode

This was the exact moment my long-held negative stigma toward the style attribute disappeared.

Just-in-Time Variables

Next, I conceived a method to apply properties globally across elements, acting much like instant, on-the-fly variables:

<p><button class="button" style="--border-radius: 1e6px;"></button></p>
<p><button class="button" style="--font-size: var(--em_large);"></button></p>
<p><button class="button" style="--inline-size: 100%;"></button></p>
Enter fullscreen mode Exit fullscreen mode
[style~="--border-radius:"] {
    border-radius: var(--border-radius);
}
[style~="--font-size:"] {
    font-size: var(--font-size);
}
[style~="--inline-size:"] {
    inline-size: var(--inline-size);
}

.button {
    /* Arguments */
    --background: transparent;
    --border-width: var(--line_x-fine);
    --border-color: currentcolor;
    --color: currentcolor;

    /* Processing */
    
}
Enter fullscreen mode Exit fullscreen mode

By establishing these utility-like attribute triggers, I successfully minimized the number of default arguments that needed to be explicitly pre-defined inside individual component classes.

Value Hacking

<div style="--color: var(--my-color);"></div>
Enter fullscreen mode Exit fullscreen mode

With this approach, you are embedding design tokens directly into the HTML layer via the style attribute.

Therefore, I initially worried about what would happen if we had to undergo a massive design overhaul and change our design tokens globally. However, I realized that we can actually hack the value itself:

:root {
  --my-color:  ;
}
.example [style*="var(--my-color)"] {
  --my-color:  ;
}
Enter fullscreen mode Exit fullscreen mode

We can simply define the token globally, or intercept and hack the injected token string itself using attribute selectors.

Fast forward to the present

I have successfully broken through the traditional limitations of inline styling.

<div style="--c3_color--: var(--red);"></div>
<div style="--c-first_color--: var(--red);"></div>
<div style="--hover_color--: var(--red);"></div>
<div style="--before_color--: var(--red);"></div>
<div style="--cqi-m_color--: var(--red);"></div>
<div style="--cqi-m_c3-first_hover_before_color--: var(--red);"></div>
Enter fullscreen mode Exit fullscreen mode
[style*="--c3_color--:"] > * > * > * {
    color: var(--c3_color--);
}
[style*="--c-first_color--:"] > *:where(:first-child) {
    color: var(--c-first_color--);
}
[style*="--hover_color--:"]:where(:hover) {
    color: var(--hover_color--);
}
[style*="--before_color--:"]::before {
    color: var(--before_color--);
}
@container (inline-size > 720px) {
    [style*="--cqi-m_color--:"] {
        color: var(--cqi-m_color--);
    }
    [style*="--cqi-m_c3-first_hover_before_color--:"] > * > * > *:where(:first-child):where(:hover)::before {
        color: var(--cqi-m_c3-first_hover_before_color--);
    }
}
Enter fullscreen mode Exit fullscreen mode

By routing specific custom property naming patterns, we can now achieve deep descendant component targeting, pseudo-element styling, and conditional branching via pseudo-classes and container queries?orchestrations previously considered impossible with standard inline styles.

Of course, it's impossible to pre-author every single combination of CSS properties and structural prefixes manually.

To solve this, I developed compilation scripts in both JavaScript and PHP that scan the HTML, extract these custom properties, and automatically generate the necessary utility CSS rulesets on the fly.

I’ve named this architecture XSA (EXtended Style Attribute) and released it as an open-source project on GitHub. If you're interested in checking out how it works under the hood:

I would love to hear your feedback on this approach!

Top comments (0)