Skip to content

Repository files navigation

Icon Atlas Studio

Icon Atlas Studio

Build pixel-perfect icon atlases in your browser.

Pack hundreds of icons into a single sprite sheet and export it with a ready-made coordinate map for CSS, TypeScript and React Native.

Live site License Tests Offline

Next.js React TypeScript Tailwind CSS Cloudflare Pages

Icon Atlas Studio is a visual editor for turning hundreds of icons into a single sprite sheet, together with the coordinate map your code needs to use it.

Pick from the 1,611-icon Lucide library or drop in your own SVGs, arrange them on a grid, and export one PNG, WebP or SVG. Alongside the image you get a JSON coordinate map, a typed TypeScript helper and a CSS file with one ready-made class per icon, delivered as a single archive.

Everything runs in the browser. There is no account, no backend, and no upload.


Overview

Managing a large icon collection means managing a lot of files:

home.svg
search.svg
user.svg
settings.svg
notification.svg
cart.svg
...

An atlas turns them into one image:

┌────────┬────────┬────────┬─────────┐
│  Home  │ Search │  User  │ Settings│
├────────┼────────┼────────┼─────────┤
│  Bell  │  Cart  │ Heart  │  Share  │
├────────┼────────┼────────┼─────────┤
│  Edit  │ Delete │  Plus  │  Menu   │
└────────┴────────┴────────┴─────────┘

One image means one request and one decode, however many icons it holds.

The workflow

Search or import icons
         ↓
   Add all at once
         ↓
 Arrange and restyle
         ↓
       Export
         ↓
  One .zip, ready to use

Positions are arithmetic, not a lookup table

Icons flow into the grid in order, so an icon's position follows from its index:

column = index % columns
row    = Math.floor(index / columns)
x      = paddingX + column * (cellWidth + gapX)
y      = paddingY + row * (cellHeight + gapY)

The sheet stays densely packed at all times. Removing an icon reflows the rest, so there are never holes to account for.


Features

Icon library

  • 1,611 Lucide icons, searchable by name, keyword and category
  • Ranked search, so home surfaces the icon actually called home
  • 215 deprecated Lucide names kept as aliases, so alert-circle still finds circle-alert
  • A windowed grid that stays smooth with the whole library on screen

Bulk placement

Search or filter a category, then add every match with one click. The entire 1,611-icon library lands on the sheet in about 100ms.

Import your own SVGs

Drop SVG files onto the library, or use the import button. Imported icons are:

  • normalised onto the same 24-unit grid as the built-in set
  • recoloured to match the sheet, with stroked artwork left as outlines
  • saved inside the project and carried in its .atlas.json file
  • exported exactly like any other icon

Imported files are parsed with an allow-list rather than a sanitiser: only known shape elements and geometry attributes survive, and everything else is discarded. Scripts, event handlers, foreignObject and external references never reach the DOM.

Layout control

  • Columns, cell width and height, horizontal and vertical gap, padding
  • Square packing: one button picks the column count that brings the sheet closest to square
  • Sort A to Z, remove duplicates, or clear the sheet
  • Drag any icon to reorder it

Styling

Set the colour, size, stroke weight and opacity for every icon at once, then override colour, size or rotation for a single icon or a shift-selected range.

Backgrounds can be transparent, previewed against a checkerboard, or a solid colour.

Export

Format Notes
PNG 1x to 4x pixel density
WebP Smaller files, same coordinates
SVG Resolution independent

Alongside the image:

  • JSON coordinate map for any language
  • TypeScript module with a name union and an iconStyle() helper
  • CSS with one ready-made class per icon

One archive. Everything arrives as a single .zip rather than four separate downloads.

Retina variants. One toggle ships @2x and @3x companions beside the base image, sharing a single coordinate map and wired up through CSS image-set.

Coordinates stay in logical pixels whatever the export density, so a 3x sheet needs no arithmetic on your side.

Works offline, installable

After the first visit the app, the icon data and every asset are cached on your device. Open the page with no connection and the full editor loads and works, exporting included. Your browser will also offer to install it as an app.

Project files

Projects autosave to the browser and can be saved to or opened from a portable file:

my-icons.atlas.json

A project holds the grid configuration, icon order and overrides, background, imported artwork and export settings.

Undo and redo

Fifty steps of history across adding, removing, reordering, restyling and layout changes.

Action Shortcut
Undo Cmd/Ctrl + Z
Redo Cmd/Ctrl + Shift + Z
Select all Cmd/Ctrl + A
Open export Cmd/Ctrl + E
Fit to view Cmd/Ctrl + 0
Zoom to 100% Cmd/Ctrl + 1
Remove selected Delete / Backspace
Clear selection Escape
Zoom Cmd/Ctrl + scroll
Select a range Shift + click
Add to selection Cmd/Ctrl + click

Using an exported sheet

The export dialog shows a copyable snippet for each of these.

HTML and CSS

<link rel="stylesheet" href="icon-atlas.css" />

<span class="icon-atlas icon-atlas--house"></span>

React

import { iconStyle } from './icon-atlas';
import sheet from './icon-atlas.png';

<span style={iconStyle('house', sheet)} />

IconName is a union of the names actually on the sheet, so a typo is a compile error rather than a blank square.

React Native

<View style={{ width: 64, height: 64, overflow: 'hidden' }}>
  <Image
    source={require('./icon-atlas.png')}
    style={{ width: 768, height: 832, marginLeft: -128, marginTop: -64 }}
  />
</View>

The coordinate map

{
  "version": 1,
  "image": "icon-atlas.png",
  "variants": [1, 2, 3],
  "atlas": { "width": 768, "height": 832, "columns": 12, "rows": 13 },
  "cell": { "width": 64, "height": 64, "gapX": 0, "gapY": 0 },
  "icons": [
    { "name": "house", "index": 0, "row": 0, "column": 0, "x": 0, "y": 0, "width": 64, "height": 64 }
  ]
}

Getting started

Requirements

  • Node.js 20 or newer
  • npm

Clone and run

git clone https://github.com/iplustsolution/Icon-Atlas-Studio.git
cd Icon-Atlas-Studio
npm install
npm run dev

Then open http://localhost:3000.

predev and prebuild regenerate the icon data file, so a fresh clone works with no extra steps.

Build

npm run build      # writes the static site to out/
npm run preview    # serves out/ exactly as Cloudflare will

Every route prerenders to a file, so next build produces a static export in out/ and there is no server to run. postbuild then writes out/sw-manifest.json, the precache list the service worker reads on install, which is why it runs after the export rather than before.

npm run preview serves the export through wrangler dev, which applies the same rules the deploy does: the _headers file, clean URLs for the generated images, and a real 404 for unknown paths.

Configuration

One variable matters, and only for a public deploy:

NEXT_PUBLIC_SITE_URL=https://icon.iplust.in

Every canonical URL, the sitemap, robots.txt, the Open Graph tags, the web app manifest and the JSON-LD graph are derived from it. Leave it unset and the value in .env.example is used. Search Console ownership tokens are optional and also read from the environment; see .env.example.


Architecture

One source of truth for geometry. scripts/build-icon-data.mjs reads the installed lucide-react package and writes every icon's path data to public/icon-data/lucide.json, about 73 KB gzipped for the whole set. The library grid, the canvas and the exporter all draw from that one file, and imported SVGs join the same map.

The preview is the export. buildAtlasSvg renders the entire sheet as a single SVG document. The canvas displays that exact markup, and a raster export rasterises the same string. There is no second code path that could drift, and a thousand icons cost one image decode rather than a thousand.

Persisted state is rebuilt, not trusted. A saved project is user-editable and outlives releases, so it is revived field by field against the defaults. A project saved by an older build, hand-edited, or truncated mid-write loads as a sane document instead of crashing the canvas.

Project structure

icon-atlas-studio/
├── .github/assets/           Brand artwork used by this README
├── e2e/                      End-to-end checks against the static export
├── wrangler.jsonc            Cloudflare Pages deploy configuration
├── functions/
│   └── _middleware.js        301s the www host to the canonical one
├── public/
│   ├── brand/                Publisher mark, served from this origin
│   ├── icon-data/            Generated icon geometry
│   ├── _headers              Response headers, since the export has no server
│   ├── ads.txt               Declares that nobody may sell ads on this domain
│   └── sw.js                 Service worker: precache and offline strategies
├── scripts/
│   ├── build-icon-data.mjs   Extracts Lucide geometry into one JSON file
│   ├── build-favicon.mjs     Assembles /favicon.ico from the PNG icons
│   └── build-sw-manifest.mjs Writes the precache list after a build
└── src/
    ├── app/                  Next.js route, layout and design system
    │   ├── page.tsx          Server component: the editor plus the guide
    │   ├── studio-shell.tsx  The client editor
    │   ├── robots.ts         robots.txt
    │   ├── sitemap.ts        sitemap.xml
    │   ├── manifest.ts       Web app manifest
    │   ├── not-found.tsx     Custom 404 with a route back into the site
    │   ├── llms.txt/         Plain-text summary for language models
    │   └── opengraph-image.tsx  Generated social card
    ├── components/atlas/     Toolbar, library, canvas, inspector, export dialog
    ├── components/seo/       The indexable guide and the JSON-LD graph
    ├── components/pwa/       Service worker registration and update prompt
    ├── components/analytics/ Cookieless page-view beacon, off unless configured
    ├── hooks/
    ├── lib/atlas/
    │   ├── layout.ts         Grid arithmetic, hit testing, validation
    │   ├── render.ts         Sheet-to-SVG rendering and rasterisation
    │   ├── codegen.ts        The JSON manifest, TypeScript and CSS output
    │   ├── icons.ts          Loads and searches the generated icon data
    │   ├── svg-import.ts     Allow-list parser for user-supplied SVG
    │   └── zip.ts            Dependency-free ZIP writer
    ├── lib/seo/              Site constants, page copy, structured data
    └── stores/               Document state, undo history and persistence

Tech stack

  • Next.js and React, exported as a static site
  • Cloudflare Pages for hosting, served from the edge
  • TypeScript, strict mode
  • Tailwind CSS
  • Zustand for state
  • Lucide for icons
  • Sonner for notifications

Six runtime dependencies in total. The service worker and the precache script are hand-written for the same reason as the ZIP writer and the SVG parser: a build plugin would have brought a dependency tree along with it. New ones are added only when they earn their place; the ZIP writer and the SVG parser are in-house for exactly that reason.


Design

The interface is glass over a slowly drifting aurora. Panels are translucent with a backdrop blur, a lit top edge, a shadowed bottom edge and an outer drop shadow, which together read as physical panes. Blur is expensive, so it is limited to the large panels and dialogs; buttons get translucency without it.

Below 1024px the side panels become overlay drawers and the toolbar strip scrolls, so every control stays reachable down to a 390px phone. All motion is disabled under prefers-reduced-motion.


Performance

Operation Measured
Add all 1,611 icons ~100ms
Icon data payload 73 KB gzipped
Canvas interaction at 1,611 icons under 60ms
Export a 1,611-icon sheet at 2048×3264 ~9s

The techniques that matter here are a windowed library grid, a deferred search value so typing stays responsive, and rendering the whole sheet as one SVG so any number of icons costs a single decode.


Discoverability

The page is one URL, so everything a search engine or an answer engine needs has to be on it.

The guide is part of the document. Below the editor sits a server-rendered guide: what an icon atlas is, a six-step how-to, every feature, the export formats, copyable integration snippets for CSS, React and React Native, fifteen answered questions and a glossary. It is around 3,200 words of real HTML in the first response, with no client JavaScript, so a crawler that runs no scripts still reads the whole page.

Structured data is generated from that same copy. src/lib/seo/content.ts is the single source for the visible guide, the JSON-LD graph and /llms.txt. Schema.org markup that disagrees with the visible page is a manual action waiting to happen, so there is only one place to edit. The graph is one connected @graph covering Organization, WebSite, WebPage, SoftwareApplication, TechArticle, HowTo, FAQPage, BreadcrumbList, DefinedTermSet and two ItemList nodes, cross-referenced by @id.

Nothing is rated, reviewed or counted that the page cannot back up. There is no invented aggregateRating.

Surface Route
Canonical, hreflang, Open Graph, Twitter card src/app/layout.tsx
robots.txt, with assistant crawlers named and allowed /robots.txt
sitemap.xml /sitemap.xml
Web app manifest, installable with maskable icons /manifest.webmanifest
Generated 1200×630 social card /opengraph-image
Plain-Markdown summary for language models /llms.txt

Nothing is blocked in robots.txt at all. Blocking scripts stops Google rendering the page as a visitor sees it, and a Disallow rule reads to an auditor as content being deliberately hidden. The handful of files that should never surface as a result carry X-Robots-Tag: noindex from public/_headers instead, which leaves them crawlable and unindexable at once.

The title and description are length-budgeted. 47 and 192 characters, inside the windows Google renders without padding or truncating. Both are asserted in the test suite, because the natural drift is upward.

/favicon.ico is generated at build time. The site declares an SVG icon and five PNG sizes, which is what a browser wants, but a great many crawlers, feed readers and audit tools request /favicon.ico and nothing else. scripts/build-favicon.mjs wraps the 32 and 48 pixel PNGs into an ICO container, which since Vista may hold PNG payloads, so no image library is needed.

Analytics is Cloudflare Web Analytics, not Google. It is cookieless, sets no identifiers and records no personal data, which is the only way to measure the site without making the privacy claims above false. Google Analytics would have needed cookies and a consent banner. The beacon ships only when NEXT_PUBLIC_CF_BEACON_TOKEN is set.

There are nine h2 headings and one h1. The editor's inspector once used h2 for its panel labels, which put four interface headings into the document outline beside the guide's and made the page look like it covered a dozen unrelated topics. They are h3 now.


Offline

The editor has no backend, so once its HTML, its JavaScript and the icon geometry are on the device, every feature works with the network switched off, including importing SVG files and exporting a finished atlas. The service worker in public/sw.js is built around that: the goal is not a degraded offline notice, it is the whole application.

Three strategies, chosen by what the request is:

Request Strategy Why
Navigations Network first, 3.5s timeout, then the cached document Fresh HTML online, the real page offline, never a browser error
/_next/static/* Cache first The URL carries a content hash, so a hit cannot be stale
Everything else Stale while revalidate Instant from cache, refreshed in the background

scripts/build-sw-manifest.mjs runs as postbuild and writes the precache list from what the build actually emitted, because Next names its chunks with hashes that only exist afterwards. Each asset is cached individually rather than through cache.addAll, so one missing file cannot throw away the whole install.

A new build waits rather than seizing control: activating under an open tab would serve one build's assets to a page running another. The page offers a reload instead, and only takes over when the user accepts. A first install never reloads the page underneath the visitor.

Deployment

The site is a static export on Cloudflare Pages. Nothing runs per request except one small redirect function: every URL is a file served from Cloudflare's edge.

npm run deploy

That builds the export and uploads out/. wrangler.jsonc points at it with pages_build_output_dir, and CLOUDFLARE_ACCOUNT_ID in .env chooses the account, because a Pages config file cannot carry one.

Pages rather than a plain Worker, for one reason. The site is served from a subdomain of iplust.in, whose DNS lives at an external provider. A Worker custom domain requires the zone to be on Cloudflare nameservers; a Pages custom domain accepts a CNAME from anywhere. Moving the zone would have meant recreating live mail records for the sake of one subdomain.

public/_headers carries the response headers. A static export has no server, so it replaces what headers() in next.config.ts would have set: the security headers, the cache policy per path, and the Content-Type for /opengraph-image, /twitter-image and /apple-icon. Next emits those three without a file extension, and without a stated media type they are served as application/octet-stream, which no link unfurler or home-screen installer will touch.

Hosts

Host Role
icon.iplust.in Canonical. Everything the site advertises points here
www.icon.iplust.in 301s to the canonical host
icon-atlas-studio.pages.dev Served as-is, so a deploy can be checked before DNS moves

functions/_middleware.js does the redirect. Hostname matching is not possible in _redirects, which understands only paths, so it needs a function. The pages.dev host is deliberately exempt.

DNS

Two CNAME records at the DNS provider for iplust.in:

Type Name Target
CNAME icon icon-atlas-studio.pages.dev
CNAME www.icon icon-atlas-studio.pages.dev

Both hostnames must be registered on the Pages project before the records are added. A CNAME pointing at pages.dev without that returns a 522.

Testing

npm run build
npm run test:e2e

The runner serves the static export on port 3210 through wrangler dev and drives it in the system Chrome through playwright-core, so no browser download is needed. Serving it the way Cloudflare does means the response headers, the content types on the generated images and the 404 behaviour are covered by the suite rather than discovered in production.

239 checks across nine suites cover the export pipeline, every toolbar, library, inspector and dialog control, editing and history, SVG import, the packaged export, the full 1,611-icon sheet, recovery from corrupt saved state, the layout at phone, tablet and desktop widths, and the search and offline surfaces.

The sharpest check crops the exported PNG at each icon's mapped coordinates and compares it against an independently rendered copy of the same icon. An off-by-one in the coordinate map would make every downstream consumer wrong while still producing a plausible-looking image, so it is verified pixel by pixel.

The import suite feeds in a hostile SVG carrying a script tag, an inline event handler, a foreignObject and a remote image, then asserts that none of it executes or reaches the DOM.

The offline suite loads the app, waits for the precache to fill, switches the network off and navigates again. A cached shell that loads but cannot reach its icon geometry looks fine in a screenshot and is useless in practice, so it counts the icons rather than assuming them: all 1,611 have to come back from cache, with the guide intact and no failed requests.

npm run lint        # ESLint
npm run typecheck   # tsc --noEmit, strict
npm run build:icons # Regenerate icon data after a lucide-react upgrade

Privacy

The editor runs entirely in the browser. Projects live in localStorage and in files you save yourself. Nothing you open, import or export is uploaded, because there is no backend to receive it, and there is no account or sign-in.

Page views and loading speed are counted with Cloudflare Web Analytics, which is cookieless and records no personal data. It ships only when NEXT_PUBLIC_CF_BEACON_TOKEN is set, so a build without that variable makes no third-party request at all. Google Analytics was considered and rejected: it would have required cookies and a consent banner, and would have made the privacy claims above untrue.


Limits

  • A sheet holds at most 4,000 icons, and a project at most 300 imported SVGs.
  • Raster exports are capped at 16,384px on a side, the ceiling browsers put on a canvas. The export dialog blocks the export and explains why before you hit it, and warns above 8,192px because some mobile GPUs cap textures at 4,096px.
  • Imported artwork must be flattened. Grouped or per-shape transforms are refused rather than drawn in the wrong place.

Roadmap

Shipped

  • Canvas with zoom, pan and fit
  • Icon library with ranked search and categories
  • Bulk placement
  • Grid layout: columns, cell size, gap, padding
  • Selection, ranges and drag reordering
  • Per-icon colour, size and rotation overrides
  • Transparent and solid backgrounds
  • PNG, WebP and SVG export
  • JSON, TypeScript and CSS output
  • Single-archive delivery and retina variants
  • Custom SVG import
  • Square packing, sort and deduplicate
  • Undo and redo
  • Project files
  • Responsive layout down to phone width
  • Offline support and installable as a progressive web app
  • Server-rendered guide, structured data, sitemap and social card
  • Static export deployed to Cloudflare's edge

Planned

  • Additional icon providers beyond Lucide
  • Per-icon export key renaming
  • Project templates
  • Trimmed and tightly packed atlases for non-uniform artwork
  • Command palette

Contributing

Contributions are welcome. See CONTRIBUTING.md for the full guide.

In short:

git checkout -b feature/my-feature
npm run lint && npm run typecheck
npm run build && npm run test:e2e
git commit -m "feat: add my feature"

Then open a pull request. Please keep npm run lint, npm run typecheck and npm run test:e2e green, and add a check for behaviour you change.


Bug reports

When reporting a bug, include:

  • Operating system, browser and version
  • Steps to reproduce
  • Expected and actual behaviour
  • Your project file if the issue depends on a specific sheet
  • Console errors and screenshots where relevant

License

MIT. See LICENSE.

Icons come from Lucide, which is ISC licensed.


Support the project

If Icon Atlas Studio is useful to you, star the repository, report bugs, suggest features, or share it with other developers.


I Plus T Solution

Built and maintained by I Plus T Solution

Website Live site GitHub

Icon Atlas Studio is released under the MIT licence. Icons come from Lucide, which is ISC licensed.

About

A professional visual editor for creating customizable icon atlases and sprite sheets with drag-and-drop, theming, pixel-perfect layout, and developer-ready exports.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages