Definition
Items placed near one another are read as related. Whitespace alone, with no border or color cue, is enough to forge a group in the reader's perception. Distance is meaning.
Why it matters for ShurIQ reports
Stack-rank rows, gap card grids, and viewport tile layouts all encode relationships through spacing. A 16-pixel gap inside a card and a 32-pixel gap between cards tells the eye what belongs together before the reader has read a word. Proximity discipline is the cheapest way to make a dense brief feel orderly.
Takeaways
- Establish a strict spacing scale (4, 8, 16, 24, 32, 48) and use it to encode hierarchy — never freehand-space.
- Tighten spacing within a unit, loosen it between units; the contrast is what creates the group.
- When two items must read as related but cannot sit close, fall back to common region or similarity — never weaken proximity by adding extra borders to compensate.
- Re-test layouts at the smallest deployment width; proximity collapses first on narrow screens.
Visual motion language
On layout-shift, related elements use magnetic-pull (50–80ms ease) to settle into proximity together; cross-group elements snap to the grid independently. The motion makes group structure legible during transitions.
Origins
Gestalt psychology — early 20th century perceptual organization research.
Cavalry scene
The script below builds this concept's motion in Cavalry through the Stallion bridge. Pipe to cavalry_run_script via MCP, or paste into Cavalry's JavaScript Editor.
// Laws of UX · law-of-proximity · Cavalry scene
// Motion family: grid-stagger (grouped reveal)
// Palette: burnt-orange + cream
// To run: pipe to cavalry_run_script tool, or paste into Cavalry's JavaScript Editor
// Built 2026-04-30 by ShurAI
(function () {
var PREFIX = "claude_lawofux_law-of-proximity_";
var existing = api.getAllSceneLayers();
for (var i = 0; i < existing.length; i++) {
try {
var nm = api.getNiceName(existing[i]);
if (nm && nm.indexOf("claude_lawofux_") === 0) api.deleteLayer(existing[i]);
} catch (e) {}
}
var BG = "#B85A22";
var DARK = "#6E3414";
var CREAM = "#D6CDB0";
var bg = api.primitive("rectangle", PREFIX + "bg");
api.set(bg, { "generator.dimensions": [1080, 1080] });
api.setFill(bg, true);
api.set(bg, { "material.materialColor": BG });
// 4x4 grid of 70px circles. Cluster of 12 dots on left, 4 on right with bigger gap.
var ROWS = 4;
var R = 35;
var SMALL_GAP = 30;
var WIDE_GAP = 100;
// x positions for cols: 3 left tightly, then 1 right wider
var leftStart = -200;
var xCols = [
leftStart,
leftStart + (R*2 + SMALL_GAP),
leftStart + 2 * (R*2 + SMALL_GAP),
leftStart + 2 * (R*2 + SMALL_GAP) + (R*2 + WIDE_GAP)
];
var startY = ((ROWS - 1) * (R*2 + SMALL_GAP)) / 2;
for (var r = 0; r < ROWS; r++) {
for (var c = 0; c < 4; c++) {
var d = api.primitive("ellipse", PREFIX + "dot_" + r + "_" + c);
var isCream = (c < 3);
api.set(d, {
"generator.radius": [R, R],
"position.x": xCols[c],
"position.y": startY - r * (R*2 + SMALL_GAP),
"opacity": 0,
"scale.x": 0.8, "scale.y": 0.8
});
api.setFill(d, true);
api.set(d, { "material.materialColor": isCream ? CREAM : DARK });
if (isCream) {
api.keyframe(d, 0, { "opacity": 0, "scale.x": 0.8, "scale.y": 0.8 });
api.keyframe(d, 17, { "opacity": 100, "scale.x": 1.0, "scale.y": 1.0 });
api.magicEasing(d, "scale.x", 17, "EaseOut", "");
api.magicEasing(d, "scale.y", 17, "EaseOut", "");
} else {
api.keyframe(d, 30, { "opacity": 0, "scale.x": 0.8, "scale.y": 0.8 });
api.keyframe(d, 45, { "opacity": 100, "scale.x": 1.0, "scale.y": 1.0 });
}
}
}
var layerCount = api.getAllSceneLayers().length;
console.log("scene built: law-of-proximity (" + layerCount + " layers)");
})();