Introduction
Thank you for purchasing Atelier. You have picked up a complete admin & dashboard template built as one standalone React 19 project — TypeScript, Vite, React Router and Tailwind CSS v4, over a design-token core. This document covers what is in the box, how to run it, how to theme it, and how to extend it.
What is Atelier?
Atelier is a starter kit for back-office software: SaaS consoles, analytics panels, e-commerce managers, CRMs, internal tools. It ships 114 pages — 17 niche dashboards, 10 full-screen web apps, a complete eCommerce suite, CRM / projects / crypto / NFT / jobs / blog modules, authentication and error screens, a UI component catalog, forms, tables, charts, maps and icons.
It is a front-end template. There is no database, no user model, no migrations and no auth backend. What you get is the entire interface layer, built to a single standard, ready for your own domain logic to be dropped in beside it.
The Bold Press design language
Atelier's look is deliberate and opinionated. Where most admin templates reach for soft shadows, big radii and glassy translucency, Atelier is built on four rules:
- One ink. A single
--at-inkcolor carries every border, every shadow and all strong text. Depth and emphasis are one gesture, not three. - Hard shadows. Offset, zero blur. A shadow is an object with an edge, not atmosphere.
- The press. Every clickable surface travels exactly its shadow offset on
:activeand lands flush against the page. Buttons feel mechanical. - Sharp geometry. Radii live between 0 and 6 pixels. Nothing is pillowy.
Every one of those is a token, not a hard-coded value — which is what makes the twelve color schemes and twelve type sets described later swap cleanly instead of half-applying.
React, not a port with React sprinkled on
Everything interactive is a real React component or hook: the collapsing sidebar, the dropdowns, the modals, the offcanvas customizer, the ⌘K command palette, the chart wrapper and the map. There is no jQuery, no Bootstrap, no CSS-in-JS and no third-party component library whose look you would have to fight.
| Concern | How it is expressed |
|---|---|
| Pages | 114 lazy-loaded route components under src/pages/, auto-registered by filename |
| Shell | Layout / AppShell / BareShell, chosen by route group |
| State | Hooks and one CustomizerContext — no Redux, no Zustand, nothing to learn |
| Styling | Tailwind v4 utilities over --at-* tokens, compiled once at build time |
| Types | TypeScript in strict mode; the build type-checks before it bundles |
Feature highlights
| Area | What you get |
|---|---|
| Pages | 114 routes |
| Dashboards | 17 — Sales, Analytics, eCommerce, CRM, Projects, Finance & Banking, Crypto, NFT, Jobs, HR & Payroll, LMS, Stocks, Healthcare, POS, Social, School, Podcast |
| Apps | 10 full-screen apps — Email, Chat, Calendar, Kanban, To-Do, File Manager, Gallery, Contacts, Notes, Media Player |
| Theming | 12 color schemes × 12 type sets × light / dark / system — composable, not fixed pairs |
| Layout | 3 sidebar styles, fluid or boxed width, collapsible icon rail, mobile drawer |
| Direction | Full RTL via logical CSS properties — the layout flips, the tokens do not |
| Data viz | ApexCharts wrapper that reads its palette from tokens and re-themes live |
| Extras | ⌘K command palette, live customizer, breadcrumb + active-trail engine driven by one manifest |
| Stack | React 19, TypeScript 5.9, React Router 7, Vite 6, Tailwind CSS v4 — no jQuery, no Bootstrap, no icon font |
What's Included
Unzip the download and you will find two folders:
atelier-react/
├── Documentation/
│ └── index.html ← this file
└── template/
├── src/ the React application
├── public/ static files served as-is
├── index.html the Vite entry + anti-flash script
├── package.json dependencies and the four npm scripts
├── vite.config.ts
├── tsconfig.json tsconfig.app.json tsconfig.node.json
└── README.md the 60-second version of this file
template/
A complete, independent project. Copy it out, rename it, and work in it — nothing outside the folder is needed and nothing inside it reaches out.
The dependency tree is not shipped. There is no node_modules/ in the package — run npm install and it is fetched from npm at the versions pinned in package.json.
Documentation/
One self-contained HTML file — the one you are reading. It has no build step and no dependencies beyond three webfonts, so it opens correctly straight off disk with a double-click.
License and changelog
Atelier is licensed through Envato under the Regular or Extended license you selected at checkout; the definitive terms are on your ThemeForest download page. Third-party libraries keep their own licenses — see Credits. Release history is in the Changelog.
The photographs, avatars and illustrations used in the demo content are for preview purposes only. They are not licensed for redistribution in your product. Swap them for your own media before you ship.
Requirements
Short list — this is a static front end, so almost everything here is for your machine, not your server.
| What | Version | Needed for |
|---|---|---|
| Node.js | 20.19+ or 22.12+ | The dev server and the production build |
| npm | 10+ (ships with Node) | Installing dependencies. Yarn and pnpm work too. |
| Your server | — | Nothing. The build output is static files. |
The Node floor is Vite's, not ours. Vite 6 refuses to start on anything below 20.19 or 22.12. Check yours with node -v.
Node is a build tool, not a runtime
Node compiles the TypeScript, the CSS and the JavaScript once, into a folder of hashed static assets. Your production host never runs Node — it serves those files. You can build locally and upload the output to anything that serves HTML.
General tooling
- A code editor — anything. VS Code with the Tailwind CSS IntelliSense extension is a comfortable default.
- A terminal — every command in this document is run from the
template/folder. - A modern browser — the last two versions of Chrome, Edge, Firefox or Safari. Internet Explorer is not supported and never will be; the design leans on CSS custom properties,
color-mix()and logical properties.
Quick Start
Three commands from download to a running app.
cd template
npm install
npm run dev # http://localhost:5181
The entry route resolves to /dashboards/sales. If the first thing you see is the Sales dashboard, everything is wired correctly.
The four scripts
| Command | What it does |
|---|---|
npm run dev | Vite dev server on port 5181, with hot module replacement. |
npm run build | tsc -b then vite build → dist/. A type error fails the build. |
npm run preview | Serves dist/ locally, so you can check the real build before deploying. |
If the page loads unstyled
The dev server compiles Tailwind on the fly, so an unstyled page almost always means npm install did not finish. Delete node_modules/ and the lockfile, run it again, and watch for an error rather than a warning.
The React App
Overview
React 19 with TypeScript, routed by React Router as a single-page app, styled by a Tailwind v4 token core. Dropdowns, modals, the customizer, the offcanvas sidebar and the command palette are all native React components and hooks — there is no second interactivity layer to reason about.
Installation and development
cd template
npm install
npm run dev # http://localhost:5181
Production build
npm run build # tsc -b && vite build → dist/
npm run preview
The build type-checks first, so a type error fails the build rather than shipping. Output is a static SPA bundle.
Because this is a single-page app, your host must rewrite unknown paths to index.html or a deep link like /apps/chat will 404 on refresh. See Deployment for the one-line config on each major host.
Project structure
template/
├── src/
│ ├── main.tsx App.tsx
│ ├── pages/ 114 screens, mirroring the route slugs
│ │ └── pageRegistry.ts auto-glob: filename → slug → lazy import
│ ├── components/
│ │ ├── shell/ sidebar, header, footer, customizer, palette
│ │ ├── ui/ the component catalog
│ │ ├── charts/ maps/ auth/
│ ├── context/CustomizerContext.tsx
│ ├── hooks/ useClickOutside, useFocusTrap
│ ├── lib/ storage · theme · nav · sidebar · manifest
│ ├── data/ nav manifest + demo JSON
│ └── styles/ the design-token core and app layers
├── index.html
└── vite.config.ts
Notes
src/lib/storage.ts,theme.tsandnav.tsown theat:localStorage keys and thedata-at-*attributes on<html>. Everything that reads or writes theme state goes through them; nothing else toucheslocalStoragedirectly.- The anti-flash script still runs first in
index.html, before React mounts. Removing it reintroduces a flash of the wrong theme on every load. - Routes are not hand-written.
src/pages/pageRegistry.tsglobs every module undersrc/pages/, derives each one's manifest slug from its filename (dashboards/Sales.tsx→dashboards/sales) and lazy-imports it — so adding a screen is adding a file. A slug that a filename cannot express (TSX files may not start with a digit, soerror/404isError404.tsx) is pinned in theSLUG_OVERRIDESmap at the top of that file.
Project Structure
Four pillars carry the whole template. Recognize them once and the rest of the tree explains itself.
The four pillars
| Pillar | What it is | Where |
|---|---|---|
styles/ | The Tailwind v4 entry, the token chain and the app layers | src/styles/, imported once in src/main.tsx |
data/ | nav-manifest.json (the route registry) + demo JSON | src/data/, bundled via resolveJsonModule |
lib/ | Theme restore, storage, customizer registry, nav, manifest helpers | src/lib/ — plain TypeScript, no React imports |
The <head> contract | Anti-flash script, fonts, meta — in that order | index.html, before React mounts |
The stylesheet, in load order
src/styles/app.css is the single entry point for CSS. Its import order is the design system's law — primitives before roles, roles before schemes, schemes before recipes:
/* 1 */ @import 'tailwindcss';
/* 2 — the token chain */
@import './tokens/_primitives.css'; raw scales, the ink and shadow system
@import './tokens/_roles.css'; role aliases: light :root + [data-at-theme=dark]
@import './tokens/_themes.css'; 11 color schemes (Atelier is inline on :root)
@import './tokens/_typography.css'; 11 type sets (Atelier is inline on :root)
@import './tokens/_charts.css'; the data-viz palette
@import './tokens/_recipes.css'; .at-card, .at-press, .at-stamp, focus rings
/* 3 — app layers */
@import './base.css'; './components.css'; './nav.css';
@import './shell.css'; './utilities.css'; './schemes.css';
/* 4 — @theme bridge: --at-* roles → Tailwind utilities */
Nothing below the token chain invents a value. Every color, radius, spacing step and shadow in base, components, nav, shell and schemes resolves through a --at-* variable — which is precisely why a scheme swap re-paints the entire application without a rebuild.
The TypeScript core
src/lib/ holds the framework-free half of the app: plain modules with no React imports, so they are testable on their own and safe to call from anywhere.
| Module | Responsibility |
|---|---|
lib/storage.ts | The at: localStorage helper. Never throws — storage may be blocked. |
lib/theme.ts | Applies theme, scheme and type set; tracks OS theme when the mode is system. |
lib/nav.ts | Active trail, breadcrumb and page title, derived from the manifest. |
lib/sidebar.ts | The collapsed icon rail and the mobile drawer. |
lib/manifest.ts | Loads the route registry and resolves slugs to nodes. |
| Component | Responsibility |
|---|---|
context/CustomizerContext.tsx | The authoritative attribute ↔ storage-key ↔ default map, exposed as one provider. |
components/shell/CommandPalette.tsx | ⌘K / Ctrl-K search across every route. |
components/ui/Icon.tsx | The inline-SVG renderer. |
components/charts/ApexChart.tsx | The ApexCharts wrapper — lazy import, token palette, live re-theme. |
One manifest, three surfaces
data/nav-manifest.json is read once per page and drives three things at the same time: the sidebar's active trail and open groups, the breadcrumb above each page title, and the ⌘K command palette's index. Add a route there and all three pick it up. That is covered in Adding pages.
Theming & Design Tokens
Everything visual in Atelier is a CSS custom property in the --at-* namespace. There are no hex codes in the component layer. This is what makes twelve color schemes, twelve type sets and a dark mode possible without a rebuild — and it is what you should work with when you brand the template.
How the tokens are layered
Three layers, each one only allowed to consume the layer above it:
| Layer | File | Holds |
|---|---|---|
| 1 — Primitives | _primitives.css | Raw scales: the spacing ramp, the radius ramp, the type ramp, the ink and hard-shadow system, semantic color ramps. Values with no meaning attached. |
| 2 — Roles | _roles.css | Meaningful aliases: --at-canvas, --at-surface, --at-ink, --at-text-muted, --at-accent. Declared for light on :root and re-declared for dark under [data-at-theme='dark']. |
| 3 — Schemes | _themes.css | Eleven blocks that override layer 2 for the eleven non-default schemes, each with its own light and dark pair. Atelier, the default, is the inline :root set in layer 2. |
A fourth file, _recipes.css, turns those roles into the Bold Press primitives every component composes: .at-card (surface + 2px ink border + hard shadow), .at-press (the mechanical depress), .at-stamp (the solid active-nav marker) and the focus ring.
The roles you will actually touch
| Token | Role |
|---|---|
--at-ink | The signature. Every border, every hard shadow, all strong text. |
--at-canvas | The page behind everything. |
--at-surface, --at-surface-subtle | Cards, panels, table headers, wells. |
--at-text, --at-text-muted, --at-text-subtle | The three body-copy weights. |
--at-accent | The primary fill — buttons, the active nav stamp, chart series 1. |
--at-accent-text | The primary foreground — links and accent-colored type. A darker cut of the same hue, because a color bright enough to sit behind ink is never dark enough to read as text on the canvas. |
--at-on-accent | What sits on top of an accent fill. |
--at-secondary, --at-tertiary, --at-lime | The supporting color blocks. |
--at-success, --at-warning, --at-danger, --at-info | Status. |
--at-accent and --at-accent-text are two separate tokens on purpose. If you brand the template and set only one of them, either your buttons or your links will fail contrast. Set both.
Changing the look
Pick the smallest hammer that does the job.
1. Re-brand the default scheme
Open styles/tokens/_roles.css and change the role values on :root (light) and on [data-at-theme='dark'] (dark). This re-brands the whole application — every page, every component, every chart — and the other eleven schemes are untouched.
:root {
--at-ink: #1b2430; borders, shadows, strong text
--at-canvas: #eef1f5;
--at-surface: #ffffff;
--at-accent: #6ea8fe; the fill
--at-accent-text: #1f5bbd; the foreground — do not skip this
--at-on-accent: #1b2430;
}
2. Add a thirteenth scheme
Append a block to _themes.css following the shape of the existing eleven, then add a matching tile to the customizer panel:
[data-at-theme-preset='harbor'] {
--at-ink: …; --at-canvas: …; --at-accent: …; …the full role set
}
[data-at-theme='dark'][data-at-theme-preset='harbor'] {
…the dark counterpart. Both are required.
}
3. Use the Tailwind utilities
The @theme block at the bottom of app.css bridges every role token into Tailwind's namespaces, so bg-surface, text-ink, border-default, text-accent, shadow-card and font-display all exist as ordinary utilities. Because the bridge is late-bound through var(), a runtime scheme swap re-paints those utilities too — no rebuild.
Prefer a utility or a token over a literal value. bg-surface follows the reader into dark mode and into all twelve schemes; bg-[#fff] does not.
Color Schemes & Dark Mode
Light, dark and system
Mode is stored under at:theme as one of light, dark or system. The resolved value — never the literal system — is written to <html data-at-theme>, so CSS only ever has two states to match. When the mode is system, a matchMedia listener re-resolves it if the reader changes their OS setting while the tab is open.
The twelve color schemes
Each scheme is a complete identity — canvas, ink, accent, secondary, tertiary and the full semantic set — with a purpose-built dark counterpart, not a mechanical inversion. Switching one changes the world the app lives in, not a single highlight color.
The scheme id is stored under at:theme-preset and written to <html data-at-theme-preset>. Atelier is the default and writes no attribute — its values are the inline :root set, so the DOM stays clean on an untouched install. Every other scheme writes its id.
<!-- Sorbet, dark -->
<html data-at-theme="dark" data-at-theme-preset="sorbet">
<!-- Atelier, light — note the absent attributes -->
<html data-at-theme="light">
Dark mode is designed, not derived
Every scheme has a hand-tuned dark pair. In dark, Atelier splits the ink token in two, and the other schemes follow the same rule:
--at-inkbecomes a muted cut of the accent hue — enough contrast to read as an edge, not so much that a page of bordered cards glows.--at-ink-strongkeeps full strength and is what type and solid fills use.
Collapsing them back into one value is the single most common way to break dark mode here: borders shout or text mumbles, and there is no value that satisfies both.
The anti-flash script
The first executable thing in every <head>, before any stylesheet, is a small synchronous IIFE. It reads the at: keys, resolves system through matchMedia, and writes every data-at-* attribute plus lang and dir onto <html> before the first paint. It runs in about a millisecond and is wrapped in try/catch so blocked storage can never stop the page.
Do not move it below the stylesheet, do not defer it, and do not convert it to a module. Any of those reintroduces a flash of the wrong theme on every single page load. It is duplicated in every edition for exactly this reason.
Storage schema and migrations
All keys are namespaced at: and versioned by at:schema, currently 2. On boot the script compares the stored version: a recognized older version is migrated, an unrecognized shape is wiped rather than trusted. That is what stops a stale key from a previous major version rendering someone a broken layout.
Typography
Typography is its own axis, independent of the color scheme. Twelve type sets live in tokens/_typography.css under data-at-type-preset, and any of them composes with any of the twelve color schemes — 144 combinations, all valid. Sorbet's palette with Stencil's condensed display face is a supported choice.
| Type set | Display | Body |
|---|---|---|
| Atelier (default) | Fraunces | Libre Franklin |
| Electric | Archivo | Instrument Sans |
| Sage | Bricolage Grotesque | Manrope |
| Violet | Sora | Manrope |
| Lagoon | Outfit | Figtree |
| Foundry | Saira | IBM Plex Sans |
| Stencil | Antonio | Public Sans |
| Gantry | Oxanium | IBM Plex Sans |
| Lacquer | Playfair Display | DM Sans |
| Nocturne | Chivo | Mulish |
| Primer | Zilla Slab | Public Sans |
| Sorbet | Gabarito | Plus Jakarta Sans |
JetBrains Mono is constant across all twelve. It sets every figure in the interface — KPI values, table numerics, chart axes, code — so numbers stay column-aligned whatever else changes.
The three font tokens
| Token | Used for |
|---|---|
--at-font-display | Headings, KPI values, the wordmark, nav labels. Consumed at weights 600–800 and at sizes down to 12px — so a replacement family must ship those as real drawn weights, not synthesised ones. |
--at-font-sans | Body copy, form labels, table cells, everything else. |
--at-font-mono | All numerics and code. |
Swapping in your own fonts
Two edits, in this order:
- In
index.html, replace the Google Fonts<link>with your own — self-hosted@font-facerules work equally well. - In
tokens/_roles.css, point--at-font-display/--at-font-sans/--at-font-monoat the new families, keeping a system fallback stack.
All twelve type sets are requested in a single Google Fonts URL, so switching a type set at runtime re-types the page instantly with no new network request. If you only ever ship one set, trimming that URL down to it is the single largest easy win on page weight.
RTL & Languages
How RTL works here
Atelier is built with logical CSS properties throughout — margin-inline-start rather than margin-left, padding-block rather than padding-top/bottom, inset-inline-end rather than right. Setting dir="rtl" on <html> therefore flips the entire layout — sidebar, nav carets, table alignment, charts, the customizer panel — while changing no token values at all. There is no separate RTL stylesheet to maintain and no app.rtl.css to keep in sync.
Turning on RTL
Three ways, all equivalent:
- The customizer — Direction → RTL. Applies live, persists to
at:dir. - The header language menu — picking العربية sets
lang="ar"and flips direction with it. - In code —
document.documentElement.setAttribute('dir','rtl').
Making RTL the default
Edit the anti-flash script's direction default in index.html. It currently derives direction from the stored language, defaulting to LTR:
/* before */
var dir = dirStored ? dirStored : (lang === 'AR' ? 'rtl' : 'ltr');
/* after — RTL unless the reader has chosen otherwise */
var dir = dirStored ? dirStored : 'rtl';
Change <html lang="en"> to your locale at the same time, and if that locale needs a different script, swap the font families as described in Typography.
The language menu
The header ships a language dropdown with English, العربية and Español. It is a direction and locale switch, not a translation engine — the template ships no translation strings, because which i18n library you use is your decision, not ours. Picking a language sets lang, derives dir (Arabic → RTL), and persists both:
setLang(code) {
document.documentElement.setAttribute('lang', code)
const dir = code === 'ar' ? 'rtl' : 'ltr'
document.documentElement.setAttribute('dir', dir)
storage.set('lang', code.toUpperCase())
storage.set('dir', dir)
}
To wire real translations, hook your i18n library into that same function — the direction handling is already done for you. To add a language, add a menu item; to add another RTL locale (Hebrew, Persian, Urdu), extend the code === 'ar' test to a set.
If you write new markup, use logical properties. A single margin-left is invisible in LTR and misaligns the moment someone flips the app.
Live Customizer
The customizer is the panel behind the palette button in the header. Every control applies live — no reload — and persists, so a reader's configuration survives navigation and return visits.
Controls
| Control | Options | Storage key | Attribute |
|---|---|---|---|
| Color Scheme | the 12 schemes | at:theme-preset | data-at-theme-preset |
| Typography | the 12 type sets | at:type-preset | data-at-type-preset |
| Mode | Light · Dark · System | at:theme | data-at-theme (resolved) |
| Direction | LTR · RTL | at:dir | dir |
| Sidebar Style | Default · Compact · Expand | at:sidebar-style | data-at-sidebar-style |
| Width | Fluid · Boxed | at:width | data-at-width |
| Reset | — | clears every key | clears every attribute |
The three sidebar styles are genuinely different geometries, not degrees of one:
- Default — collapsible. The header's hamburger toggles between the full sidebar and a narrow icon rail. Collapsed is the initial state.
- Compact — locked to the icon rail, with flyout submenus for nested routes.
- Expand — locked open at full width.
Compact and Expand each lock one geometry, so there is nothing to collapse and the desktop hamburger is hidden for both. Below 993px all three become the same thing — the sidebar is an off-canvas drawer and the hamburger is its trigger.
The scheme and type-set tiles
Each color tile paints its own scheme's canvas, ink, accent and secondary as literal hex values, and each typography row renders its sample in the faces it applies. Both have to show a world that is not the active one, which is the one place in the codebase where hardcoded values are correct. If you add or re-brand a scheme, update its tile in partials/customizer.html to match — nothing else reads those values.
Beyond the panel
The REGISTRY in js/core/customizer.js is the authoritative map of every tunable axis, and it carries more than the panel exposes:
| Registry name | Attribute | Key | Default |
|---|---|---|---|
menu | data-at-menu | at:menu | click |
page | data-at-page | at:page | regular |
headerPosition | data-at-header-position | at:header-position | fixed |
sidebarPosition | data-at-sidebar-position | at:sidebar-position | fixed |
headerScheme | data-at-header | at:header-scheme | light |
Set any of them from the console or from your own code, and they behave exactly like the panel's controls:
import { setByName } from './js/core/customizer.js'
setByName('headerPosition', 'static')
To surface one in the panel, add a segmented control that calls setByName() with that name. No other wiring is needed — the registry already knows the attribute, the key and the default.
Navigation orientation is hard-coded vertical, and the sidebar surface scheme follows light/dark automatically. Neither is a tunable, which is why neither appears above.
Making a Configuration the Default
The customizer is a demo affordance. In a real product you usually want your configuration to be what every visitor sees — and often you want the panel gone entirely.
How settings persist
Every axis follows one convention, and understanding it is most of the work:
The default writes nothing. Selecting a value equal to the default removes the attribute from <html> and deletes the storage key. Only non-defaults are written. That keeps the DOM clean and makes "has this reader chosen anything?" a question storage can answer.
One key is deliberately inverted. at:collapsed exists because the icon rail is the default state for the Default sidebar style — so the attribute is written when the key is absent, and the key stores '0' only as the opt-out the hamburger writes when a reader expands the sidebar.
The full key list
| Key | Values | Default |
|---|---|---|
at:schema | 2 | written on boot |
at:theme | light · dark · system | light |
at:theme-preset | a scheme id | atelier |
at:type-preset | a type-set id | atelier |
at:lang · at:dir | EN/AR/ES · ltr/rtl | EN · ltr |
at:sidebar-style | default · compact · expand | default |
at:collapsed | '0' = expanded (inverted) | absent = collapsed |
at:width | fluid · boxed | fluid |
at:menu · at:page | click/hover · regular/… | click · regular |
at:header-position · at:sidebar-position | fixed · static | fixed |
at:header-scheme | light · dark | light |
at:accent-custom | a hex string | absent |
Baking your configuration in
Step 1 — find the configuration you want. Open the app, set the customizer exactly how you like it, then read the result back from the console:
Object.keys(localStorage)
.filter(k => k.startsWith('at:'))
.forEach(k => console.log(k, '=', localStorage.getItem(k)))
Step 2 — change the defaults in the anti-flash script. In index.html, the fallbacks are the defaults. Change them and every first-time visitor gets your configuration before the first paint:
/* was */ var theme = get('at:theme') || 'light';
/* now */ var theme = get('at:theme') || 'dark';
/* was */ var preset = get('at:theme-preset') || 'atelier';
/* now */ var preset = get('at:theme-preset') || 'nocturne';
Step 3 — match the registry. Update the same defaults in REGISTRY (js/core/customizer.js), or Reset will restore the old ones and the panel will show the wrong control as active. The two must agree.
Step 4 (optional) — remove the panel. Delete the customizer partial's include and the header's palette button. Nothing else depends on it; theming keeps working, readers just cannot change it.
Forcing a new default onto returning visitors
Readers who already have at: keys stored keep their old configuration — that is the point of persistence. To push a new default to everyone, bump the schema version: change the at:schema value the script writes (from '2' to '3') and let the mismatch branch wipe the at: namespace. Every visitor then re-derives from your new defaults on their next load.
That is a one-way reset — it discards every preference every reader has set. Use it when you have genuinely changed the product's identity, not for a routine deploy.
Adding & Editing Pages
The two things every page needs
- A file under
src/pages/. - A manifest entry, if you want it in the sidebar, the breadcrumb and the ⌘K palette.
The second is optional. A page with no manifest entry still renders and is still reachable by URL — it just is not advertised anywhere.
Where pages live
One file per route, at src/pages/<section>/<Page>.tsx. There is no route to register: src/pages/pageRegistry.ts globs every module under src/pages/, turns its filename into a slug (dashboards/Sales.tsx → dashboards/sales) and lazy-imports it. Adding a screen is adding a file.
A slug a filename cannot express — TSX files may not start with a digit, so error/404 is Error404.tsx — is pinned in the SLUG_OVERRIDES map at the top of pageRegistry.ts.
Anatomy of a page
// src/pages/reports/Summary.tsx
import { PageHead } from '../../components/shell/PageHead';
export default function Summary() {
return (
<>
<PageHead title="Summary" />
{/* your content */}
</>
);
}
That is the whole contract. The shell — sidebar, header, footer, customizer, command palette — is supplied by the layout route, chosen from the slug: apps/* gets AppShell, auth/* and error/* get BareShell, everything else gets Layout. <PageHead> renders the breadcrumb and the <h1>, both resolved from the manifest, and each shell keeps data-at-route on <html> in step as you navigate.
Registering the route
Add a node to data/nav-manifest.json:
{
"id": "reports.summary",
"title": "Summary",
"slug": "reports/summary",
"icon": "chart-bar",
"parent": "grp.reports",
"section": "MAIN",
"order": 1,
"badge": null,
"keywords": ["summary", "report"],
"inMenu": true,
"alias": null,
"external": false
}
| Field | Meaning |
|---|---|
id | Unique. Dotted convention, <group>.<page>. |
slug | The route, without extension. Must match data-at-route. |
parent | The group node this nests under, or null for top level. |
section | Which sidebar heading it falls under — MAIN, APPLICATIONS, MODULES, PAGES, UI & FORMS. |
order | Sort position within its parent. |
keywords | Extra terms the ⌘K palette matches on. |
inMenu | false keeps the route working but hides it from the sidebar. |
Worked example — a new report page
- Copy
src/pages/dashboards/Analytics.tsxtosrc/pages/reports/Summary.tsx. - Rename the component to
Summaryand set<PageHead title="Summary" />. - Strip the demo content, keeping the
<PageHead>. - Add the manifest node above (and a
grp.reportsgroup node if the group is new). npm run dev→ the page is at/reports/summary, the sidebar shows it, the breadcrumb reads Reports › Summary, and ⌘K finds it.
Start from the page nearest to what you want rather than from a blank file. Every page in the template already carries the correct shell, spacing rhythm and component usage, and copying keeps you inside the design system by default.
Charts
All data visualization is ApexCharts, wrapped by src/components/charts/ApexChart.tsx. The wrapper does three things worth knowing about: it lazy-imports ApexCharts so a page with no chart never pays for it, it reads its palette from the --at-chart-* tokens, and it re-renders every chart on the page when the theme changes.
Using a chart
One component, fully typed. Drop it in and you are done:
import { ApexChart } from '../../components/charts/ApexChart';
<ApexChart
type="area"
height={280}
categories={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}
series={[{ name: 'Revenue', data: [31, 40, 28, 51, 42, 109] }]}
/>
| Prop | Type | Default |
|---|---|---|
type | 'line' | 'area' | 'bar' | 'donut' | 'pie' | 'radialBar' | 'polarArea' | required |
series | ApexOptions['series'] | required |
categories | string[] — x-axis labels | — |
labels | string[] (donut / pie / radial) | — |
height | number, px. On a chart that fills its card (an .at-chart__body that is a .at-card’s own child) this is a floor, not a fixed size: the chart is handed 100% and keeps re-fitting as the row’s height moves. Anywhere else — sparklines, a chart nested in a column — it is the exact height. | 260 |
color | string — a hex, or a token name such as --at-accent | the token palette |
sparkline | boolean — strips axes, grid and padding | false |
stacked · horizontal | boolean | false |
legend · values · track | boolean | false |
tooltip | boolean — false to suppress | true |
centerLabel · centerValue | string — donut center label and figure | — |
unit | string — a suffix for formatted values | — |
Charts for data that arrives later
Nothing special to do: series is a prop, so pass state and the chart follows it. The wrapper mounts ApexCharts once per element and updates in place rather than re-creating the instance.
const [series, setSeries] = useState<ApexAxisChartSeries>([]);
useEffect(() => {
fetch('/api/revenue').then(r => r.json()).then(setSeries);
}, []);
return <ApexChart type="area" height={280} series={series} />;
Live re-theming
Charts are canvas and SVG, so unlike the rest of the interface they cannot inherit a CSS variable change. The wrapper listens for the at:change event that every customizer write dispatches, re-reads the --at-chart-* tokens and re-renders. That is why switching scheme or mode re-colors the charts along with everything else.
If you build a component that changes theme state without going through the customizer, dispatch the same event so charts stay in step:
window.dispatchEvent(new CustomEvent('at:change', { detail: { mode: 'dark' } }))
Axis formatting
The wrapper compacts axis numbers (74210 → 74.2k) and computes a "nice" axis ceiling that hugs the data while landing on readable ticks — 0 · 4k · 8k · 12k, never 0 · 4.25k · 8.5k. Both behaviors are in ApexChart.tsx and can be replaced if your data wants different treatment.
ApexCharts touches window, so it is imported inside an effect rather than at module scope. If you move the template onto a server-rendered framework later, that is the line that keeps it from running on the server.
Maps & Icons
Maps
Mapping is Leaflet — open source, no API key, no account, no per-view billing. The demo is at maps/leaflet, and src/components/maps/LeafletMap.tsx imports the library inside an effect, so its ~150 kB only lands on the route that renders it.
Leaflet's own stylesheet and Atelier's overrides are static imports at the top of that component, because bundlers cannot code-split CSS that sits behind a dynamic import. Render <LeafletMap /> anywhere and both come with it.
Tiles come from OpenStreetMap by default. Swap in Mapbox, Stadia, Carto or your own tile server by changing the tile-layer URL; check the terms of whichever provider you point at before shipping.
Icons
Every icon in Atelier is an inline SVG. There is no icon font, no sprite sheet and no icon package in package.json. The advantages are practical: icons inherit currentColor, scale without hinting artifacts, cost no extra request and cannot flash-of-unstyled on load.
src/components/ui/Icon.tsx renders one, wrapping inner path data in the house contract — viewBox="0 0 24 24", fill="none", stroke="currentColor", round caps and joins — so every icon in the interface is geometrically consistent:
import { Icon } from '../../components/ui/Icon';
<Icon path='<circle cx="12" cy="12" r="10"/>' />
<Icon path={COMPASS} size={14} stroke={2.5} />
Color is never a parameter — it comes from the surrounding color, so an icon inside a button is automatically the button's foreground in every scheme and both modes.
Adding an icon
Pass the inner markup only; the wrapper <svg> is generated. Keep a shared set in a module and import it where you need it:
export const ICONS = {
// …
compass: '<circle cx="12" cy="12" r="10"/><path d="m16.24 7.76-2.12 6.36-6.36 2.12 2.12-6.36z"/>',
};
Any 24×24 stroke icon set drops in unchanged. For a one-off, paste the SVG straight into the JSX — that is what most pages do.
Match the stroke width of the icons already in place (2 for most, 2.5 for small chrome) or the new icon will read heavier or lighter than its neighbours at the same size.
Plugins Used
Atelier is built on a deliberately short dependency list. Nothing here is jQuery-era, nothing is a UI kit whose look must be fought, and every runtime library is present because a page genuinely needs it.
Runtime libraries
| Library | Version | Used for |
|---|---|---|
| React · React DOM | 19 | The whole interface |
| React Router | 7 | Routing, from one nav manifest |
| Tailwind CSS | v4 | The utility layer and the @theme bridge |
| ApexCharts | v4 | Every chart — lazily imported |
| Leaflet | 1.9 | The interactive map — lazily imported |
| Floating UI | 1.8 | Dropdown, tooltip and popover positioning |
Six runtime dependencies, and two of the six are fetched only by the routes that use them.
Build tooling
| Tool | Role |
|---|---|
| Vite 6 | Dev server and production bundler. |
@vitejs/plugin-react | Fast Refresh in development, JSX transform in the build. |
@tailwindcss/vite | Tailwind v4's Vite integration. No PostCSS config, no tailwind.config.js — v4 is configured in CSS. |
| TypeScript 5.9 | strict mode. tsc -b runs as part of npm run build, so a type error fails the build. |
What is deliberately absent
- No jQuery, and no plugin that requires it.
- No Bootstrap or other CSS framework beyond Tailwind.
- No icon font — see Icons.
- No CSS-in-JS, no styled-components, no runtime style injection — one compiled stylesheet.
- No state library — hooks and one context are the whole state story.
Each library is distributed under its own license — see Credits.
Deployment
At a glance
npm run build # type-check, then bundle → dist/
npm run preview # check dist/ locally before you upload it
Upload dist/. Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3 + CloudFront, or any nginx/Apache document root all serve it, and none of them need Node at runtime.
The one piece of configuration
This is a single-page app, so unknown paths must fall back to index.html — without it, a refresh on a deep link such as /apps/chat returns a 404:
# netlify.toml
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# vercel.json
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }
# nginx
location / {
try_files $uri $uri/ /index.html;
}
# Apache — .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
Deploying under a subfolder
If the app will not live at the domain root, build with the path Vite should prefix its asset URLs with, and hand the same value to the router:
npm run build -- --base=/admin/
Then set basename={import.meta.env.BASE_URL} on the <BrowserRouter> in src/App.tsx, so route matching and asset loading agree on the prefix.
Caching
Every build content-hashes its asset filenames, so the compiled CSS and JS can be cached forever:
Cache-Control: public, max-age=31536000, immutable # hashed assets
Cache-Control: public, max-age=0, must-revalidate # HTML documents
nav-manifest.json and the demo JSON are bundled rather than fetched, so they are hashed with everything else. If you switch them to runtime fetch, keep them out of the long cache — a stable filename plus an immutable cache outlives every edit.
Frequently Asked Questions
Can I drop this into an existing React app?
Yes, piece by piece. src/styles/ is self-contained, so importing app.css gives you the token core, the Bold Press recipes and the Tailwind bridge on their own. From there, copy the shell components you want; they take props and context, not globals.
Can I use just the CSS?
Yes. src/styles/ is self-contained — copy it into your project and import app.css. You get the token core, the Bold Press recipes and the Tailwind bridge without any of the markup or any React at all. Interactive components need their JavaScript; static styling does not.
Is TypeScript required?
No. Rename the files to .jsx, drop tsc -b from the build script and it is a plain JavaScript project. You lose the type-check, not the template.
Can I move it onto a server-rendered React framework?
The pages and components are ordinary React with no Vite-only APIs, so they port. Two things need attention: routing is React Router here, and ApexCharts and Leaflet are browser-only — keep them behind a client boundary wherever the first render happens on the server.
How do I change the primary color?
Set --at-accent and --at-accent-text in tokens/_roles.css, for both the light :root block and the dark one. See Theming & tokens.
Does it support dark mode and RTL?
Both, first-class. Dark mode is hand-designed for every one of the twelve schemes, not derived by filter. RTL is built on logical properties, so dir="rtl" flips the layout with no separate stylesheet.
Can I remove the customizer?
Yes — drop <Customizer /> from the shells and the header's palette button. Theming keeps working; readers just cannot change it. See Default config.
Are the demo images licensed for production?
No. The photographs, avatars and illustrations are preview-only. Replace them with your own media, or media you hold rights to, before you ship.
Is there a backend, auth or database?
No. Atelier is the interface layer. The auth screens are UI: real forms, real validation states, no server behind them.
What browsers are supported?
The last two versions of Chrome, Edge, Firefox and Safari, desktop and mobile. Internet Explorer is not supported.
Where do updates come from?
Your ThemeForest Downloads page. Re-download the item to get the latest version; see Support & updates.
Support & Updates
Thank you for choosing Atelier. Here is where to look first, and how to reach a human if that is not enough.
Before you reach out
Most questions are answered fastest by what is already in your download:
- This documentation — installation, building, deploying, theming and the structure of the project.
- The live demo at atelier.jawad.work — every page and feature running, and a quick way to confirm expected behavior.
- The code itself — the shared core is heavily commented, and the comments explain why a thing is the way it is, which is usually the question behind the question.
Getting help
Contact the author through the item's page on ThemeForest — the Comments tab for general questions, the author's Support tab for item support. Please include:
- your Node and npm versions (
node -v,npm -v), - what you expected versus what happened, and the exact error text if there is one.
Support email: jawad.ahbab.turna@gmail.com
Item support covers defects, questions about the included features, and help getting the template running. It does not cover customization work, third-party integration or your own application code — though if you want that built, ask; it can be arranged separately.
Updates
Atelier is updated through ThemeForest. When a new version ships, sign in and re-download the item from your Downloads page — your purchase entitles you to the updates published for this item. The Changelog records what changed in each release, so you can review before upgrading. Upgrading is a matter of merging the changed files into your project.
Keep your changes in version control from day one. Then pulling in a future release is a reviewable merge rather than a hunt for everything you once edited.
Changelog
v1.0.0 — Initial release
- React 19 + TypeScript 5.9 + React Router 7 on Vite 6 and Tailwind CSS v4, over one
--at-*token core. - 114 routes — 17 niche dashboards, 10 full-screen apps, a complete eCommerce suite, CRM / projects / crypto / NFT / jobs / blog modules, authentication and error screens, a UI component catalog, forms, tables, charts, maps and icons.
- The Bold Press design language: one ink token driving borders, hard offset shadows and strong text, plus the mechanical press interaction on every clickable surface.
- Twelve color schemes and twelve type sets as independent, freely composable axes.
- Hand-designed dark mode for every scheme, with the two-ink split that keeps borders quiet and type legible.
- Full RTL through logical properties — no second stylesheet.
- Live customizer with light/dark/system, direction, three sidebar styles and fluid/boxed width, all persisted under versioned
at:storage. - ⌘K command palette, breadcrumb and active-trail engine, all driven by one nav manifest.
- ApexCharts wrapper with token-driven palettes and live re-theming; Leaflet maps; an inline-SVG icon registry.
Credits
Atelier stands on a lot of excellent open-source work. Thank you to the maintainers and communities behind everything listed here.
Fonts
All typefaces are served through Google Fonts, under the SIL Open Font License:
- Display faces — Fraunces, Archivo, Bricolage Grotesque, Sora, Outfit, Saira, Antonio, Oxanium, Playfair Display, Chivo, Zilla Slab, Gabarito.
- Body faces — Libre Franklin, Instrument Sans, Manrope, Figtree, IBM Plex Sans, Public Sans, DM Sans, Mulish, Plus Jakarta Sans.
- Monospace — JetBrains Mono, constant across all twelve type sets.
Icons
Icon path data follows the Lucide / Tabler Icons convention (24×24 viewBox, currentColor stroke). Both are MIT-licensed. Icons are inlined as SVG — there is no icon font or icon package to install.
Libraries
- Tailwind CSS v4 — the utility layer the design system bridges into. MIT.
- ApexCharts — every chart. MIT.
- Leaflet — the interactive map. BSD-2-Clause. Default tiles from OpenStreetMap contributors, ODbL.
- Floating UI — dropdown, tooltip and popover positioning. MIT.
- React and React DOM, and React Router — the application and its routing. MIT.
- Vite and @vitejs/plugin-react — the dev server and build. MIT.
- TypeScript — Apache-2.0.
Each project is distributed under its own license; refer to the respective projects for the full terms.
Demo media
The photographs, avatars and illustrations shown throughout the template are included for preview purposes only and are not licensed for redistribution in your own product. Replace them with your own assets, or media you hold the rights to, before deploying to production.
License
Atelier itself is licensed through Envato under the Regular or Extended license you purchased. In short: the Regular license covers one end product that is not sold to end users; the Extended license covers one end product that is. The authoritative terms are on your ThemeForest download page and at themeforest.net/licenses.
Thank you again to every author and contributor whose work makes Atelier possible — and thank you for choosing it.