<div class="wrapper">
	<header>
		<h1>Compound grid generator</h1>
		<div>
			<p>A little tool to generate compound grids. Enter the number of columns for each of your grids, and they’ll be magically merged into a compound grid. Use the output in your <code>grid-template-columns</code> property when using <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout">CSS Grid</a> for layout.</p>
			<a href="https://www.smashingmagazine.com/2019/07/inspired-design-decisions-pressing-matters/">What is a compound grid?</a>
		</div>
	</header>
	<form class="form">
		<div class="form__group">
			<label for="grid1">Grid 1
				<input type="number" id="grid1" value="2" min="2" max="18" data-input />
			</label>
			<div class="sm-grid" data-sm-grid>
				<div class="sm-grid__item"></div>
				<div class="sm-grid__item"></div>
			</div>
		</div>
		<div class="form__group">
			<label for="grid2">Grid 2
				<input type="number" id="grid2" value="4" min="2" max="18" data-input />
			</label>
			<div class="sm-grid sm-grid--second" data-sm-grid>
				<div class="sm-grid__item"></div>
				<div class="sm-grid__item"></div>
				<div class="sm-grid__item"></div>
				<div class="sm-grid__item"></div>
			</div>
		</div>
		<div class="form__info" data-info>
			<p>One grid is a multiple of the other</p></div>
		<button class="form__button" type="button" data-button>Create grid</button>
	</form>
	<code class="result" data-result>
		1fr 1fr 1fr 1fr
	</code>
	<div class="grid" data-grid>
		<div class="item"></div>
		<div class="item"></div>
		<div class="item"></div>
		<div class="item"></div>
	</div>
</div>
@import url('https://fonts.googleapis.com/css?family=Ubuntu&display=swap');

$bg: #171926;
	
* {
	box-sizing: border-box;
}

p {
	max-width: 70ch;
	line-height: 1.5;
	color: lighten($bg, 80%);
}

a {
	color: lighten($bg, 50%);
	
	&:hover,
	&:focus {
		color: orchid;
	}
}

body {
	font-family: 'Ubuntu', sans-serif;
	background-color: $bg;
	color: white;
	font-size: 1.2rem;
	padding: 1rem 1rem 3rem 1rem;
	min-height: 100vh;
	display: flex;
	align-items: center;
	
	@media (min-width: 50rem) {
		padding: 3rem 3rem 5rem 3rem;
	}
}

.wrapper {
	width: 100%;
	max-width: 75rem;
	margin: 0 auto;
	display: grid;
	grid-template-columns: 1fr;
	gap: 1rem;
	
	@media (min-width: 50rem) {
		grid-template-columns: 1fr 2fr;
		gap: 3rem;
	}
}

header {
	grid-column: 1 / -1;
}

.form {
	@media (min-width: 50rem) {
		grid-column: 1;
		grid-row: 2;
	}
}

label {
	display: flex;
	align-items: baseline;
	margin-bottom: 1rem;
}

input {
	font-size: 1.4rem;
	max-width: 6rem;
	text-align: center;
	margin-left: 1rem;
	border: none;
	padding: 0.5rem;
	background: darken($bg, 10%);
	color: white;
}

.result {
	@media (min-width: 50rem) {
		grid-column: 1;
		grid-row: 3;
	}
}

.form__button {
	font-size: inherit;
	font-family: inherit;
	padding: 0.8rem 1.9rem;
	border: none;
	border-radius: 0.5rem;
	margin-top: 2rem;
	font-weight: 700;
	background-color: turquoise;
}

.grid {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	min-height: 25rem;
	gap: 1.5%;
	
	@media (min-width: 50rem) {
		grid-column: 2;
		grid-row: 2 / span 2;
	}
}

.item {
	background-color: orchid;
	opacity: 0;
	transform: translate3d(0, 0.5rem, 0);
	animation: fadeIn 750ms ease-in-out var(--delay, 0ms) forwards;
	box-shadow: 0.25rem 0.25rem 1rem darken($bg, 5%);
}

@keyframes fadeIn {
	100% { opacity: 1; transform: translate3d(0, 0, 0); };
}

.sm-grid {
	width: 9rem;
	display: grid;
	grid-template-columns: repeat(var(--cols, 2), 1fr);
	grid-auto-rows: 5rem;
	gap: 0.3rem;
	margin-left: 2rem;
}

.sm-grid--second {
	grid-template-columns: repeat(var(--cols, 4), 1fr);
}

.sm-grid__item {
	background-color: rgba(turquoise, 0.3);
}

.form__group {
	margin-bottom: 2rem;
	display: flex;
}

.form__info {
	--primary: #{lighten($bg, 40%)};
	background-color: darken($bg, 3%);
	padding: 0.25rem 1.5rem;
	font-size: 1rem;
	opacity: 0;
	border-radius: 0.3rem;
	position: relative;
	border: 1px solid var(--primary);
	
	&::before {
		content: '\2139';
		position: absolute;
		top: -0.6rem;
		font-size: 1.1rem;
		color: var(--primary);
		border-radius: 50%;
		width: 1.45rem;
		height: 1.45rem;
		border: 2px solid var(--primary);
		text-align: center;
		background-color: $bg;
	}
}
View Compiled
const code = document.querySelector('[data-result]')
const grid = document.querySelector('[data-grid]')
const button = document.querySelector('[data-button]')
const inputs = [...document.querySelectorAll('[data-input]')]
const smallGrids = [...document.querySelectorAll('[data-sm-grid]')]
const infoBox = document.querySelector('[data-info]')

const real_sort = function(a, b) {return a - b}

const getInputValues = () => {
	return inputs.map(input => parseInt(input.value))
}

let grids = getInputValues()

const createArray = (n) => {
	const newArray = new Array(n).fill(1)
	return newArray.map((i, index) => 1 / n * index).slice(1)
}

const initialValues = () => {
	const grid1 = createArray(grids[0])
	const grid2 = createArray(grids[1])
	return [...grid1, ...grid2, 1].sort()
}

const render = (columns) => {
	const gridColumns = columns.map((column) => {
		return `${column}fr`
	}).join(' ')
	
	code.innerHTML = gridColumns
	grid.style.gridTemplateColumns = gridColumns
	renderGridElements(columns)
}

const gridIsMultiple = () => {
	const largestGrid = grids.sort(real_sort)[1]
	const smallestGrid = grids.sort(real_sort)[0]
	
	return largestGrid % smallestGrid === 0
}

const renderGridElements = (columns) => {
	const gridColumnElements = columns.map((el, index) => {
		return `<div class="item" style="--delay: ${index * 70}ms"></div>`
	}).join('')
	
	grid.innerHTML = gridColumnElements
}

const calculateGridColumns = () => {
	const smallestSegment = initialValues().reduce((acc, curr) => {
		let x = 1
		x = curr - acc < x ? curr - acc : x
		return x
	})

	let y = 0
	const tracks = []

	initialValues().map((column, index) => {
		const n = column / smallestSegment
		const roundedTrackValue = parseFloat(n.toFixed(2))
		
		/* Don’t include duplicate values */
		if (!tracks.includes(roundedTrackValue)) {
			return tracks.push(roundedTrackValue)
		}
	})

	const columns = []
	/* Probably refactor this */
	tracks.map((i, index) => {
		/* Multiply each column so it’s at least 1fr */
		const column = (i - y) * 10
		columns.push(column.toFixed(1))
		y = i
	})
	
	/* Get the smallest column, that will be 1fr, others will be n * 1fr */
	const sm = columns.reduce((acc, curr) => acc > curr ? curr : acc)
	const colsFrs = columns.map((col) => Math.round(col / sm))
	
	/* Filter out any that are 0 */
	return colsFrs.filter((i) => i > 0)
}

const buildGrid = (e) => {
	e.preventDefault()
	
	/* If one grid is multiple of the other, find the largest and build that grid */
	if (gridIsMultiple()) {
		const columns = new Array(grids.sort()[1]).fill(1)
		return render(columns)
	}

	return render(calculateGridColumns())
}

button.addEventListener('click', (e) => buildGrid(e))

const renderSmallGridItems = (quantity) => {
	const colsArray = new Array(quantity).fill(1)
	return colsArray.map((el) => {
		return `<div class="sm-grid__item"></div>`
	}).join('')
}

const renderSmallGrid = (index) => {
	const qty = getInputValues()[index]
	
	smallGrids[index].style.setProperty('--cols', qty)
	smallGrids[index].innerHTML = renderSmallGridItems(qty)
}

const renderInfo = () => {
	if (gridIsMultiple()) {
		infoBox.style.opacity = 1
		infoBox.setAttribute('aria-hidden', false)
	} else {
		infoBox.style.opacity = 0
		infoBox.setAttribute('aria-hidden', true)
	}
}

inputs.forEach((input, index) => {
	input.addEventListener('change', () => {
		grids = getInputValues()
		renderSmallGrid(index)
		renderInfo()
	})
})


Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.