flex or grid columns based on (modulo) number of Items

So, I'm on a CMS / frontend where the content team can add a collection of cards (like: this is our contact team, these are our sponsors, ...). These cards are listed by default with max 4 cards in a row. Then the next ones in the next row. Now, here comes the problem: It looks WAY better when if you've got 5 items you don't show 4 in the first row and just 1 in the last. But display 3 in the first one and 2 in the last. Same for 13 items (4,4,3,2) - that's why modulo in the topic . Is there a way to solve this CSS only (at least 2 items in a row) or if not (what I guess) - how would you create the JS and CSS setup (I'm using Vue, I know the numbers of items and can dynamically assign classes based on the modulo). Currently the content team create different collections (a 3 items one and a 2 one) to archive this - but this is not a nice solution. I'm currently thinking of: const rest = items.length % 4 if rest !== 1 -> everything is fine if rest === 1 -> create a second list, where 2 items spliced from the original one and display them in a new flexed <div>
3 Replies
Kevin Powell
Kevin Powell•2y ago
Are you willing to use :has() as a progressive enhancement, and people on older browsers get the sort of ugly grids? If so, you can do it with CSS only. If not, then you'd have to use JS. If you don't mind using :has(), then you can do something like this:
.card-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(var(--col-count, 1), 1fr);
}

/* has 2 children */
.card-grid:has(:nth-child(2):is(:last-child)) {
--col-count: 2;
}

/* has 3 children */
.card-grid:has(:nth-child(3):is(:last-child)) {
--col-count: 3;
}

/* or, you can check if it's divisible by a certain number */

/* child count is divisible by 4 */
.card-grid:has(:nth-child(4n):is(:last-child)) {
--col-count: 4;
}

/* child count is divisible by 5 */
.card-grid:has(:nth-child(5n):is(:last-child)) {
--col-count: 3;
}

/* etc */
.card-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(var(--col-count, 1), 1fr);
}

/* has 2 children */
.card-grid:has(:nth-child(2):is(:last-child)) {
--col-count: 2;
}

/* has 3 children */
.card-grid:has(:nth-child(3):is(:last-child)) {
--col-count: 3;
}

/* or, you can check if it's divisible by a certain number */

/* child count is divisible by 4 */
.card-grid:has(:nth-child(4n):is(:last-child)) {
--col-count: 4;
}

/* child count is divisible by 5 */
.card-grid:has(:nth-child(5n):is(:last-child)) {
--col-count: 3;
}

/* etc */
And just to add some weight to this solution, :has() support is at 87% now... https://caniuse.com/css-has Only issue with the above solution is it just goes to a 1 column layout as a fallback, might be better to have a 3 column layout as a fallback or something?
phyrasaur
phyrasaur•2y ago
@bibamann I did experiment with something similar for someone else on this server. How to avoid orphans in flexbox/grid. Not the cleanest code (I'm more a designer than a coder 😅 ), for same width elements: https://codepen.io/petpeeve/pen/abaRvNj Variable width elements: https://codepen.io/petpeeve/pen/MWqPjyz But I think your requirement is much simpler?
bibamann
bibamannOP•2y ago
@Kevin Thanks, I'll try this out tomorrow! I definitely need a dynamic solution like the "child count is divisible by ..." one. And the CSS only solution doesn't look like the nightmare I expected 😉 @phyrasaur yea, that's somehow my first thought, working with JS using modulo (with the difference of assigning different css classes instead of creating 2 lists). I'll come back on this if Kevins solution doesn't work. But also big thanks! @Kevin "Only issue with the above solution is it just goes to a 1 column layout as a fallback, might be better to have a 3 column layout as a fallback or something?" - I'll see tomorrow @Kevin Well, your solution just gives me 4 or 3 column grid layouts which wasn't what I wanted. However here the solution with some js calculating a grid-column-end as I also needed to center the cards I just did the 3-2 separation for exact 5 elements as for more it looked too much like a reversed pyramid (4-3-2). Here the Vue file (I guess still easy to read / resuse in plain js)
<template>
<div class="card-grid">
<company-card-full
v-for="(company, index) in companies"
:company="company.company"
:class="getGridOffsetClass(index)"
/>
</div>
</template>

<script>
import CompanyCardFull from '../cards/CompanyCardFull.vue';

export default {
name: 'FrontendCompanyCollection',
components: {CompanyCardFull},
props: {
collection: Object
},
computed: {
companies() {
return this.collection.frontendCompanyCollectionCompanies;
}
},
methods: {
getGridOffsetClass(index) {
// special case for 5 elements
if (this.companies.length === 5) {
if (index === 0) {
return 'offset-3'
}
if (index === 3) {
return 'offset-2'
}
// center the last items if needed
} else if (index + 4 > this.companies.length && this.companies.length % 4 !== 0 && index % 4 === 0) {
return 'offset-' + this.companies.length % 4;
}
}
}
}
</script>

<style scoped lang="scss">
.card-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-auto-rows: 1fr;
gap: 1rem;

& > * {
grid-column: span 2;
}
}

.offset-1 {
grid-column-end: -4;
}

.offset-2 {
grid-column-end: -5;
}

.offset-3 {
grid-column-end: -6;
}
</style>
<template>
<div class="card-grid">
<company-card-full
v-for="(company, index) in companies"
:company="company.company"
:class="getGridOffsetClass(index)"
/>
</div>
</template>

<script>
import CompanyCardFull from '../cards/CompanyCardFull.vue';

export default {
name: 'FrontendCompanyCollection',
components: {CompanyCardFull},
props: {
collection: Object
},
computed: {
companies() {
return this.collection.frontendCompanyCollectionCompanies;
}
},
methods: {
getGridOffsetClass(index) {
// special case for 5 elements
if (this.companies.length === 5) {
if (index === 0) {
return 'offset-3'
}
if (index === 3) {
return 'offset-2'
}
// center the last items if needed
} else if (index + 4 > this.companies.length && this.companies.length % 4 !== 0 && index % 4 === 0) {
return 'offset-' + this.companies.length % 4;
}
}
}
}
</script>

<style scoped lang="scss">
.card-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-auto-rows: 1fr;
gap: 1rem;

& > * {
grid-column: span 2;
}
}

.offset-1 {
grid-column-end: -4;
}

.offset-2 {
grid-column-end: -5;
}

.offset-3 {
grid-column-end: -6;
}
</style>
Want results from more Discord servers?
Add your server