(Art by shahabalizadeh)
- 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
thiswould work in<style>tags. - Want all CSS features: Nesting, animations. Get scoped
@keyframes! - You wish
@mediaqueries 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
<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.
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 leaves your existing styles untouched. Mix in scoped <style> at your leisure.
βοΈ 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>
Use whatever you'd like, but there's a few advantages with this approach over Tailwind, Twind, UnoCSS:
- No repeat styles on child elements (no more @apply, no
[&>thing]). - No repeat prefixes for media queries, hover, focus, etc.
- No visual noise on every
<div>. Use<style>on groups of elements. - Share real CSS between inline
<style>and.cssfiles. Just replaceme - Regain your "inspect + edit styles + paste" workflow in your browser!
- No suffering from lost syntax highlighting on properties and units.
- No high risk of eventually requiring a build step.
- No chance of deprecations. 16 lines is infinitely maintainable.
- No suffering from FOUC (a flash of unstyled content).
- No special binaries, no add-ons, no plugins to install.
- Use vanilla CSS variables for your design system.
- Flattened
1 rule = 1 linecan be small like Tailwind. See examples - Positional selectors may be easier using
div[n1]for<div n1>(instead ofdiv:nth-child(1)) - Child combinator can be very useful for granular control. Example:
div>button - Use the short
@mediaqueries for responsive design.- Based on Tailwind breakpoints.
- We use
xxnot2xlto avoid breaking CSS highlighters. - Unlike Tailwind, you can nest your @media styles!
- We use
- π’ = Default, no breakpoint required. See the Live Example!
- Mobile First (flows above default)
- π’ Default
xssmmdlgxlxxπ End
- π’ Default
- Desktop First (flows below default)
- π End
xs-sm-md-lg-xl-xx-π’ Default
- π End
- Based on Tailwind breakpoints.
Tailwind verbosity goes up with more child elements.
<!-- CSS Scope Inline -->
<div>
<style>
me { background: red; }
me div { background: green; }
me [n1] { background: yellow; }
me [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>At first glance, Tailwind Example 2 looks very promising! Exciting ...but:
- π΄ Every child style requires an explicit selector.
- Tailwinds' shorthand advantages sadly disappear.
- Any more child styles added in Tailwind will become longer than vanilla CSS.
- This limited example is the best case scenario for Tailwind.
- π΄ Not visible on github: no highlighting for properties and units begins to be painful.
<!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>- Why do you use
querySelectorAll()and not just process theMutationObserverresults directly?- This was indeed the original design; it will work well up until you begin recieving subtrees (ex: DOM swaps with htmx, ajax, jquery, etc.) which requires walking all subtree elements to ensure we do not miss a
<style>. This unfortunately involves re-scanning thousands of repeated elements. This is whyquerySelectorAll()ends up the performance (and simplicity) winner.
- This was indeed the original design; it will work well up until you begin recieving subtrees (ex: DOM swaps with htmx, ajax, jquery, etc.) which requires walking all subtree elements to ensure we do not miss a
- Started change log.
- Fix for domain names ending in
.mewhen usedurl()properties.