Tile Planner

Installation & API

One script tag, any website. No backend, no build step, no account required.

cdn.tily.work
View pricing →

Quick start

Up in 3 steps

1add CDN links
index.html
1<!-- No install — works in any HTML page -->
2<link rel="stylesheet"
3 href="https://cdn.tily.work/tile-planner.css">
4<script
5 src="https://cdn.tily.work/tile-planner.js"
6></script>

No install required — add two tags and it works.

2add a container
index.html
1<!-- Any HTML — just give it a size -->
2<div id="planner"
3 style="height: 600px;"
4></ div>

Size the container — the planner fills it automatically.

3mount
index.html
1<script>
2 TilyPlanner.mount("#planner", {
3 planType: "garden",
4 });
5</ script>

Returns a handle — handle.unmount() removes it, handle.update(opts) changes config live.

API reference

Mount options

All options are optional — the planner works with zero config. The same options are accepted by <TilePlannerCore /> as props.

optiontypedefaultdescription
planType"garden" | "indoor" | "wall""garden"Starting shape and default tile preset. "wall" shows a rectangular elevation view with door/window cutouts.
sizeTileSizeLock a tile size and hide the size picker. E.g. { kind: "600x600" }.
persistKeystringlocalStorage key for automatic plan persistence.
onSave(plan: PlanExport) => Promise<void>Called on Save. Return a promise to show a loading state.
onResult(r: TilePlannerResult | null) => voidFires ~300 ms after every change with tile count, boxes, area, estimatedCost. Use to update cart or price.
onAddToCart(r: TilePlannerResult) => voidShows a built-in add-to-basket button inside the result card. Called on click with the full result.
onShareEmail(email: string, url: string) => voidWhen set, the Share button opens an email input form instead of copying to clipboard.
initialPlanPlanExportPre-load a saved plan on mount.
tilePatternstringImage URL or data URL painted as a repeating fill inside each tile.
themePlannerThemeCSS custom-property overrides for colours and font.
tokenstringOptional embed token for token-validated CDN embeds (TilyPlanner.mount only).
config.showResetbooleanfalseShow "New plan" reset button.
config.showSharebooleanfalseShow "Copy share link" button.
config.showExportsbooleanfalseShow PDF / PNG / JSON export buttons.
config.showMaterialEstimatorbooleanfalseShow the material cost estimator panel.
config.pdf.shopNamestringShop name printed in the header of exported PDFs.
config.pdf.logoUrlstringLogo URL (PNG/JPEG) rendered top-right in exported PDFs.
config.pdf.primaryColor[number, number, number]RGB triplet for PDF table header backgrounds. Defaults to Tily green.
config.lockedTile.skustringProduct SKU forwarded through onResult. Ideal for product-page embeds.
config.lockedTile.pricePerTilenumberPrice per tile — used to compute estimatedCost in onResult.
config.lockedTile.pricePerSqMnumberPrice per m² — alternative to pricePerTile.
config.lockedTile.currencySymbolstring"£"Currency symbol shown in the UI and forwarded in onResult as currencySymbol.
config.lockedTile.textureUrlstringTile photo URL used as a visual fill on the canvas.
config.lockedTile.tilesPerBoxnumberTiles per box/pack — used to compute the boxes count in onResult.
licenceKeystringTily Pro licence key. Unlocks canvas editor, all patterns, exports, share, and removes the Powered-by badge. Falls back to Free tier silently when missing or invalid.
compactbooleanfalseHide sidebar; show a slim bottom bar + slide-up panel instead. Auto-enabled on mobile.

Mount handle — returned by both mount functions

types.d.ts
1interface TilePlannerHandle {
2 unmount: () => void; // remove from DOM
3 update(partial: Partial<MountOptions>): void; // live-update config, no remount
4}

PlanExport — the object passed to onSave

types.d.ts
1interface PlanExport {
2 version: number;
3 planType: "garden" | "indoor";
4 exportedAt: string; // ISO 8601
5 shape: { vertices: Vertex[]; offset: Vertex };
6 tiles: {
7 size: TileSize;
8 herringbone: boolean;
9 herringboneRotation: 0 | 45 | 90 | 135;
10 groutMm: number;
11 };
12}

Theming — override any colour token

index.html
1const handle = TilyPlanner.mount("#planner", {
2 planType: "garden",
3 tilePattern: imgDataUrl, // ← any img URL
4 theme: {
5 primary: "#6366f1", // indigo
6 highlight: "#f59e0b", // amber
7 fontFamily: "Inter, sans-serif",
8 },
9});
10
11// update live — no remount needed:
12handle.update({ tilePattern: newUrl });

Cart integration via onResult

shop.js
1TilyPlanner.mount("#planner", {
2 planType: "indoor",
3 onResult: function(r) {
4 if (!r) return; // canvas empty
5 // r.tiles, r.boxes, r.sku, r.areaSqM, r.estimatedCost
6 updateCartQty(r.sku, r.tiles);
7 },
8 size: { kind: "600x600" },
9});

TilePlannerResult — the object passed to onResult / onAddToCart

types.d.ts
1interface TilePlannerResult {
2 tiles: number; // total tiles incl. waste
3 boxes?: number; // boxes (if tilesPerBox set)
4 areaSqM: number; // net floor/wall area in m²
5 wasteFactor: number; // 0.05–0.20
6 tileSize: TileSize;
7 material: "tile" | "laminate";
8 sku?: string; // from lockedTile.sku
9 planType?: "garden"|"indoor"|"wall";
10 estimatedCost?: number; // set when price configured
11 currencySymbol?: string; // e.g. "£", "€", "$"
12}

Recipe

Product page integration

Embed the planner directly on a tile product page. Lock the SKU and price, show a texture preview, and wire the result into your cart — all in under 20 lines.

Complete product-page embed

product-page.js
1TilyPlanner.mount("#tile-planner", {
2 planType: "indoor", // or "wall" for kitchen splashbacks
3 quickMode: true, // simple width × height form
4 config: {
5 lockedTile: {
6 sku: "TILE-600-WHT",
7 label: "White Marble 600×600",
8 size: { kind: "600x600" },
9 tilesPerBox: 6,
10 pricePerTile: 4.99,
11 currencySymbol: "£",
12 textureUrl: "https://cdn.myshop.com/tiles/white-marble.jpg",
13 },
14 pdf: { // branded export PDF
15 shopName: "My Tile Shop",
16 logoUrl: "https://cdn.myshop.com/logo.png",
17 },
18 },
19 onResult: function(r) {
20 if (!r) return;
21 // r.tiles, r.boxes, r.estimatedCost, r.currencySymbol
22 document.getElementById("qty").textContent = r.tiles;
23 document.getElementById("total").textContent =
24 r.currencySymbol + r.estimatedCost.toFixed(2);
25 },
26 onAddToCart: function(r) {
27 addToCart({ sku: r.sku, qty: r.tiles, boxes: r.boxes });
28 },
29 addToCartLabel: "Add tiles to basket",
30});

What the customer sees

  • Width × height input with unit toggle (m / cm / mm / ft / in)
  • Waste % selector auto-bumps to 20 % for diagonal/herringbone patterns
  • Deduct areas (kitchen island, bathtub…) up to 3 exclusion zones
  • Live result card: tile count, box count, and estimated cost
  • Built-in "Add tiles to basket" button — no extra JS needed

Wall tiling variant

product-page-wall.js
1TilyPlanner.mount("#planner", {
2 planType: "wall", // elevation view
3 quickMode: true,
4 config: { lockedTile: { sku: "WALL-300", ... } },
5 // Up to 2 openings (door / window) auto-deducted
6 onResult: (r) => syncCart(r),
7});

Questions about embedding or integrating with your platform?