<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>
<em>Resize window to switch view from grid to table.</em>
<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's not working (yet).</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 url("https://unpkg.com/open-props");
@import url("https://unpkg.com/modern-css-reset@1.4.0/dist/reset.min.css");
@layer userdatalist {
.user-datalist-wrapper {
container-type: inline-size;
resize: both;
overflow: scroll;
border: 0.25rem solid darkcyan;
}
.user-datalist {
outline: 1px solid var(--stone-4);
inline-size: 100%;
@container (max-width: 750px) {
display: block;
outline-color: transparent;
}
thead {
text-align: left;
@container (max-width: 750px) {
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);
}
}
tbody {
@container (max-width: 750px) {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(calc(var(--spacer) * 13), 1fr)
);
gap: var(--spacer);
}
tr {
padding: 0;
vertical-align: middle;
background-color: var(--stone-0);
border: 1px solid transparent;
@container (max-width: 749px) {
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 (min-width: 750px) {
background-color: var(--stone-2);
}
}
&:is(:focus-within, :has(td:hover)) {
background-color: var(--indigo-1);
}
&:is(:focus-within) {
outline: var(--outline-size) solid var(--outline-color);
outline-offset: calc(var(--outline-size) * -1);
}
& :where(td) {
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
line-height: var(--font-lineheight-1);
&:last-child {
padding-inline-end: calc(var(--spacer) / 2);
}
@container (max-width: 749px) {
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: center;
padding-inline-end: calc(var(--spacer) / 1.5);
}
}
}
}
}
}
:where(.user-datalist
td:not(.user-dataitem__thumbnail, .user-dataitem__actions)) {
@container (max-width: 749px) {
margin-block: calc(var(--size-1) / 2);
}
}
.user-dataitem__thumbnail {
grid-area: thumbnail;
@container (max-width: 749px) {
background-color: var(--stone-3);
}
}
.user-dataitem__image {
inline-size: 100%;
@container (min-width: 750px) {
max-inline-size: none;
inline-size: calc(var(--spacer) * 2);
block-size: calc(var(--spacer) * 2);
}
}
td.user-dataitem__full-name {
grid-area: full-name;
@container (max-width: 749px) {
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 (max-width: 749px) {
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 (max-width: 749px) {
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;
align-items: end;
justify-content: center;
outline: none;
}
}
@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);
}
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) {
document.querySelector(".user-datalist-type").style.setProperty("--user-datalist-view", e.target.dataset.viewType);
}
});
function formatFullName(user) {
return `${user.name.first} ${user.name.last}`;
}
const getUserDatalistTemplate = (users) => `<div class="user-datalist-type">
<table class="user-datalist">
<thead>
<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>
${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 xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8l-8-8z"/></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.