details tabs
a tabbed element made from details elements. javascript can be added to ensure a tab is always visible (force), and to remember the open tab across reloads (keep).
"hi"
"bye""hi"
"hi!""hi"
"what's up?"other elements can sit in the tab row between the details, and dense shrinks the tabs to their content.
semi keeps the other elements at their content size, while the details share the remaining space equally.
webc
<div :class="['detabs', ...['force', 'dense', 'nb', 'semi', 'keep'].filter(k => (this[k] ?? false) !== false)].join(' ')">
<slot></slot>
</div>
<style webc:is="css.detabs" webc:nokeep></style>
<script webc:is="js.detabs" webc:nokeep></script>html
<div class="detabs force">
<details open name="*">
<summary>"hi"</summary>
"bye"
</details>
<details name="*">
<summary>"hi"</summary>
"hi!"
</details>
<details open name="*">
<summary>"hi"</summary>
"what's up?"
</details>
</div>css
.detabs {
&:has(> :nth-child(1)) { --tabs: 1; }
&:has(> :nth-child(2)) { --tabs: 2; }
&:has(> :nth-child(3)) { --tabs: 3; }
&:has(> :nth-child(4)) { --tabs: 4; }
&:has(> :nth-child(5)) { --tabs: 5; }
&:has(> :nth-child(6)) { --tabs: 6; }
&:has(> :nth-child(7)) { --tabs: 7; }
&:has(> :nth-child(8)) { --tabs: 8; }
&:has(> :nth-child(9)) { --tabs: 9; }
display: grid;
container-type: inline-size;
overflow-x: auto;
scrollbar-width: thin;
grid-template: auto / repeat(var(--tabs), minmax(max-content, 1fr));
&.dense {
grid-template: auto / repeat(var(--tabs), max-content) 1fr;
}
&.semi {
&:has(> :nth-child(1)) { --col-1: max-content; }
&:has(> :nth-child(2)) { --col-2: max-content; }
&:has(> :nth-child(3)) { --col-3: max-content; }
&:has(> :nth-child(4)) { --col-4: max-content; }
&:has(> :nth-child(5)) { --col-5: max-content; }
&:has(> :nth-child(6)) { --col-6: max-content; }
&:has(> :nth-child(7)) { --col-7: max-content; }
&:has(> :nth-child(8)) { --col-8: max-content; }
&:has(> :nth-child(9)) { --col-9: max-content; }
&:has(> details:nth-child(1)) { --col-1: minmax(max-content, 1fr); }
&:has(> details:nth-child(2)) { --col-2: minmax(max-content, 1fr); }
&:has(> details:nth-child(3)) { --col-3: minmax(max-content, 1fr); }
&:has(> details:nth-child(4)) { --col-4: minmax(max-content, 1fr); }
&:has(> details:nth-child(5)) { --col-5: minmax(max-content, 1fr); }
&:has(> details:nth-child(6)) { --col-6: minmax(max-content, 1fr); }
&:has(> details:nth-child(7)) { --col-7: minmax(max-content, 1fr); }
&:has(> details:nth-child(8)) { --col-8: minmax(max-content, 1fr); }
&:has(> details:nth-child(9)) { --col-9: minmax(max-content, 1fr); }
grid-template: auto / var(--col-1,) var(--col-2,) var(--col-3,) var(--col-4,) var(--col-5,) var(--col-6,) var(--col-7,) var(--col-8,) var(--col-9,);
}
& > :nth-child(1) { --tab: 1; }
& > :nth-child(2) { --tab: 2; }
& > :nth-child(3) { --tab: 3; }
& > :nth-child(4) { --tab: 4; }
& > :nth-child(5) { --tab: 5; }
& > :nth-child(6) { --tab: 6; }
& > :nth-child(7) { --tab: 7; }
& > :nth-child(8) { --tab: 8; }
& > :nth-child(9) { --tab: 9; }
&.nb:has(details[open])::before {
content: '';
grid-area: 1 / 1 / 2 / -1;
border-bottom: 1px solid var(--mix-2);
pointer-events: none;
}
&:not(.nb)::before {
content: '';
grid-area: 1 / 1 / 2 / -1;
border-bottom: 1px solid var(--mix-2);
pointer-events: none;
}
& > :not(details) {
grid-area: 1 / var(--tab) / 2 / span 1;
}
& > :is(span, a) {
padding: calc(0.25ex + 1px) 1ch calc(0.35ex + 1px) calc(1ch + 1px);
}
& > details {
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
grid-area: 1 / 1 / 3 / -1;
pointer-events: none;
summary {
pointer-events: auto;
grid-area: 1 / var(--tab) / 2 / span 1;
display: grid;
cursor: pointer;
padding: 0.25ex 1ch 0.35ex;
background: var(--mix-1);
border: solid var(--mix-2);
border-width: 1px 0 1px 1px;
}
&::details-content {
grid-area: 2 / 1 / 3 / -1;
position: sticky;
inset-inline-start: 0;
inline-size: 100cqi;
box-sizing: border-box;
}
&[open] > summary {
background: var(--background);
border-bottom-width: 0;
}
&[open]::details-content {
pointer-events: auto;
border: solid var(--mix-2);
border-width: 0 1px 1px 1px;
background: var(--background);
padding: .5ex 1ch;
}
&:not(:has(+ details)) > summary {
border-right-width: 1px;
}
}
}javascript
document.querySelectorAll('.detabs.keep').forEach((tabs, i) => {
const key = `detabs:${location.pathname}:${i}`;
const all = [...tabs.querySelectorAll(':scope > details')];
try {
const saved = all[sessionStorage.getItem(key)];
if (saved) all.forEach(d => d.open = d === saved);
} catch {}
all.forEach((d, n) => d.addEventListener('toggle', () => {
try {
if (d.open) sessionStorage.setItem(key, n);
else if (!all.some(o => o.open)) sessionStorage.removeItem(key);
} catch {}
}));
});
document.querySelectorAll('.detabs.force').forEach(tabs => {
const all = [...tabs.querySelectorAll(':scope > details')];
const first = all.find(d => d.open) ?? all[0];
if (first) first.open = true;
tabs.addEventListener('click', e => {
if (e.target.closest('summary')?.parentNode.open) e.preventDefault();
});
});