<div class="view-selector">
<button id="viewAsTable" data-view-type="table">View as table</button>
<button id="viewAsGrid" data-view-type="grid">View as grid</button>
</div>
<div class="user-datalist-wrapper"></div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="arrow-right" fill="none" viewBox="0 0 24 24" strokewidth="1.5" stroke="currentColor">
<path strokelinecap="round" strokelinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</symbol>
<symbol id="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokewidth="1.5">
<path d="M4.5 12.75L10.5 18.75L19.5 5.25" stroke-linecap="round" stroke-linejoin="round"></path>
</symbol>
</svg>
<div class="prose">
<p>This is an experiment where I attempt to switch tabular data views from a grid view to a table view with the power of style container queries. Unfortunatly, it needs a setTimeout of 0 JS workaround to make it work.</p>
<p>For this experiment, I compare 3 different approaches:</p>
<ol>
<li><a href="https://codepen.io/mrtnvh/pen/KKxeLrX">Style container queries + HTML table (Prefered solution -> Semantic markup + CSS for visual layout)</a></li>
<li><a href="https://codepen.io/mrtnvh/pen/yLxmEQM">Style container queries + HTML divs + CSS Grid</a></li>
<li><a href="https://codepen.io/mrtnvh/pen/dyqxBVx">Media queries + HTML table</a></li>
</ol>
</p>
</div>
@layer variables, root, userdatalist, utilities;
@import "https://unpkg.com/open-props";
@import "https://unpkg.com/modern-css-reset@1.4.0/dist/reset.min.css";
@layer userdatalist {
.user-datalist-type {
container-type: normal;
--user-datalist-view: grid; /* grid | table */
}
.user-datalist {
outline: 1px solid var(--stone-4);
inline-size: 100%;
@container style(--user-datalist-view: grid) {
display: block;
outline-color: transparent;
}
}
.user-datalist-head {
text-align: left;
@container style(--user-datalist-view: grid) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
th {
padding-block: calc(var(--spacer) / 4);
}
}
.user-datalist-body {
@container style(--user-datalist-view: grid) {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(calc(var(--spacer) * 13), 1fr)
);
gap: var(--spacer);
}
}
.user-dataitem {
padding: 0;
vertical-align: middle;
background-color: var(--stone-0);
border: 1px solid transparent;
@container style(--user-datalist-view: table) {
display: table-row;
}
@container style(--user-datalist-view: grid) {
display: grid;
grid-template:
"full-name thumbnail" auto
"date-of-birth thumbnail" auto
"phone thumbnail" auto
"email actions" auto / auto calc(var(--spacer) * 3);
padding-inline-start: var(--spacer);
border-color: var(--stone-4);
}
&:nth-child(odd) {
@container style(--user-datalist-view: table) {
background-color: var(--stone-2);
}
}
&:is(:focus-within, :has(td:hover)) {
background-color: var(--indigo-1);
cursor: pointer;
}
&:is(:focus-within) {
outline: var(--outline-size) solid var(--outline-color);
outline-offset: calc(var(--outline-size) * -1);
}
& :where(> *) {
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
line-height: var(--font-lineheight-1);
@container style(--user-datalist-view: table) {
padding: calc(var(--spacer) / 8);
vertical-align: middle;
cursor: pointer;
white-space: nowrap;
&:first-child {
padding-inline-start: 0;
padding-block: 0;
}
&:last-child {
text-align: end;
padding-inline-end: calc(var(--spacer) / 1.5);
}
}
}
}
:where(.user-datalist
td:not(.user-dataitem__thumbnail, .user-dataitem__actions)) {
@container style(--user-datalist-view: grid) {
margin-block: calc(var(--size-1) / 2);
}
}
.user-dataitem__thumbnail {
grid-area: thumbnail;
@container style(--user-datalist-view: grid) {
background-color: var(--stone-3);
}
}
.user-dataitem__image {
inline-size: 100%;
@container style(--user-datalist-view: table) {
max-inline-size: none;
inline-size: calc(var(--spacer) * 2);
block-size: calc(var(--spacer) * 2);
}
}
.user-dataitem__full-name {
grid-area: full-name;
@container style(--user-datalist-view: grid) {
margin-block-start: var(--spacer);
font-size: var(--size-5);
font-weight: 900;
line-height: var(--font-lineheight-0);
}
}
.user-dataitem__email {
grid-area: email;
margin-block-end: var(--spacer);
}
.user-dataitem__date-of-birth {
grid-area: date-of-birth;
@container style(--user-datalist-view: grid) {
font-size: var(--font-size-0);
margin-block-end: calc(var(--size-1));
color: var(--stone-8);
}
}
.user-dataitem__phone {
grid-area: phone;
}
.user-dataitem__actions {
text-align: center;
@container style(--user-datalist-view: grid) {
grid-area: actions;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: var(--stone-3);
&:has(tr:hover) {
background-color: var(--indigo-3);
}
}
}
.user-dataitem__action--detail {
display: flex;
flex-direction: column;
justify-content: center;
outline: none;
@container style(--user-datalist-view: grid) {
align-items: end;
}
}
}
@layer variables {
/* Variables */
:root {
--brand-primary: var(--indigo-8);
--spacer: clamp(1.5rem, 3vw, 2rem);
--container-spacer: clamp(1rem, 6vw, 4.5rem);
--shadow-strength: 2.5%;
--outline-size: 0.125rem;
--outline-color: var(--indigo-6);
--outline-offset: 0.25rem;
}
}
@layer root {
:root {
font-size: clamp(15px, 1.5vw, 18px);
}
body {
font-family: Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans",
Arial, sans-serif;
font-size: 1rem;
background-color: var(--stone-1);
color: var(--stone-10);
padding: var(--size-6);
display: flex;
flex-direction: column;
gap: var(--size-6);
}
.prose > * + * {
margin-block: var(--size-3) !important;
}
a {
color: var(--stone-10);
text-decoration: underline;
}
svg {
width: 1.5em;
height: 1.5em;
}
/*
* Outline color
*/
a,
button {
&:focus-visible {
outline-color: var(--outline-color);
outline-offset: var(--outline-offset);
}
}
table {
border-collapse: collapse;
}
}
@layer utilities {
.visually-hidden:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
}
View Compiled
document.documentElement.addEventListener("click", (e) => {
if (e.target.dataset.viewType) {
$userDataListType = document.querySelector(".user-datalist-type");
// Hack to make it work. Make the container invisible and show it again. Currently works with timeout 0.
$userDataListType.style.display = "none";
$userDataListType.style.setProperty(
"--user-datalist-view",
e.target.dataset.viewType
);
setTimeout(() => {
$userDataListType.style.display = "block";
}, 0);
}
});
function formatFullName(user) {
return `${user.name.first} ${user.name.last}`;
}
const getUserDatalistTemplate = (users) => `<div class="user-datalist-type">
<table class="user-datalist">
<thead class="user-datalist-head">
<tr>
<th><span class="visually-hidden">Picture</span></th>
<th>Name</th>
<th>Email</th>
<th>Date of birth</th>
<th>Phone</th>
<th><span class="visually-hidden">Actions</span></th>
</tr>
</thead>
<tbody class="user-datalist-body">
${users
?.map((user) => {
const fullName = formatFullName(user);
const dateOfBirth = new Date(user.dob.date);
const detailUrl = `/users/${user.login.uuid}`;
return `<tr key="${
user.login.uuid
}" class="user-dataitem" data-href="${detailUrl}">
<td class="user-dataitem__thumbnail">
<img class="user-dataitem__image" src="${
user.picture.thumbnail
}" alt="Picture of ${fullName}" />
</td>
<td class="user-dataitem__full-name">${fullName}</td>
<td class="user-dataitem__email">${user.email}</td>
<td class="user-dataitem__date-of-birth">${dateOfBirth.toLocaleDateString()}</td>
<td class="user-dataitem__phone">${user.phone}</td>
<td class="user-dataitem__actions">
<a class="user-dataitem__action--detail" href="${detailUrl}">
<svg>
<use xlink:href="#arrow-right" />
</svg>
<span class="visually-hidden">More details about ${fullName}</span>
</a>
</td>
</tr>`;
})
.join("")}
</tbody>
</table>
</div>`;
fetch("https://randomuser.me/api?results=10")
.then((response) => response.json())
.then(({ results }) => {
const wrapper = document.querySelector(".user-datalist-wrapper");
wrapper.innerHTML = getUserDatalistTemplate(results);
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.