๐ CSS Scope Inline
(Art by shahabalizadeh)
Why does this exist?
- You want an easy inline vanilla CSS experience without Tailwind CSS.
- Hate creating unique class names over.. and over.. to use once.
- You want to co-locate your styles for โก๏ธ Locality of Behavior (LoB)
- You wish
this
would work in<style>
tags. - Want all CSS features: Nesting, animations. Get scoped
@keyframes
! - You wish
@media
queries were shorter for responsive design. - Only 16 lines. No build step. No dependencies.
- Pairs well with htmx and Surreal
- Want fewer layers, less complexity. Are aware of the cargo cult.
โ๏ธ
โจ Want to also scope your <script>
tags? See our companion project Surreal
๐๏ธ How does it look?
<div>
<style>
me { background: red; } /* โจ this & self also work! */
me button { background: blue; } /* style child elements inline! */
</style>
<button>I'm blue</button>
</div>
See the Live Example! Then view source.
๐ How does it work?
This uses MutationObserver
to monitor the DOM, and the moment a <style>
tag is seen, it scopes the styles to whatever the parent element is. No flashing or popping.
This method also leaves your existing styles untouched, allowing you to mix and match at your leisure.
๐ Install
โ๏ธ copy + ๐ paste the snippet into <script>
in your <head>
Or, ๐ฅ download into your project, and add <script src="script.js"></script>
in your <head>
Or, ๐ CDN: <script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
๐ค Why consider this over Tailwind CSS?
Use whatever you'd like, but there's a few advantages with this approach over Tailwind, Twind, UnoCSS:
- No more repeating styles on child elements (..no @apply, no
[&>thing]
per style). It's just CSS! - No endless visual noise on every
<div>
. Use a local<style>
per group. - No high risk of eventually requiring a build step.
- No chance of deprecations. 16 lines is infinitely maintainable.
- Get the ultra-fast "inspect, play with styles, paste" workflow back.
- No suffering from FOUC (a flash of unstyled content).
- Zero friction movement of styles between inline and
.css
files. Just replaceme
- No special tooling or plugins to install. Universal vanilla CSS.
โก Workflow Tips
- Flat, 1 selector per line can be very short like Tailwind. See the examples.
- Use just plain CSS variables in your design system.
- Use the short
@media
queries for responsive design.- Mobile First (flow: above breakpoint): ๐ข None
sm
md
lg
xl
xx
๐ - Desktop First (flow: below breakpoint): ๐
xs-
sm-
md-
lg-
xl-
๐ข None - ๐ข = No breakpoint. Default. See the Live Example!
- Based on Tailwind breakpoints. We use
xx
not2xl
to not break CSS highlighters. - Unlike Tailwind, you can nest your @media styles!
- Mobile First (flow: above breakpoint): ๐ข None
- Positional selectors may be easier using
div[n1]
for<div n1>
instead ofdiv:nth-child(1)
- Try tools like- Auto complete styles: VSCode or Sublime
๐๏ธ CSS Scope Inline vs Tailwind CSS Showdowns
Basics
Tailwind verbosity goes up with more child elements.
<!-- CSS Scope Inline -->
<div>
<style>
me { background: red; }
me div { background: green; }
me div[n1] { background: yellow; }
me div[n2] { background: blue; }
</style>
red
<div>green</div>
<div>green</div>
<div>green</div>
<div n1>yellow</div>
<div n2>blue</div>
<div>green</div>
<div>green</div>
</div>
<!-- Tailwind -->
<div class="bg-[red]">
red
<div class="bg-[green]">green</div>
<div class="bg-[green]">green</div>
<div class="bg-[green]">green</div>
<div class="bg-[yellow]">yellow</div>
<div class="bg-[blue]">blue</div>
<div class="bg-[green]">green</div>
<div class="bg-[green]">green</div>
</div>
CSS variables and child styling
<!doctype html>
<html>
<head>
<style>
:root {
--color-1: hsl(0 0% 88%);
--color-1-active: hsl(214 20% 70%);
}
</style>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
</head>
<body>
<!-- CSS Scope Inline -->
<div>
<style>
me { margin:8px 6px; }
me div a { display:block; padding:8px 12px; margin:10px 0; background:var(--color-1); border-radius:10px; text-align:center; }
me div a:hover { background:var(--color-1-active); color:white; }
</style>
<div><a href="#">Home</a></div>
<div><a href="#">Team</a></div>
<div><a href="#">Profile</a></div>
<div><a href="#">Settings</a></div>
<div><a href="#">Log Out</a></div>
</div>
<!-- Tailwind Example 1 -->
<div class="mx-2 my-4">
<div><a href="#" class="block py-2 px-3 my-2 bg-[--color-1] rounded-lg text-center hover:bg-[--color-1-active] hover:text-white">Home</a></div>
<div><a href="#" class="block py-2 px-3 my-2 bg-[--color-1] rounded-lg text-center hover:bg-[--color-1-active] hover:text-white">Team</a></div>
<div><a href="#" class="block py-2 px-3 my-2 bg-[--color-1] rounded-lg text-center hover:bg-[--color-1-active] hover:text-white">Profile</a></div>
<div><a href="#" class="block py-2 px-3 my-2 bg-[--color-1] rounded-lg text-center hover:bg-[--color-1-active] hover:text-white">Settings</a></div>
<div><a href="#" class="block py-2 px-3 my-2 bg-[--color-1] rounded-lg text-center hover:bg-[--color-1-active] hover:text-white">Log Out</a></div>
</div>
<!-- Tailwind Example 2 -->
<div class="mx-2 my-4
[&_div_a]:block [&_div_a]:py-2 [&_div_a]:px-3 [&_div_a]:my-2 [&_div_a]:bg-[--color-1] [&_div_a]:rounded-lg [&_div_a]:text-center
[&_div_a:hover]:bg-[--color-1-active] [&_div_a:hover]:text-white">
<div><a href="#">Home</a></div>
<div><a href="#">Team</a></div>
<div><a href="#">Profile</a></div>
<div><a href="#">Settings</a></div>
<div><a href="#">Log Out</a></div>
</div>
</body>
</html>
๐ Technical FAQ
- Why do you use
querySelectorAll()
and not just process theMutationObserver
results directly?- Processing
MutationObserver
results will work well until you begin recieving subtrees (ex: DOM swap, htmx, ajax, jquery) which requires you to walk all subtree child elements to not miss a<style>
. This can involve re-scanning thousands of repeated elements, andquerySelectorAll()
ends up the simplicty and performance winner.
- Processing