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.
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:
- Creating separate vanilla CSS stylesheets and linking them with HTML pages within
<head>
sections using<link>
tags - 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:
<style>
tag scoping
Best practices for inline 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
<style>
tag scoping solutions
Drawbacks of existing inline 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:
Also, it’s possible to use the BEM method and use only CSS classes, as shown in the following code snippet:
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.
@scope
Using the native CSS 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:
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.
<style>
tag scoping
Using a frontend library that supports inline Most frontend frameworks, like Riot and Vue, implement inbuilt, scoped CSS features. For example, look at the following Riot component:
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.
css-scope-inline
offer a better CSS scoping solution?
How does 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 |
css-scope-inline
Highlighted features of 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.
<style>
tags
Creating basic inline scoped 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:
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:
Let’s add a button to the page. Add another scoped CSS-styled segment with the following HTML code:
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:
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:
As you can see, the library accomplished the scoping process with the following steps:
- Add a dynamically generated unique class name for the parent element that holds the
<style>
tag - Replace
me
with the dynamically generated class name in the CSS source segment - 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:
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:
- Using a
<style>
tag within the<head>
section - Creating a separate stylesheet
- 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:
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:
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:
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:
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:
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
css-scope-inline
with other JavaScript libraries
Integrating 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 buttonThe 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
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 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.