DevTools dock side with data-⚓️-side
A DevTools panel docked before/below/after the page it inspects.
<main class="demo">
<header class="intro">
<h1>Dock to side</h1>
<p>
DevTools is anchored to the page with one attribute,
<code>data-⚓️-side</code>. Pick a dock side and the panel swings round;
its size comes from the page via <code>data-⚓️-sync-*</code>,
the swing from <code>data-⚓️-tween</code>. The script only swaps attribute values.
</p>
</header>
<div class="controls">
<fieldset class="seg">
<legend>Dock side</legend>
<label title="Dock to left">
<input type="radio" name="dock" value="before">
<svg viewBox="0 0 20 16" aria-hidden="true"><rect x="1" y="1" width="18" height="14" rx="2"/><rect class="fill" x="1" y="1" width="7" height="14" rx="2"/></svg>
<span>before</span>
</label>
<label title="Dock to bottom">
<input type="radio" name="dock" value="below">
<svg viewBox="0 0 20 16" aria-hidden="true"><rect x="1" y="1" width="18" height="14" rx="2"/><rect class="fill" x="1" y="9" width="18" height="6" rx="2"/></svg>
<span>below</span>
</label>
<label title="Dock to right">
<input type="radio" name="dock" value="after" checked>
<svg viewBox="0 0 20 16" aria-hidden="true"><rect x="1" y="1" width="18" height="14" rx="2"/><rect class="fill" x="12" y="1" width="7" height="14" rx="2"/></svg>
<span>after</span>
</label>
</fieldset>
<label class="xray-toggle"><input type="checkbox" id="xray"> X-ray</label>
</div>
<div class="window">
<div class="chrome" aria-hidden="true">
<span class="dots"><i></i><i></i><i></i></span>
<span class="url">https://anchored.example/</span>
</div>
<div class="viewport" data-⚓️-root>
<article class="page" data-⚓️-name="--page">
<nav class="site-nav"><b>anchored</b><span>Docs</span><span>Blog</span><span>Play</span></nav>
<section class="hero">
<h2>Layout that holds on.</h2>
<p>This page reflows as DevTools takes its space. The panel never measures anything.</p>
</section>
<ul class="cards">
<li>Pins</li><li>Edges</li><li>Side</li><li>Fill</li><li>Tween</li><li>Glide</li>
</ul>
</article>
<section
class="devtools"
aria-label="DevTools"
data-⚓️-to="--page"
data-⚓️-side="after"
data-⚓️-align-top
data-⚓️-sync-width
data-⚓️-sync-height
data-⚓️-tween
data-⚓️-transition="morph"
>
<div class="dt-tabs" aria-hidden="true">
<span class="on">Elements</span><span>Console</span><span>Sources</span><span>Network</span>
</div>
<div class="dt-body"><div class="dt-grid">
<pre class="dom" aria-hidden="true"><span class="t"><body></span>
<span class="t"><article</span> <span class="a">data-⚓️-name</span>=<span class="v">"--page"</span><span class="t">></span>…<span class="t"></article></span>
<span class="sel"><span class="t"><section</span> <span class="a">class</span>=<span class="v">"devtools"</span>
<span class="a">data-⚓️-to</span>=<span class="v">"--page"</span>
<span class="a">data-⚓️-side</span>=<span class="v live"></span><span class="t">></span></span>
<span class="t"></body></span></pre>
<div class="styles" aria-hidden="true">
<p class="pane-title">Styles</p>
<pre><span class="s">[data-⚓️-side="<span class="live-raw"></span>"]</span> {
<span class="p">inset-*</span>: <span class="v">anchor(…)</span>;
}
<span class="s">[data-⚓️-sync-height]</span> {
<span class="p">height</span>: <span class="v">anchor-size(block)</span>;
}</pre>
</div>
</div></div>
</section>
</div>
</div>
</main>/* Demo styling only. Every position comes from data-⚓️-* attributes;
this file sets looks, the dock size and the page's reflow. */
:root {
color-scheme: dark;
--bg: oklch(16% 0.012 260);
--ink: oklch(90% 0.01 260);
--muted: oklch(68% 0.015 260);
--line: oklch(32% 0.02 260);
--accent: oklch(78% 0.14 200);
--ease: cubic-bezier(0.25, 1, 0.5, 1);
background: var(--bg);
color: var(--ink);
font: 400 1rem/1.5 system-ui, sans-serif;
}
body { margin: 0; }
.demo {
max-width: 64rem;
margin-inline: auto;
padding: clamp(1rem, 4vw, 2.5rem);
display: grid;
gap: 1.25rem;
}
.intro h1 { margin: 0 0 0.35rem; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; }
.intro p { margin: 0; color: var(--muted); max-width: 62ch; }
code { font: 0.88em ui-monospace, monospace; color: var(--accent); }
/* ---------- controls ---------- */
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem; }
.seg {
display: flex;
gap: 0.25rem;
margin: 0;
padding: 0.25rem;
border: 1px solid var(--line);
border-radius: 0.75rem;
background: oklch(20% 0.014 260);
}
.seg legend {
float: left;
align-self: center;
padding-inline: 0.5rem 0.4rem;
color: var(--muted);
font-size: 0.85rem;
}
.seg label {
position: relative;
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.45rem 0.7rem;
border-radius: 0.55rem;
cursor: pointer;
font: 500 0.85rem ui-monospace, monospace;
color: var(--muted);
transition: background 0.2s, color 0.2s;
}
.seg input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.seg label:hover { color: var(--ink); }
.seg label:has(:checked) { background: oklch(30% 0.04 220); color: var(--ink); }
.seg label:has(:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.seg svg { width: 1.25rem; fill: none; stroke: currentColor; stroke-width: 1.4; }
.seg svg .fill { fill: currentColor; stroke: none; opacity: 0.55; }
.seg label:has(:checked) svg .fill { fill: var(--accent); opacity: 1; }
.xray-toggle { display: flex; gap: 0.4rem; align-items: center; color: var(--muted); font-size: 0.9rem; cursor: pointer; }
.xray-toggle input { accent-color: var(--accent); }
/* ---------- the browser window ---------- */
.window {
border: 1px solid var(--line);
border-radius: 0.9rem;
overflow: hidden;
background: oklch(21% 0.014 260);
box-shadow: 0 1.5rem 3rem -1rem oklch(0% 0 0 / 0.6);
}
.chrome {
display: flex;
align-items: center;
gap: 0.9rem;
padding: 0.55rem 0.8rem;
border-block-end: 1px solid var(--line);
}
.dots { display: flex; gap: 0.35rem; }
.dots i { width: 0.7rem; aspect-ratio: 1; border-radius: 50%; background: oklch(40% 0.02 260); }
.url {
flex: 1;
max-width: 26rem;
padding: 0.2rem 0.75rem;
border-radius: 99px;
background: oklch(15% 0.012 260);
color: var(--muted);
font: 0.8rem ui-monospace, monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* The viewport is the positioning root; the page is a plain grid item that
gives up room on the docked side. That room size is the only number here,
and the panel reads its own size from the same variables (see .devtools). */
.viewport {
--dock-w: 42%;
--dock-h: 11.5rem;
display: grid;
height: clamp(24rem, 62vh, 34rem);
}
.page {
margin: 0;
overflow: hidden;
padding: clamp(0.8rem, 3vw, 1.5rem);
background: oklch(97% 0.008 90);
color: oklch(25% 0.02 260);
transition: margin 0.5s var(--ease);
}
.viewport:has(> [data-⚓️-side="after"]) > .page { margin-inline-end: var(--dock-w); }
.viewport:has(> [data-⚓️-side="before"]) > .page { margin-inline-start: var(--dock-w); }
.viewport:has(> [data-⚓️-side="below"]) > .page { margin-block-end: var(--dock-h); }
.site-nav { display: flex; gap: 1rem; align-items: baseline; font-size: 0.85rem; color: oklch(45% 0.02 260); }
.site-nav b { margin-inline-end: auto; color: oklch(25% 0.02 260); font-size: 1rem; }
.hero h2 { margin: 1.4rem 0 0.3rem; font-size: clamp(1.3rem, 3.5vw, 2.1rem); line-height: 1.1; letter-spacing: -0.02em; text-wrap: balance; }
.hero p { margin: 0; color: oklch(42% 0.02 260); font-size: 0.9rem; max-width: 40ch; }
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
gap: 0.6rem;
margin: 1.2rem 0 0;
padding: 0;
list-style: none;
}
.cards li {
display: grid;
place-items: end start;
min-height: 4.25rem;
padding: 0.5rem 0.6rem;
border-radius: 0.6rem;
background: oklch(90% 0.05 calc(150 + sibling-index() * 40));
font: 600 0.8rem ui-monospace, monospace;
}
/* ---------- DevTools ----------
Size = (page size * mult) + offset, from data-⚓️-sync-width/height.
Docked beside the page it matches the page's height and takes --dock-w;
docked below it matches the page's width and takes --dock-h. */
.devtools {
--⚓️-mult-x: 0;
--⚓️-offset-x: var(--dock-w);
--⚓️-mult-y: 1;
--⚓️-offset-y: 0px;
--side: attr(data-⚓️-side);
display: flex;
flex-direction: column;
overflow: hidden;
background: oklch(19% 0.012 260);
color: oklch(85% 0.01 260);
font: 0.78rem/1.55 ui-monospace, monospace;
border-inline-start: 1px solid var(--line);
}
.devtools[data-⚓️-side="before"] { border-inline: 0 1px solid var(--line); border-inline-end: 1px solid var(--line); }
.devtools[data-⚓️-side="below"] {
--⚓️-mult-x: 1;
--⚓️-offset-x: 0px;
--⚓️-mult-y: 0;
--⚓️-offset-y: var(--dock-h);
border-inline: 0;
border-block-start: 1px solid var(--line);
}
.dt-tabs { display: flex; gap: 0.2rem; padding: 0.3rem 0.4rem 0; border-block-end: 1px solid var(--line); background: oklch(22% 0.014 260); white-space: nowrap; overflow: hidden; }
.dt-tabs span { padding: 0.25rem 0.55rem; color: var(--muted); }
.dt-tabs .on { color: var(--ink); box-shadow: inset 0 -2px var(--accent); }
.dt-body {
flex: 1;
min-height: 0;
container-type: inline-size;
overflow: auto;
}
.dt-grid { display: grid; min-height: 100%; grid-template-rows: auto 1fr; }
@container (width > 34rem) {
.dt-grid { grid-template-columns: 1.4fr 1fr; grid-template-rows: none; }
.styles { border-block-start: 0 !important; border-inline-start: 1px solid var(--line); }
}
.dt-body pre { margin: 0; padding: 0.6rem 0.75rem; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
.dom .t { color: oklch(72% 0.13 300); }
.dom .a { color: oklch(78% 0.11 60); }
.dom .v, .styles .v { color: oklch(78% 0.12 160); }
.dom .sel { display: inline-block; margin-inline: -0.75rem; padding-inline: 0.75rem; background: oklch(30% 0.05 240 / 0.6); }
.dom .live::before { content: "\"" var(--side) "\""; }
.styles { border-block-start: 1px solid var(--line); }
.pane-title { margin: 0; padding: 0.35rem 0.75rem; color: var(--muted); border-block-end: 1px solid var(--line); }
.styles .s { color: oklch(80% 0.1 90); }
.styles .p { color: oklch(76% 0.1 230); }
.styles .live-raw::before { content: var(--side); }
/* ---------- X-ray: outline anchors, label the live side ---------- */
.demo:has(#xray:checked) [data-⚓️-name] { outline: 2px dashed var(--accent); outline-offset: -2px; }
.demo:has(#xray:checked) [data-⚓️-to] { outline: 2px solid oklch(75% 0.17 330); outline-offset: -2px; }
.demo:has(#xray:checked) .devtools::after {
content: "side=" var(--side);
position: absolute;
inset-block-start: 0.4rem;
inset-inline-end: 0.4rem;
padding: 0.1rem 0.45rem;
border-radius: 0.3rem;
background: oklch(75% 0.17 330);
color: oklch(15% 0.02 330);
font: 600 0.75rem ui-monospace, monospace;
}
.demo:has(#xray:checked) .page::after {
content: "--page";
position: absolute;
margin: 0.4rem;
padding: 0.1rem 0.45rem;
border-radius: 0.3rem;
background: var(--accent);
color: oklch(15% 0.02 220);
font: 600 0.75rem ui-monospace, monospace;
inset-block-start: 0;
inset-inline-start: 0;
}
.page { position: relative; }
@media (prefers-reduced-motion: reduce) {
.page, .seg label { transition: none; }
}
@media (width < 36rem) {
.viewport { --dock-w: 48%; --dock-h: 10rem; }
.seg span { display: none; }
}// State only: the dock side is one attribute value. The cross-axis edge the
// panel lines up with (top when beside the page, leading when below it) is
// the other attribute that changes. No measuring, no position maths.
const panel = document.querySelector(".devtools");
const SIDE = "data-⚓️-side";
document.querySelector(".seg").addEventListener("change", (e) => {
const side = e.target.value;
panel.setAttribute(SIDE, side);
panel.toggleAttribute("data-⚓️-align-leading", side === "below");
panel.toggleAttribute("data-⚓️-align-top", side !== "below");
});data-⚓️-align-leadingdata-⚓️-align-topdata-⚓️-namedata-⚓️-rootdata-⚓️-sidedata-⚓️-sync-heightdata-⚓️-sync-widthdata-⚓️-todata-⚓️-transitiondata-⚓️-tween