Scoping CSS inline styles with css-scope-inline - LogRocket Blog (original) (raw)

Inline <style> tags offer a modern HTML styling pattern, but this mechanism has a known scoping issue in vanilla CSS and HTML: the traditional <style> tag typically needs to be placed within the head tag, so all styling rules within a particular <style> tag affect the whole HTML page by default. This makes it difficult if you need to scope a <style> tag for a specific HTML element because the native CSS @scope tag is still experimental and doesn’t have good browser support yet.

Scoping CSS inline styles with css-scope-inline

Using a unique DOM identifier, following BEM, or using frontend frameworks are possible workarounds for inline <style> tag scoping. In this article, however, we’ll cover the css-scope-inline library, which offers a simple JavaScript code snippet to scope your inline <style> tags without adding a build step to your vanilla CSS/HTML project.

I’ll explain practical use cases of inline <style> tag scoping, describe how the css-scope-inline project works internally, list highlighted features, and demonstrate how to use it practically with your web projects to simplify inline <style> tag scoping.

What is inline <style> tag scoping?

Almost all web developers use CSS to design webpages with responsive grid systems, JavaScript-free animations, and dynamic styles. There are two predominant methods for adding CSS definitions to HTML pages:

  1. Creating separate vanilla CSS stylesheets and linking them with HTML pages within <head> sections using <link> tags
  2. Using inline <style> tags within the HTML document body and inside HTML elements to keep both element structures and styling definitions in the same place for better readability

Before we discuss the inline <style> tag scoping requirements, we need to understand the Locality of Behavior (LoB) principle. When practicing LoB, you place action-oriented code as close as possible to the related action element, which gives readers of your code a better understanding of its behavior.

For example, suppose you write the implementation of a specific click action and place a <script> tag closer to the related action button without writing the implementation in a separate JavaScript file. In that case, you’ll implement the LoB principle with the specific action button.

Many popular frontend frameworks adhere to the LoB principle by letting developers write JavaScript and HTML in the same component source file.

Similarly, we can implement the LoB principle for CSS and HTML by writing our CSS styling definitions within the HTML segment we want to style. Then, child elements of the primary element can also be styled within the same <style> tag using traditional or modern nested selectors.

This technique helps us instantly browse styling definitions for a particular element without navigating to a separate CSS stylesheet or scrolling to another section of the same page. This CSS writing strategy is known as inline <style> tag scoping.

Below, we create a scope for CSS definitions based on an HTML element:

Best practices for inline <style> tag scoping

The inline <style> tag scoping technique is recommended in any scenario where you implement LoB-based styling. In other words, this scoping method is necessary in situations where you need to set styles for specific HTML elements with the standard <style> tag by avoiding global styling collisions (i.e., styling a specific <div>‘s <button> elements using the tag name without affecting other buttons on your webpage).

You may need LoB-based styling and inline <style> tag scoping in the following practical use cases:

  • Isolating element-specific CSS styles and HTML contents of a web app frontend that doesn’t use a third-party frontend framework
  • Implementing collision-free styles for an app that is rendered through a micro-frontend framework
  • Styling a segment of an article in a CMS without affecting CMS container app styles
  • Styling a web widget that doesn’t use a modern web component implementation
  • Implementing an isolated CSS styling demo without using iframes

Drawbacks of existing inline <style> tag scoping solutions

The following are solutions for inline <style> tag scoping, but they have considerable drawbacks, as we’ll explain in each section.

Using a DOM unique identifier and the BEM methodology

We can write a scoped <style> tag with a unique DOM identifier, as follows:

Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Also, it’s possible to use the BEM method and use only CSS classes, as shown in the following code snippet:

Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

The drawback is that you have to add manual unique identifiers or class names for each scoped CSS element with this approach. Manual unique identifiers or class names are needed to apply unique styles via CSS selectors regardless of the DOM element order. For example, using element-1​ class name twice applies the same .element-1​ selector-based styles to both elements, so we need element-2​ to make the second element unique.

Using the native CSS @scope

The @scope at-rule, the successor of the deprecated HTML scoped attribute, offers an inbuilt, native browser feature for writing scoped <style> tags, according to the official specification:

Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Here, we styled the parent element using the :scope pseudo-selector and child elements with normal CSS class selectors.

However, the CSS @scope feature is still experimental, and browser support is immature. According to MDN, only the latest Chromium-based browsers support this feature.

It takes some time to stabilize a new browser feature since not all users actively download every browser release, so it isn’t recommended to use this feature in production at the time of writing this article.

Using a frontend library that supports inline <style> tag scoping

Most frontend frameworks, like Riot and Vue, implement inbuilt, scoped CSS features. For example, look at the following Riot component:

Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

If you choose this approach, you should use a frontend framework that triggers a build step even if you plan to use vanilla CSS and HTML, which don’t require a build step.

How does css-scope-inline offer a better CSS scoping solution?

css-scope-inline offers a simple script that you can easily include in any webpage to enable scoped inline <style> tags. The script doesn’t force you to add unique CSS classes or DOM identifiers, and doesn’t include a build step to generate scoped CSS.

Instead, this script monitors <style> tag changes via the MutationObserver and query selector APIs and adds scoping to styles using unique, auto-generated CSS class names. It also automates the manual <style> tag scoping method, which uses unique CSS classes, with a pre-developed script.

The Web APIs used in this script have very good browser support (see CanIUse for MutationObserver), especially as compared to the native @scope experimental feature. Moreover, this script has just 16 lines of code, so it won’t cause bundle bloat.

css-scope-inline vs. native @scope

The standard experimental @scope CSS at-rule offers the same scoping feature that css-scope-inline offers. Other existing CSS scoping solutions come with various critical drawbacks, but the standard @scope at-rule may affect the popularity of css-scope-inline in the future because @scope is an inbuilt browser feature that comes with better performance.

However, @scope‘s browser support is not production-friendly yet and it doesn’t offer developer-friendly CSS naming like me, this, or self.

Take a look at the following comparison table to identify differences before selecting one for your next web project:

Comparison factor css-scope-inline Native CSS @scope
Internal implementation method JavaScript-based implementation with standard web APIs Native CSS feature from the browser
Browser support Works on all modern browsers that support MutationObserver (see CanIUse) Work on only the latest Chromium browsers at this moment (see CanIUse)
Production usage Possible Discouraged at the time of writing due to browser support limitations
Performance Depends on the MutationObserver and query selector API performance Depends on the internal browser implementation. Offers better performance since the implementation runs on the browser’s CSS parser — not on a JavaScript interpreter like css-scope-inline
Developer-friendly aliases for selecting the parent element Yes, this and self No
Possibility of customizing the scoping logic Yes, by modifying the script source No
Offers inbuilt responsive design shortcuts Yes No

Highlighted features of css-scope-inline

The following features should motivate web developers to choose css-scope-inline for writing scoped CSS:

  • A simple, fast, standard JavaScript-based implementation that doesn’t require a build step
  • Works on vanilla HTML
  • No preprocessors, like Tailwind, Sass, or Less, are required
  • Works on all standard browsers, compared to the experimental CSS @scope feature
  • Offers a better, developer-friendly, and productivity-focused syntax than @scope and other methods for writing scoped CSS
  • Offers an inbuilt shorthand syntax for implementing responsive screens
  • Lets developers write scoped CSS animations
  • Works collaboratively with other JavaScript libraries that follow the LoB principle, i.e., HTMX and Surreal

css-scope-inline tutorial

Now that we’ve covered inline <style> tag scoping and the theoretical concepts of the css-scope-inline project, let’s use this library practically to write scoped CSS.

Installation options

This is a simple library with only a few lines of code, so you can copy-paste it into your web projects productively. For better maintainability in somewhat large multi-page projects, you can place this library in a separate JavaScript file.

It’s also possible to use a cloud CDN service to load this script, as shown in the following code snippet that uses the JsDelivr CDN:

In this tutorial, we’ll use the JsDelivr-based approach.

Creating basic inline scoped <style> tags

Let’s use css-scope-inline to build a simple HTML card element. Create an HTML file with the following content and drag-and-drop it to the web browser:

Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Here, the custom me selector refers to the parent div element that holds the style segment. You can use this and self aliases instead of me according to your naming preference.

When you run the above HTML document on the browser, you’ll see a styled card element:

A card element with a scoped style tag

Let’s add a button to the page. Add another scoped CSS-styled segment with the following HTML code:

Action button

Even though we use the custom selector me again, it won’t create styling collisions and affect the previous card. Look at the following preview:

Two segments were created with scoped inline style tags

Understanding DOM changes after scoping

What do you think about DOM changes after scoping with the library? It’s not possible to scope a CSS source snippet with only one unique class name in vanilla CSS and HTML.

Let’s understand the process under the hood with DevTools. Inspect both HTML segments and see the dynamically generated content by the library:

Inspecting DOM changes after the scoping process

As you can see, the library accomplished the scoping process with the following steps:

  1. Add a dynamically generated unique class name for the parent element that holds the <style> tag
  2. Replace me with the dynamically generated class name in the CSS source segment
  3. Stop the recursive mutation observer monitoring process by adding the ready attribute to each scoped <style> tag

Using nested CSS

The native CSS nesting feature offers a way to write organized CSS documents avoiding repetitive selector prefixes by nesting CSS definitions inside parent CSS selectors. In the previous example, we used the me prefix for styling child elements, but we can avoid repetitive me prefix with native CSS nesting, as follows:

At the moment of writing this article, only Firefox fully implements CSS nesting according to MDN documentation, so be careful with production usage.

Creating scoped CSS variables

The native CSS variables (custom properties) feature often helps developers implement dynamic global color schemes without switching CSS class names or loading additional stylesheets. Also, we can use CSS variables to eliminate repetitive, hardcoded CSS property values.

With css-scope-inline, you can create scoped CSS variables that don’t affect other global CSS variables, as shown in the following code snippet:

Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

The above code snippet defines the scoped --border-color CSS variable to set the border colors for two HTML elements. This variable won’t be exposed beyond the parent div since we defined it within the me scope. This is a great way to overcome repetitive CSS property issues while working with scoped inline <style> tags.

Mixing global CSS with scoped CSS

Writing scoped CSS <style> tags is a great way to achieve the LoB principle in HTML pages, but it may create repetitive CSS code even when you use scoped CSS variables.

Assume that you need to create two versions of the card element we just created above: one with a grey color scheme and one with a yellow color scheme. We’ll only use each version once on the web page.

We can add common CSS code using the following strategies:

  1. Using a <style> tag within the <head> section
  2. Creating a separate stylesheet
  3. Defining global styles within a scoped <style> tag

The first approach offers a better way to add global styles without invalidating the LoB principle. Look at the following HTML document that renders two card elements with two color schemes:

Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Here, we define global positional-adjustments-specific styles for card elements within the <head> tag and used scoped styles for color-specific styling. The above source code will render two card elements as follows:

Styling two card elements by mixing global CSS and scoped CSS

Achieving responsive design with media query shortcuts

Popular CSS frameworks like Bootstrap and Tailwind implement inbuilt responsive media query breakpoints for each pre-developed component. If you don’t use a CSS framework that supports responsive design, you’ll need to add media query breakpoint values manually, as shown in the following CSS source snippet:

@media only screen and (max-width: 639px) { me button { display: block; width: 100%; } }

The css-scope-inline library lets you use Tailwind responsive breakpoints within scoped <style> tags as follows:

Action button

The library will expand xs- to a responsive breakpoint (@media (max-width: 639px)) during the CSS scoping process and activate the nested CSS style for smaller viewpoints:

A responsive design built with the css-scope-inline library

Implementing scoped CSS animations

We can create keyframe-based animations in CSS using the @keyframes <identifier> at-rule. This at-rule requires a unique identifier, so scoping is required to use the same keyframe identifier within multiple <style> tags.

The library supports keyframe scoping and lets you create scoped animation definitions as follows:

Action button

Here, we defined a keyframe set with the me- prefix, so the library will scope it accordingly using a unique dynamic identifier. As a result, you can use me-button as a keyframe set identifier in another scoped <style> tag. The above code snippet renders a scoped animation as follows:

A CSS animation made with scoped CSS

A CSS animation made with scoped CSS

Integrating css-scope-inline with other JavaScript libraries

The css-scope-inline library scopes <style> tags using the MutationObserver API, so it will detect and scope dynamically added <style> tags with JavaScript libraries like HTMX, Surreal, and JQuery.

Look at the following HTMX code snippet:

Action button

The above code snippet clones a button that contains a scoped style by making an HTTP request to the same HTML file itself. The css-scope-inline seamlessly works with HTMX and creates new elements without FOUC (Flash Of Unstyled Content).

Look at the following preview:

Cloning a button with scoped CSS using HTMX without FOUC occurring

Cloning a button with scoped CSS using HTMX without FOUC occurring

Conclusion

In this article, we explored the minimal css-scope-inline library and practically implemented several scoped CSS examples. This library helps you scope inline <style> tags with no build steps. It offers a simple custom CSS selector me (and two aliases) to select the scoped element without asking developers to add unique CSS classes or DOM identifiers manually.

css-scope-inline also offers productivity-focused features, such as responsive design shortcuts, using custom scoping logic. So, css-scope-inline is the most suitable method for writing scoped CSS for vanilla HTML webpages at this time.

Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.

LogRocket Dashboard Free Trial Banner

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — start monitoring for free.