<h1><code>CSSRule</code> Debugger</h1>
<p><em>How does a browser interpet your CSS?</em></p>
<h2>Input CSS</h2>
<p>Load one of the pre-built snippets, or write your own <code>CSSRule</code></p>
<button data-id=0>Load snippet #0</button>
<button data-id=1>Load snippet #1</button>
<button data-id=2>Load snippet #2</button>
<style block>.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}</style>
<h2>Visual output</h2>
<div class="foo">I am foo</div>
<h2>Parsed CSS</h2>
<div class="warning"><p>⚠️ Your browser does not support <code>CSSNestedDeclarations</code> so expect some differences compared to the input.</p></div>
<pre id="parsed"></pre>
<h2>Serialized CSS</h2>
<pre id="serialized"></pre>
<div class="warning warning--info" style="display: block"><p>ℹ️ The <code>specificity</code> and <code>resolvedSelectorText</code> attributes included in the serialized output are not part of the CSSOM’s <code>CSSStyleRule</code> and were added for demonstration purposes.</p></div>
<footer><p>This page is a demo for <a href="https://web.dev/blog/css-nesting-cssnesteddeclarations" target="_top">https://web.dev/blog/css-nesting-cssnesteddeclarations</a></p></footer>
@layer layout {
@layer general {
html {
font-family: sans-serif;
line-height: 1.42;
}
body {
padding: 3rem 0 10rem;
width: 90%;
max-width: 52em;
margin: 0 auto;
}
h2 {
margin-top: 2em;
}
a {
color: #0000aa;
text-decoration: none;
&:hover {
color: blue;
}
}
footer {
text-align: center;
font-style: italic;
margin: 4em 0;
}
}
@layer code {
style[contenteditable], script[block], pre {
display: block;
white-space: pre;
border: 1px solid #dedede;
border-left: 0.5em solid LinkText;
background: #fafafa;
padding: 0.75em;
font-family: monospace;
overflow-x: auto;
margin: 1em 0;
tab-size: 2;
font-size: 1.25em;
border-radius: 0.25em;
tab-size: 2;
}
code:not(pre code), /* output:not(code:has(output) output) */ {
background: #f7f7f7;
border: 1px solid rgb(0 0 0 / 0.2);
padding: 0.1rem 0.3rem;
margin: 0.1rem 0;
border-radius: 0.2rem;
display: inline-block;
}
}
@layer warning {
.warning {
display: none;
box-sizing: border-box;
padding: 1em;
margin: 1em 0;
border: 1px solid #ccc;
background: rgba(255 255 205 / 0.8);
}
.warning > :first-child {
margin-top: 0;
}
.warning > :last-child {
margin-bottom: 0;
}
.warning a {
color: blue;
}
.warning--info {
border: 1px solid #123456;
background: rgb(205 230 255 / 0.8);
}
.warning--alarm {
border: 1px solid red;
background: #ff000010;
}
}
}
import { format } from "https://esm.sh/@projectwallace/format-css@1";
import Specificity from "https://esm.sh/@bramus/specificity@2";
// Show warning when there is no `CSSNestedDeclarations` support.
if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
document.querySelector('.warning').style.display = 'block';
}
const examples = [
`
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
`,
`
#foo,
.foo,
.foo::before {
width: fit-content;
background-color: red;
@media screen {
background-color: green;
}
}
.foo::before {
content: '(before)';
margin-right: 1ch;
}
`,
`
.foo {
width: fit-content;
background-color: green;
@starting-style {
opacity: 0;
scale: 0.5;
}
transition: all 0.5s ease;
opacity: 1;
scale: 1;
}
`
];
const go = (exampleIndex = 0) => {
console.clear();
// Use the CSS in the document.
// Use @projectwallace/format-css for the formatting.
const $input = document.querySelector('style[block]');
$input.innerText = format(examples[exampleIndex]);
process();
};
const process = () => {
// The CSSRule to process
const cssRule = document.styleSheets[1].cssRules[0];
console.log(cssRule);
// Re-insert .foo to make sure things like starting-style are visible
const $newFoo = document.createElement('div');
$newFoo.classList.add('foo');
$newFoo.innerText = 'I am foo';
document.querySelector('.foo').replaceWith($newFoo);
// Get the parsed CSS from the cssRule’s cssText.
// Use @projectwallace/format-css for the formatting.
const $parsed = document.querySelector('pre#parsed');
$parsed.innerText = format(cssRule?.cssText ?? '');
// Manually build the structure of the cssRule
const $serialized = document.querySelector('pre#serialized');
$serialized.innerText = serialize(cssRule);
}
const ruleTypes = {
1: 'STYLE_RULE',
2: 'CHARSET_RULE',
3: 'IMPORT_RULE',
4: 'MEDIA_RULE',
5: 'FONT_FACE_RULE',
6: 'PAGE_RULE',
9: 'MARGIN_RULE',
10: 'NAMESPACE_RULE',
};
const extractDeclarationsFromStyle = (style) => {
return Array.from(style)
.map(p => `${p}: ${style[p]}`)
};
// Polyfill for https://github.com/w3c/csswg-drafts/issues/10246
const resolveSelector = (cssRule) => {
let selector = '';
const selectorTexts = [];
do {
if (cssRule.selectorText) {
selectorTexts.unshift(cssRule.selectorText);
}
cssRule = cssRule.parentRule;
} while (cssRule);
for (let selectorText of selectorTexts) {
selector = selectorText.replaceAll('&', `:is(${selector})`);
}
return selector;
};
const padLines = (text, level = 0) => {
if (level == 0) return text;
return text.split('\n').map(l => ' '.repeat((level) * 2) + l).join('\n');
};
const serialize = (cssRule, level = 0) => {
if (!cssRule) return null;
const { type, selectorText, style, cssRules } = cssRule;
let toReturn = `↳ ${cssRule.constructor.name}\n`;
if (type) {
toReturn += ` .type = ${ruleTypes[type]}\n`;
}
if (selectorText) {
const resolvedSelectorText = resolveSelector(cssRule);
const specificity = Specificity.calculate(resolvedSelectorText);
toReturn += ` .selectorText = "${selectorText}"\n`;
toReturn += ` .resolvedSelectorText = "${resolvedSelectorText}"\n`;
toReturn += ` .specificity = ${specificity}\n`;
}
if (style && style.length) {
toReturn += ` .style (CSSStyleDeclaration, ${style.length}) =\n`;
const declarations = extractDeclarationsFromStyle(style);
for (let declaration of declarations) {
toReturn += ` - ${declaration}\n`;
}
// toReturn += ` \n`;
}
if (cssRules && cssRules.length) {
toReturn += ` .cssRules (CSSRuleList, ${cssRules.length}) =\n`;
for (let childCssRule of cssRules) {
toReturn += ` ${serialize(childCssRule, level + 1)}\n`;
}
toReturn += ` \n`;
}
return padLines(toReturn, level).trim('\n');
}
const $buttons = document.querySelectorAll('button');
$buttons.forEach($button => {
$button.addEventListener('click', (e) => {
e.preventDefault();
go(parseInt($button.dataset.id, 10));
});
});
go(new URL(window.location).searchParams.get('demo') ?? 0);
// Allow live editing of the CSS
// The logic is explained in https://www.bram.us/2024/02/18/custom-highlight-api-for-syntax-highlighting/
const codeBlock = document.querySelector('style[block]');
// Allow only plaintext editing
// Firefox doesn’t do 'plaintext-only', but does do 'true'
codeBlock.setAttribute('contenteditable', 'plaintext-only');
if (codeBlock.contentEditable != 'plaintext-only') {
codeBlock.setAttribute('contenteditable', 'true');
}
codeBlock.addEventListener('keyup', () => {
process();
});
codeBlock.addEventListener('keydown', e => {
// The tab key should insert a tab character
if (e.keyCode == 9) {
document.execCommand('insertHTML', false, ' ');
e.preventDefault();
}
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.