solid-snap
v0.1.0
Published
SolidJS library for constrained, grid-based 2D layouts.
Readme
solid-snap
SolidJS library for creating constrained, grid-based 2D layouts.
Solid Snap gives rectangular items freedom within a logical grid. It handles snapping, blocking collisions, surface boundaries, resize constraints, keyboard movement, and deterministic JSON validation while leaving application state with the consumer.
Installation
npm install solid-snap solid-jsThe package is written in TypeScript and publishes ES modules with generated declaration files. solid-js is a peer dependency; interactjs supplies pointer and touch interaction mechanics.
Controlled usage
Grid.layout is the canonical geometry state. GridItem children are keyed by ID and provide content plus interaction constraints.
import { createSignal } from "solid-js";
import { Grid, GridItem, type GridLayout } from "solid-snap";
import "solid-snap/style.css";
const initialLayout: GridLayout = {
columns: 48,
rows: 32,
items: [
{ id: "headline", x: 0, y: 0, w: 16, h: 8 },
{ id: "hero", x: 24, y: 8, w: 16, h: 16 }
]
};
function Editor() {
const [layout, setLayout] = createSignal(initialLayout);
return (
<Grid
layout={layout()}
snap={8}
showGrid
onChange={setLayout}
onSelect={(id) => console.log("selected", id)}
>
<GridItem id="headline" minW={8} minH={4}>
<h1>Headline</h1>
</GridItem>
<GridItem id="hero" minW={8} minH={8}>
<div>Hero</div>
</GridItem>
</Grid>
);
}The parent surface needs a size. Its logical geometry scales to any CSS dimensions without changing the persisted layout:
.editor-surface {
width: min(100%, 960px);
aspect-ratio: 16 / 9;
}Grid also accepts optional columns and rows overrides when a canonical layout is being displayed at a different effective density. Changing snap never rewrites existing item geometry.
Public layout API
interface GridRect {
x: number;
y: number;
w: number;
h: number;
}
interface GridItemData extends GridRect {
id: string;
}
interface GridLayout {
columns: number;
rows: number;
items: GridItemData[];
}Core helpers are pure TypeScript and can run without a DOM:
import {
canPlace,
convertLayout,
findCollisions,
isInsideBounds,
overlaps,
validateLayout
} from "solid-snap";For a server-side or command-line validator, the DOM-free entry point is also available explicitly:
import { validateLayout } from "solid-snap/core";Adjacent rectangles are valid. Colliding items block the candidate and are never automatically moved or repacked.
Validation and JSON generation
const result = validateLayout(layout);
if (!result.valid) {
for (const error of result.errors) {
console.log(error.code, error.itemId);
}
}Validation errors are machine-readable: OUT_OF_BOUNDS, COLLISION, INVALID_SIZE, INVALID_COORDINATE, and DUPLICATE_ID. The exported gridLayoutSchema describes the integer-only JSON format for external or LLM-generated layouts.
Grid density conversion is lossless by default:
const canonical = convertLayout(layout, { columns: 48, rows: 32 });
const smaller = convertLayout(canonical, { columns: 6, rows: 4 });Lossy conversion throws LayoutConversionError unless { allowRounding: true } is passed explicitly.
GridItem options
GridItem supports minW, minH, maxW, maxH, draggable, resizable, locked, disabled, selected, and ariaLabel. Resize handles are available on all four edges and four corners when resizing is enabled.
Keyboard arrows move the selected/focused item by one snap increment. Shift plus an arrow moves by four increments. Keyboard candidates use the same bounds and collision engine as pointer interactions.
Grid emits onDragStart, onDrag, onDragEnd, onResizeStart, onResize, and onResizeEnd events. Each event includes the item ID, current rectangle, previous rectangle, original rectangle, and resize direction when applicable.
Styling
The library ships sensible headless defaults in solid-snap/style.css. Customize these variables or override the documented class names:
.solid-snap {
--solid-snap-line-color: rgb(14 165 233 / 0.25);
--solid-snap-selection-color: #f97316;
--solid-snap-handle-size: 10px;
}Grid and GridItem also accept a style prop, so custom properties can travel with a specific surface or item without overriding its controlled geometry:
<Grid style={{ "--solid-snap-selection-color": "#fbbf24" }} /* … */>
<GridItem id="hero" style={{ "--card-image": "url('/hero.jpg')" }}>
<article class="card-with-image">Hero</article>
</GridItem>
</Grid>For state that belongs to an application item rather than its geometry, use GridItem.render. It receives the stable grid item ID, so it can look up and render arbitrary content—such as a table—without adding non-geometric data to GridLayout:
<Grid layout={layout()} onChange={setLayout}>
<GridItem
id="sales-table"
render={(id) => <SalesTable data={itemData()[id]} />}
/>
</Grid>render takes precedence over static children if both are provided.
Grid visibility is independent of snapping: showGrid={false} hides lines while snap continues to constrain interactions.
Examples and landing page
Run the interactive SolidJS demo:
npm run dev -- --config examples/basic/vite.config.tsFor a single development server with several focused playgrounds (basic layout, item constraints, and interaction events), run:
npm run dev:examplesBuild the demo or the standalone landing page:
npm run build:demo
npm run build:examples
npm run build:landingThe landing page includes a live playground, reusable SVG logo, and links to the project source at github.com/only-cliches/solid-snap.
GitHub Pages
The marketing site deploys from main through the Deploy landing site to GitHub Pages workflow. Set Settings → Pages → Source to GitHub Actions after the repository is renamed; deployment targets:
https://only-cliches.github.io/solid-snap/The workflow builds with the /solid-snap/ asset base required for a project Pages site. Local npm run dev:landing remains available at /.
Development
npm install
npm test
npm run typecheck
npm run build
npm run qa:e2eqa:e2e starts the demo and uses the local agent-browser development dependency to verify the controls, JSON validation, keyboard movement, pointer drag, and pointer resize flows in headless Chromium. It requires Chrome or Chromium to be available on the host.
The library intentionally excludes layers, application-specific data, arbitrary pixel positioning, rotation, freeform overlap, automatic packing, multi-selection, grouping, undo/redo, animation, and AI calls.
