DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Building a CSS Grid generator: it's a skeleton of tracks, and you place items on the lines, not the cells

CSS Grid trips people up because they picture cells they draw. It isn't that. A grid is a skeleton of tracks — a list of column widths and a list of row heights — with numbered lines running between them, and you place items by naming lines. I built a live grid generator to make that click: size the tracks, drag items to span, copy the exact grid-template CSS. The clever part is that I never compute a single pixel — the browser's own grid engine does. Here's how it works, no library.

The whole state is tracks + gaps + items

A grid is a list of column tracks, a list of row tracks, two gap sizes, and a list of items. Each track carries a unit and value; each item carries four line numbers — cs/ce for column start/end, rs/re for row start/end. One plain object is the document.

let grid = {
  cols: [ {unit:"fr",v1:1}, {unit:"fr",v1:1}, {unit:"fr",v1:1} ],
  rows: [ {unit:"auto"},    {unit:"fr",v1:1} ],
  colGap: 12, rowGap: 12,
  items: [
    { name:"A", cs:1, ce:2, rs:1, re:2 },   // top-left cell
    { name:"B", cs:2, ce:4, rs:2, re:3 }    // spans cols 2-3 on row 2
  ]
};
Enter fullscreen mode Exit fullscreen mode

Serialise one track to its CSS unit

There are exactly four track shapes: fr (a fraction of leftover space), px (a fixed length), auto (size to content), and minmax(min, max) (a floor and a ceiling). This tiny function is the heart of the serialiser; joining an array of them with spaces gives you a whole grid-template-columns string.

function trackStr(t){
  if (t.unit === "fr")     return t.v1 + "fr";
  if (t.unit === "px")     return t.v1 + "px";
  if (t.unit === "auto")   return "auto";
  if (t.unit === "minmax") return "minmax(" + t.v1 + "px, " + t.v2 + "fr)";
}
function templateStr(tracks){ return tracks.map(trackStr).join(" "); }   // "1fr 1fr 1fr"
Enter fullscreen mode Exit fullscreen mode

An item is four line numbers

Placement is line-based: grid-column: start / end and grid-row: start / end. The end line is exclusive, so 2 / 4 occupies the two tracks between lines 2 and 4. This is the single idea that prevents most grid bugs — N tracks make N+1 lines, numbered from 1, and you place against lines even though you size tracks.

function itemRule(it){
  return "." + it.name.toLowerCase() + " { "
       + "grid-column: " + it.cs + " / " + it.ce + "; "
       + "grid-row: "    + it.rs + " / " + it.re + "; }";
}
// { name:"B", cs:2, ce:4, rs:2, re:3 }  ->  ".b { grid-column: 2 / 4; grid-row: 2 / 3; }"
Enter fullscreen mode Exit fullscreen mode

The preview is a real grid container

Here's what makes the preview real, and the whole tool cheap: I create a display:grid element and inline the same template strings I generate. The browser's grid algorithm now owns every measurement — fr distribution, minmax clamping, gap subtraction — and I never compute a pixel myself. The generated CSS and the on-screen layout can't drift, because they're the same strings.

function renderPreview(g){
  const box = document.getElementById("gridPreview");
  box.style.gridTemplateColumns = templateStr(g.cols);
  box.style.gridTemplateRows    = templateStr(g.rows);
  box.style.gap = gapStr(g);               // real CSS, laid out by the engine
  box.innerHTML = ghostCells(g) + itemDivs(g);
}
Enter fullscreen mode Exit fullscreen mode

Ghost cells reveal the skeleton

To let you see the tracks, I render one faint cell per column×row, each explicitly placed on its own line pair so it never fights the real items for auto-placement. Items sit on top with a higher z-index. The cells are the tracks; the numbers on them are the lines you place against.

function ghostCells(g){
  let html = "";
  for (let r = 1; r <= g.rows.length; r++)
    for (let c = 1; c <= g.cols.length; c++)
      html += `<div class="gcell" style="grid-column:${c}/${c+1};grid-row:${r}/${r+1}">${c}·${r}</div>`;
  return html;   // explicit placement = no auto-flow surprises
}
Enter fullscreen mode Exit fullscreen mode

Drag to span is min/max on line numbers

Dragging is pure arithmetic. The cell at column c sits between lines c and c+1, so from the cell you grabbed to the one under the cursor, the start line is the smaller index and the end line is the larger index plus one. Clamp every line to 1 … tracks+1 so an item can never point off the grid.

function applyDrag(item, startC, startR, c, r){
  item.cs = Math.min(startC, c);
  item.ce = Math.max(startC, c) + 1;      // +1: end line is past the cell
  item.rs = Math.min(startR, r);
  item.re = Math.max(startR, r) + 1;
}
Enter fullscreen mode Exit fullscreen mode

A few gotchas the generator makes visible: fr shares what's left after fixed tracks and gaps are subtracted, so 1fr 1fr splits the remainder 50/50, not the whole width; gap only sits between tracks, never around the edge; and placing an item past your defined tracks silently creates implicit auto-sized ones. Every preset — holy-grail, a card wall, a sidebar shell — is just a set of tracks plus a few placed items, i.e. data you drop straight into state. The lesson underneath: a grid is not cells you draw, it's a skeleton of tracks with numbered lines between them. Describe the skeleton and let the browser lay it out.

Size the tracks, drag to span, copy the CSS:
https://dev48v.infy.uk/solve/day38-css-grid-generator.html

Top comments (0)