Building Responsive UIs with RectUtils — Practical Patterns & ExamplesResponsive user interfaces require precise control of geometry: positioning, sizing, alignment, hit testing, and transformations of rectangular elements. RectUtils — a compact set of rectangle utility functions — helps you handle common 2D layout tasks reliably and with minimal code. This article explains practical patterns and examples you can use in web apps, games, and custom UI libraries.
What is RectUtils?
RectUtils is a collection of small, focused functions that operate on rectangles. A rectangle (rect) is typically represented as an object with numeric properties like { x, y, width, height } or { left, top, right, bottom }. RectUtils converts between representations, performs arithmetic, tests intersections, computes containment and alignment, handles scaling/anchoring, and normalizes values for responsive layouts.
Key advantages:
- Simplifies repetitive geometry logic
- Centralizes consistent coordinate conventions
- Makes responsive behavior predictable and testable
Rect representations and conventions
Common rect shapes:
- A = { x, y, width, height } — x/y are top-left coordinates.
- B = { left, top, right, bottom } — explicit edges.
- C = { cx, cy, w, h } — center-based.
RectUtils should provide conversion helpers:
- toLTRB(rectXYWH) → { left, top, right, bottom }
- toXYWH(rectLTRB) → { x, y, width, height }
- toCenter(rectXYWH) → { cx, cy, w, h }
Normalization rules to adopt:
- Ensure width/height are non-negative (swap edges if necessary).
- Round when producing pixel-aligned layouts when appropriate.
- Always document coordinate origin (top-left vs. bottom-left).
Example conversion (JS):
function toLTRB({ x, y, width, height }) { return { left: x, top: y, right: x + width, bottom: y + height }; } function toXYWH({ left, top, right, bottom }) { return { x: left, y: top, width: right - left, height: bottom - top }; }
Core RectUtils functions (recommended set)
Essential operations every RectUtils library should include:
- create(x,y,w,h) / fromLTRB(l,t,r,b)
- normalize(rect)
- clone(rect)
- contains(rect, pointOrRect)
- intersects(rectA, rectB)
- intersection(rectA, rectB) → rect or null
- union(rectA, rectB) → rect
- inset(rect, dx, dy) / outset(rect, dx, dy)
- align(rect, container, alignment) — e.g., “center”, “top-right”
- fit(rect, container, mode) — modes: “contain”, “cover”, “stretch”
- scaleTo(rect, scale, anchor) — anchor like { x:0-1, y:0-1 } or named (center, top-left)
- snapToPixel(rect, devicePixelRatio)
These form the building blocks for responsive behavior.
Responsive layout patterns using RectUtils
Below are practical patterns showing how RectUtils helps build responsive UIs.
Pattern: Container-aware alignment
- Problem: Place child elements relative to a container even when container resizes.
- Solution: Compute child’s rect from desired alignment and margins each layout pass.
Example (center with margin):
function alignCenter(childSize, containerRect, margin = 0) { const { width: cw, height: ch } = containerRect; const x = containerRect.x + (cw - childSize.width) / 2; const y = containerRect.y + (ch - childSize.height) / 2; return { x: Math.round(x) + margin, y: Math.round(y) + margin, ...childSize }; }
Pattern: Responsive scaling (contain vs. cover)
- “Contain”: scale element to fit entirely inside container while preserving aspect ratio.
- “Cover”: scale so it fully covers container, possibly cropping.
Example scale-to-fit:
function fitRectTo(rect, container, mode = 'contain') { const sx = container.width / rect.width; const sy = container.height / rect.height; const scale = mode === 'cover' ? Math.max(sx, sy) : Math.min(sx, sy); const w = rect.width * scale; const h = rect.height * scale; const x = container.x + (container.width - w) / 2; const y = container.y + (container.height - h) / 2; return { x, y, width: w, height: h, scale }; }
Pattern: Edge-aware hit testing
- Problem: UI interactions should distinguish clicks on edges, corners, or interior (resize vs. move).
- Solution: Use insets and point containment tests to classify regions.
Example:
function hitRegion(rect, px, py, edgeSize = 8) { const inside = px >= rect.x && px <= rect.x + rect.width && py >= rect.y && py <= rect.y + rect.height; if (!inside) return 'none'; const left = px - rect.x < edgeSize; const right = rect.x + rect.width - px < edgeSize; const top = py - rect.y < edgeSize; const bottom = rect.y + rect.height - py < edgeSize; if ((left && top) || (right && bottom)) return 'corner'; if ((left && bottom) || (right && top)) return 'corner'; if (left || right || top || bottom) return 'edge'; return 'inside'; }
Advanced techniques
Anchored scaling and fractional anchors
- Use anchors with normalized coordinates (0..1) so scaling keeps a chosen pivot fixed.
- Example: anchor { x:0.5, y:1 } keeps bottom-center fixed during resizes.
Composition with CSS transforms
- For DOM elements with transforms, compute bounding rects after transform and use RectUtils to align overlays or tooltips.
- Use getBoundingClientRect() as source rect and convert to your coordinate space.
Layout animations
- Interpolate between rects for smooth transitions (position + size + alpha).
- Use easing and requestAnimationFrame. Store startRect and endRect, compute lerp for each frame:
- x(t) = x0 + (x1 – x0)*e(t)
Pixel snapping and crisp rendering
- Snap coordinates and sizes to device pixels or half-pixels depending on stroke widths to avoid blurry borders.
Collision avoidance for floating UI
- When placing menus or tooltips, compute candidate placements (top, bottom, left, right). Use intersection/contain tests with viewport rect and pick placement minimizing overflow.
Example candidate selection:
function pickPlacement(anchorRect, tooltipSize, viewport) { const candidates = [ { name: 'top', rect: { x: anchorRect.x + (anchorRect.width - tooltipSize.width)/2, y: anchorRect.y - tooltipSize.height, width: tooltipSize.width, height: tooltipSize.height }}, { name: 'bottom', rect: { x: anchorRect.x + (anchorRect.width - tooltipSize.width)/2, y: anchorRect.y + anchorRect.height, width: tooltipSize.width, height: tooltipSize.height }}, // left/right... ]; candidates.forEach(c => c.overflow = Math.max(0, c.rect.x - viewport.x) + Math.max(0, (c.rect.x + c.rect.width) - (viewport.x + viewport.width)) + Math.max(0, c.rect.y - viewport.y) + Math.max(0, (c.rect.y + c.rect.height) - (viewport.y + viewport.height))); candidates.sort((a,b) => a.overflow - b.overflow); return candidates[0]; }
Examples: Implementing a responsive card grid
Goal: Cards resize and reflow across breakpoints, maintain consistent aspect ratio, center content within each card, and provide hot zones for interactions.
Approach:
- Compute container rect (available width).
- Decide columns count based on container.width breakpoints.
- Compute cardWidth = (container.width – gap*(cols-1))/cols.
- Compute cardHeight = cardWidth * aspectRatio; build rects for each card by aligning into rows.
- Use RectUtils.intersects to detect visible range for virtualization.
- For hover/resize zones, add edge detection via hitRegion.
Pseudo-code:
const cols = container.width > 1200 ? 4 : container.width > 800 ? 3 : container.width > 500 ? 2 : 1; const cardW = (container.width - gap * (cols - 1)) / cols; const cardH = cardW * aspect; let x = container.x, y = container.y; for (let i = 0; i < items.length; i++) { const rect = { x, y, width: cardW, height: cardH }; // render card at rect x += cardW + gap; if ((i+1) % cols === 0) { x = container.x; y += cardH + rowGap; } }
Testing and debugging RectUtils
- Unit tests: cover conversions, edge cases (zero/negative sizes), containment, intersection math.
- Visual debug overlay: render rect outlines and labels to verify layout math in real-time.
- Property-based tests: random rects to ensure union/intersection invariants hold.
Performance considerations
- Keep rects lightweight (plain objects with numeric fields).
- Avoid allocating lots of temporary objects in hot loops — reuse objects where possible.
- Vectorize layout: compute bulk positions in a single pass and minimize DOM writes (batch style updates).
- For canvas or WebGL, use typed arrays for large lists of rects.
API design tips
- Keep functions pure (no side effects) so results are deterministic and easy to test.
- Provide both immutable and in-place variants (e.g., inset() vs insetInPlace()).
- Accept flexible inputs (support both {x,y,w,h} and {left,top,right,bottom}) but normalize internally.
- Document coordinate origin and rounding behavior clearly.
Small, practical utilities (snippets)
Center rect inside another:
function centerRect(inner, outer) { return { x: outer.x + (outer.width - inner.width) / 2, y: outer.y + (outer.height - inner.height) / 2, width: inner.width, height: inner.height }; }
Clamp a rect inside container:
function clampRect(rect, container) { const x = Math.max(container.x, Math.min(rect.x, container.x + container.width - rect.width)); const y = Math.max(container.y, Math.min(rect.y, container.y + container.height - rect.height)); return { ...rect, x, y }; }
Interpolate between two rects:
function lerpRect(a, b, t) { return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, width: a.width + (b.width - a.width) * t, height: a.height + (b.height - a.height) * t }; }
Summary
RectUtils encapsulates the geometric logic essential to responsive UI. With a compact, well-documented set of rectangle operations you can:
- Align and anchor elements precisely,
- Fit and scale content predictably,
- Handle interactions like hit testing and resizing,
- Make intelligent placement decisions for overlays,
- Animate and virtualize layouts efficiently.
Start small: implement conversions, containment, intersection, fit/align, and scaling with anchors. Build test coverage and debug overlays. Those few primitives unlock most responsive layout patterns and make your UI predictable, maintainable, and performant.
Leave a Reply