There are a number of problems with the obvious attempts; here I enforce a gap on the elements in the layout, in order to take the first, most-obvious approach, that of separating the adjacent borders. This prevents those elements from being placed immediately beside one another, so avoiding the the problem of the visual 'double-width' border effect:
*,
::before,
::after {
box-sizing: border-box;
font: 1rem / 1.5 Roboto, Montserrat, sans-serif;
margin: 0;
padding: 0;
}
section {
width: clamp(50em, 70vw, 1200px);
margin-block: 2em;
margin-inline: auto;
}
.item {
display: flex;
flex-wrap: wrap;
margin-block: 1em;
}
.item.withGap {
gap: 1em;
}
.items {
border: 1px solid red;
width: 200px;
padding: 10px;
}
.item.noGap .items+.items {
border-inline-start-width: 0;
}
.setColumns {
--columns: 5;
--gap: 1em;
--flexBasis: calc((100%/var(--columns) - var(--gap)));
gap: var(--gap);
}
.setColumns .items {
flex-basis: var(--flexBasis);
}
<section>
<div class="item withGap">
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
</div>
</section>
This, of course, is reduces the the way in which you can lay out your elements; to later reduce, or remove, the gap means you have to reconsider the border of the elements being laid out.
The other obvious option is to remove borders on adjacent items:
*,
::before,
::after {
box-sizing: border-box;
font: 1rem / 1.5 Roboto, Montserrat, sans-serif;
margin: 0;
padding: 0;
}
section {
width: clamp(50em, 70vw, 1200px);
margin-block: 2em;
margin-inline: auto;
}
.item {
display: flex;
flex-wrap: wrap;
margin-block: 1em;
}
.item.withGap {
gap: 1em;
}
.items {
border: 1px solid red;
width: 200px;
padding: 10px;
}
.item.noGap .items+.items {
border-inline-start-width: 0;
}
.setColumns {
--columns: 5;
--gap: 1em;
--flexBasis: calc((100%/var(--columns) - var(--gap)));
gap: var(--gap);
}
.setColumns .items {
flex-basis: var(--flexBasis);
}
<section>
<div class="item noGap">
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
</div>
</section>
This can work quite well for the first row of the layout, but if – or when – your layout runs to a second row it means that the first element of the new row is lacking its border-inline-start, and all elements of the second row retain their border-block-start because there's no way (as of writing) to select elements according to the row in which they're laid out, unfortunately.
Another obvious – and somewhat complicated – way is to use JavaScript, of course:
;
const getRows = (haystack, n) => {
let temp = [...haystack],
result = [];
if (n === Infinity) {
return haystack;
}
while (temp.length > 0) {
result.push(
temp.splice(0, n)
);
}
return result;
},
unitsInPixels = (val) => {
let cssVals = cssValue(val),
{
number,
units
} = cssVals,
div = document.createElement('div');
div.style.width = `1${units}`;
div.style.height = `1${units}`;
div.style.margin = 0;
div.style.padding = 0;
div.style.boxSizing = 'border-box';
div.style.position = 'absolute';
div.style.left = '1000%'
document.body.appendChild(div);
let measurements = div.getBoundingClientRect(),
{
width,
height
} = measurements;
div.remove();
return Math.floor(number * Math.min(width, height));
},
cssValue = (value, opts = {}) => {
let settings = {
toInt: false,
toSensibleMinimum: false,
};
Object.keys(opts).forEach(
(key) => settings[key] = opts[key]
);
let n = settings.toInt === true ? parseInt(value, 10) : parseFloat(value),
unit = value.replace(/^(\d+)/, '');
n = settings.toSensibleMinimum === true && n === 0 ? ++n : n;
return {
originalValue: value,
number: n,
units: unit,
numberWithUnits: `${n + (unit ? unit : '')}`,
}
},
styleCellsOf = (targets) => {
if (!targets) {
return false;
}
targets.forEach(
(el) => {
const children = el.children,
details = window.getComputedStyle(el, null),
columns = cssValue(details.getPropertyValue('--columns'), {
toSensibleMinimum: true
}).number,
gap = cssValue(details.getPropertyValue('--gap')),
matrix = getRows(children, columns);
if (unitsInPixels(gap.numberWithUnits) > 0) {
return false;
}
matrix.forEach(
(rowN, rowCounter) => {
rowN.forEach(
(cell, colCounter) => {
if (rowCounter > 0) {
cell.style.borderBlockStartWidth = 0;
}
if (colCounter > 0) {
cell.style.borderInlineStartWidth = 0;
}
});
});
});
};
styleCellsOf([...document.querySelectorAll('.setColumns')]);
window.addEventListener('resize', (e) => {
styleCellsOf([...document.querySelectorAll('.setColumns')])
});
*,
::before,
::after {
box-sizing: border-box;
font: 1rem / 1.5 Roboto, Montserrat, sans-serif;
margin: 0;
padding: 0;
}
section {
width: clamp(50em, 70vw, 1200px);
margin-block: 2em;
margin-inline: auto;
}
.item {
display: flex;
flex-wrap: wrap;
margin-block: 1em;
}
.item.withGap {
gap: 1em;
}
.items {
border: 1px solid red;
width: 200px;
padding: 10px;
}
.item.noGap .items+.items {
border-inline-start-width: 0;
}
.setColumns {
--columns: 5;
--gap: 1em;
--flexBasis: calc((100%/var(--columns) - var(--gap)));
gap: var(--gap);
}
.setColumns .items {
flex-basis: var(--flexBasis);
}
<section>
<div class="item setColumns" style="--columns: 4; --gap: 1em">
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
</div>
<div class="item setColumns" style="--columns: 5; --gap: 0.05em">
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
</div>
<div class="item setColumns" style="--columns: 3; --gap: 0.03em">
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
</div>
<div class="item setColumns" style="--columns: 3; --gap: 0em">
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
<div class="items">
<h4>Card 1</h4>
<span>Card content</span>
</div>
</div>
</section>
But this is potentially expensive, and needs to be explicitly connected to the HTML elements. This also comes with caveats, if you look at the second JavaScript flex-layout (in the demo just above), you may notice a tiny break in the apparently-flowing borders on the block-axis due to the way in which browsers, and monitors, handle sub-pixel rendering. So this approach is, also, imperfect although it does let you use CSS custom properties to style the elements.
It also reduces the purpose of using flex-layout, since it requires an explicitly-set number of columns (this could, of course, be worked around but to do so would seem to make the semi-simple JavaScript rather more complex, unfortunately).
So, again, while it mostly works in the event of a --gap custom property being set to 0, small deviations between, as an example (in my case) between 0em and 0.03em leads to either apparent-doubling of adjacent borders, or slight gaps between borders.
The second and third show gaps of 0.05em and 0.03em; each of which – in my case – seem to show the small notches/gaps in the borders.
Unfortunately, the only solutions are non-ideal.