Linked Lists with CSS Anchor Positioning
It’s ideal for settings panels, workflow maps, and form steps where the current choice needs a visible path back to its related items.
1. Create two <ul> elements inside a shared container: one for checkboxes (.items) and one for radio buttons (.options).
<div id="linked-list" class="linked-list">
<!-- Checkbox list — multiple items can be checked simultaneously -->
<ul class="list items" aria-labelledby="items-label">
<li>
<label for="task-1">
<input type="checkbox" id="task-1" class="cb" value="1" checked> Task Alpha
</label>
</li>
<li>
<label for="task-2">
<input type="checkbox" id="task-2" class="cb" value="2"> Task Beta
</label>
</li>
<li>
<label for="task-3">
<input type="checkbox" id="task-3" class="cb" value="3" checked> Task Gamma
</label>
</li>
<li>
<label for="task-4">
<input type="checkbox" id="task-4" class="cb" value="4"> Task Delta
</label>
</li>
</ul>
<!-- Radio list — only one option can be selected at a time -->
<ul class="list options" role="radiogroup" aria-labelledby="options-label">
<li>
<label for="phase-1">
<input type="radio" id="phase-1" name="phase" class="rb" value="1"> Phase A
</label>
</li>
<li>
<label for="phase-2">
<input type="radio" id="phase-2" name="phase" class="rb" value="2"> Phase B
</label>
</li>
<li>
<label for="phase-3">
<input type="radio" id="phase-3" name="phase" class="rb" value="3" checked> Phase C
</label>
</li>
<li>
<label for="phase-4">
<input type="radio" id="phase-4" name="phase" class="rb" value="4"> Phase D
</label>
</li>
<li>
<label for="phase-5">
<input type="radio" id="phase-5" name="phase" class="rb" value="5"> Phase E
</label>
</li>
</ul>
</div> 2. Add the CSS. The two anchoring rules are the heart of the pattern. anchor-name: --checked-option on the checked checkbox label registers it as the connector’s source point. anchor-name: --radio-option on the checked radio li registers it as the target. All segment edges are derived from the anchor() function referencing those two named points.
@layer base, demo;
@layer demo {
.linked-list {
/* — Theming variables — adjust to match your design system — */ --checkbox-checked-border-color: dodgerblue;
--radio-checked-border-color: dodgerblue;
--join-stroke: 1px;
--join-line: var(--join-stroke) dashed dodgerblue; /* Connector line style */ --join-radius: 20px; /* Corner rounding on L-shaped bends */ --gap: 10vw; /* Horizontal gap between the two columns */
position: relative;
display: grid;
align-items: start;
grid-template-columns: 1fr 1fr; /* Two equal columns */ width: min(100%, 800px);
gap: var(--gap);
}
.list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.list > li {
border: 1px solid var(--clr-lines);
display: flex;
align-items: center;
}
.list > li > label {
flex: 1;
padding: .5rem;
cursor: pointer;
}
/* Checked checkbox label registers as the named anchor source */ .list.items li:has(:checked) label {
anchor-name: --checked-option;
/* anchor-scope scopes this name to the local subtree so multiple
checked checkboxes each get their own independent anchor chain */ anchor-scope: --checked-option;
border-color: var(--checkbox-checked-border-color);
}
/* First segment: spans from the right edge of the checkbox to the midpoint column */ .list.items li:has(:checked) label::before,
.list.items li:has(:checked) label::after {
content: '';
position: absolute;
border: var(--join-line);
right: calc(anchor(left --radio-option) + var(--gap) / 2);
left: anchor(right --checked-option); /* Left edge aligns to checkbox right */ }
/* Second segment: spans from the midpoint column to the left edge of the radio option */ .list.items li:has(:checked) label::after {
right: anchor(left --radio-option);
left: calc(anchor(left --radio-option) - var(--gap) / 2 - var(--join-stroke));
}
/* — Routing case 1: checkbox sits ABOVE the radio button — */ @container style(--relation: -1) {
.list.items li:has(:checked) label::before {
border-left-color: transparent;
border-bottom-color: transparent;
border-radius: 0 var(--join-radius) 0 0; /* Rounds the top-right corner */ top: anchor(center --checked-option);
bottom: anchor(top --radio-option);
}
.list.items li:has(:checked) label::after {
border-right-color: transparent;
border-top-color: transparent;
border-radius: 0 0 0 var(--join-radius); /* Rounds the bottom-left corner */ top: calc(anchor(top --radio-option) - var(--join-stroke));
bottom: anchor(center --radio-option);
}
}
/* — Routing case 2: checkbox sits BELOW the radio button — */ @container style(--relation: 1) {
.list.items li:has(:checked) label::before {
border-left-color: transparent;
border-top-color: transparent;
border-radius: 0 0 var(--join-radius) 0; /* Rounds the bottom-right corner */ top: anchor(bottom --radio-option);
bottom: anchor(center --checked-option);
}
.list.items li:has(:checked) label::after {
border-right-color: transparent;
border-bottom-color: transparent;
border-radius: var(--join-radius) 0 0 0; /* Rounds the top-left corner */ top: anchor(center --radio-option);
bottom: calc(anchor(bottom --radio-option) - var(--join-stroke));
}
}
/* — Routing case 3: checkbox and radio share the same vertical index — */ @container style(--relation: 0) {
.list.items li:has(:checked) label::before {
/* Straight horizontal connector — no bends or radius needed */ border-top-color: transparent;
border-right-color: transparent;
border-left-color: transparent;
border-radius: 0;
top: calc(anchor(center --radio-option) - var(--join-stroke));
bottom: calc(anchor(center --checked-option) + var(--join-stroke));
right: anchor(left --radio-option);
left: anchor(right --checked-option);
}
.list.items li:has(:checked) label::after {
display: none; /* Second segment is not needed for a straight line */ }
}
/* Checked radio option registers as the named anchor target */ .list.options li:has(:checked) {
anchor-name: --radio-option;
border-color: var(--radio-checked-border-color);
}
}
/* — Base layer: general layout and color-scheme — */@layer base {
* { box-sizing: border-box; }
:root {
color-scheme: light dark;
--bg-dark: rgb(16, 24, 40);
--bg-light: rgb(248, 244, 238);
--txt-light: rgb(10, 10, 10);
--txt-dark: rgb(245, 245, 245);
--line-light: rgba(0 0 0 / .25);
--line-dark: rgba(255 255 255 / .25);
/* light-dark() picks the first value in light mode, second in dark mode */ --clr-bg: light-dark(var(--bg-light), var(--bg-dark));
--clr-txt: light-dark(var(--txt-light), var(--txt-dark));
--clr-lines: light-dark(var(--line-light), var(--line-dark));
}
body {
background-color: var(--clr-bg);
color: var(--clr-txt);
min-height: 100svh;
margin: 0;
padding: 2rem;
font-family: "Jura", sans-serif;
display: grid;
place-content: center;
gap: 2rem;
}
/* Fallback notice for browsers without anchor-positioning support */ @supports not (position-anchor: --test) {
body::before {
content: "Your browser does not support CSS anchor-positioning";
position: fixed;
top: 2rem;
left: 50%;
translate: -50% 0;
font-size: 0.8rem;
}
}
} 3. Determine whether each checkbox row sits above, below, or at the same index as the currently selected radio option, then write that relationship as a CSS custom property.
const root = document.querySelector('.linked-list');
// Maps the three possible Math.sign() outputs to CSS --relation values
// Index 0 = above (-1), Index 1 = same (0), Index 2 = below (1)
const REL = ['-1', '0', '1'];
function updateRelations() {
// Find the currently selected radio option row
const radioLi = root.querySelector('.options li:has(input:checked)');
if (!radioLi) return;
// Get the zero-based position of that row in the radio list
const radioIndex = [...radioLi.parentElement.children].indexOf(radioLi);
// Write --relation to every checkbox row based on its position vs. the radio
root.querySelectorAll('.items li').forEach((li, i) => {
// Math.sign returns -1 (above), 0 (same level), or 1 (below)
// Adding 1 shifts the result to 0, 1, or 2 for array indexing
li.style.setProperty('--relation', REL[Math.sign(i - radioIndex) + 1]);
});
}
// Re-run on any radio or checkbox change inside the container
root.addEventListener('change', updateRelations);
// Set the initial --relation values on page load
updateRelations(); The post Linked Lists with CSS Anchor Positioning appeared first on CSS Script.
Two Direction Sticky Sidebar is a pure JavaScript utility that implements bi-directional sticky sidebar behavior…
Two Direction Sticky Sidebar is a pure JavaScript utility that implements bi-directional sticky sidebar behavior…
LANSING, MI (WOWO) Governor Gretchen Whitmer has expanded Michigan’s state of emergency as severe weather…
LANSING, MI (WOWO) Advocates and lawmakers are urging Michigan Governor Gretchen Whitmer to grant clemency…
A proof-of-concept (PoC) exploit has been publicly released for a newly disclosed vulnerability in Microsoft’s…
INDIANAPOLIS, IND. (WOWO) State leaders in Indiana are supporting a major new investment aimed at…
This website uses cookies.