react-upload-pro
v0.1.2
Published
The most feature-rich React file upload & dropzone library. Drag & drop, chunk uploads, cloud adapters, 20+ UI variants, i18n, a11y, SSR-safe.
Downloads
435
Maintainers
Readme
react-upload-pro
The most feature-rich React file upload & dropzone library.
Drag & drop · chunk uploads · cloud adapters · 21+ UI variants · i18n in 23 locales · a11y · SSR-safe · TypeScript-first.
Table of contents
- Why react-upload-pro?
- Install
- 30-second example
- Step-by-step setup
- Tailwind setup
- Core usage
- Pre-built variants
- Hooks-first usage
- Cloud adapters
- Validation
- Internationalization (i18n)
- Theming
- Upload modes & strategies
- Props reference
- TypeScript
- SSR / Next.js notes
- Performance
- Contributing / development
- License
Why react-upload-pro?
react-dropzone stops at "drop files, get a callback". react-upload-pro ships everything you need to ship a real upload feature:
- ✅ Inputs — drag/drop, click, paste from clipboard, folder upload (recursive)
- ✅ Upload engine —
instant/manual/auto/queuemodes, parallel + sequential, chunked, pause / resume / retry / cancel - ✅ Cloud adapters — AWS S3 (presigned), Cloudinary, Firebase Storage, Supabase, DigitalOcean Spaces, Azure Blob, GCS
- ✅ Validation — MIME, extension, size (min/max), max files, duplicates, magic-number signatures, custom async validators
- ✅ Preview — image / video / audio / PDF / text / Office (icon) with zoom & rotate
- ✅ Progress — bar / circle / striped, per-file + aggregate, EWMA speed + ETA
- ✅ 21+ UI variants across Minimal / Business / Creative / Enterprise / Layouts
- ✅ a11y — ARIA roles, keyboard nav, focus rings, RTL
- ✅ i18n — 23 built-in locales (en, hi, gu, fr, de, ar, zh, es, pt, ru, ja, ko, it, tr, id, bn, ur, nl, pl, vi, th, he, fa) + custom messages
- ✅ Theming — light / dark / auto with CSS variables and a Tailwind preset
- ✅ Framework agnostic — works with Axios / Fetch / GraphQL / REST via custom
getUploadToken - ✅ Tree-shakeable — core ships without
framer-motionor any cloud SDK
Install
# npm
npm install react-upload-pro
# yarn
yarn add react-upload-pro
# pnpm
pnpm add react-upload-pro
# bun
bun add react-upload-proPeer deps: react >= 17, react-dom >= 17. framer-motion is optional (only some animated variants need it).
30-second example
import { Dropzone, ThemeProvider } from "react-upload-pro";
import "react-upload-pro/styles.css";
export default function App() {
return (
<ThemeProvider defaultTheme="auto">
<Dropzone
endpoint="/api/upload"
accept="image/*,application/pdf"
maxSize={10 * 1024 * 1024} // 10 MB
maxFiles={20}
mode="auto"
retries={3}
onUploadSuccess={(file) => console.log("done", file)}
onUploadError={(file, err) => console.error(err)}
/>
</ThemeProvider>
);
}That's it. The component handles drag/drop, validation, progress bars, retries, previews, and gallery rendering.
Import the styles
You must import the bundled stylesheet exactly once — otherwise the dropzone renders unstyled (no dashed border, no accent color, no layout).
// Vite, CRA, Remix, Astro — anywhere with a global entry file
import "react-upload-pro/styles.css";// Next.js App Router
// app/layout.tsx
import "react-upload-pro/styles.css";// Next.js Pages Router
// pages/_app.tsx
import "react-upload-pro/styles.css";The stylesheet is self-contained — it ships every utility class the components use, so you do not need Tailwind, PostCSS, or any extra build step in your app to get the demo look. If you also use Tailwind in your project, see Tailwind setup below to share design tokens.
Full sample with comments
A complete, copy-paste-ready example. Every prop is annotated so you can see exactly what each piece does — delete the bits you don't need.
// src/components/MyUploader.tsx
// 1️⃣ Core component + a couple of types we'll use in the callbacks.
import {
Dropzone,
ThemeProvider,
I18nProvider,
type UploadFile,
type ValidationError,
} from "react-upload-pro";
// 2️⃣ Bundled stylesheet. Import it once at the top level of your app
// (e.g. main.tsx / layout.tsx) — including it here works too.
import "react-upload-pro/styles.css";
import { useState } from "react";
export default function MyUploader() {
// Track files that the dropzone rejects (wrong type, too big, etc.) so we
// can show them in a toast or modal. Optional — leave it out if you don't
// need rejection UI.
const [rejected, setRejected] = useState<ValidationError[]>([]);
return (
// ThemeProvider gives you light / dark / auto. Wrap once near the root.
<ThemeProvider defaultTheme="auto">
{/* I18nProvider translates every internal string. 23 locales built in. */}
<I18nProvider locale="en">
<Dropzone
// ───── Where the files go ─────
// Pass `endpoint` for a standard multipart POST, OR pass `cloud`
// (S3 / Cloudinary / Firebase / …) for direct-to-bucket uploads.
endpoint="/api/upload"
// ───── Validation ─────
accept="image/*,application/pdf" // MIME globs or extensions
maxSize={10 * 1024 * 1024} // 10 MB per file
minSize={1024} // 1 KB per file
maxFiles={5} // total file cap
multiple // allow picking many at once
rejectDuplicates // skip identical files
// ───── Upload behaviour ─────
mode="auto" // 'manual' | 'instant' | 'auto' | 'queue'
strategy="parallel" // 'parallel' | 'sequential'
concurrency={3} // parallel upload slots
retries={2} // retry attempts per file on network errors
chunkSize={5 * 1024 * 1024} // 5 MB chunks (omit for single-shot)
// ───── Built-in UI extras ─────
previewable // eye icon → fullscreen preview
editable // pencil icon → rename + tag
clipboard // allow ⌘V / Ctrl+V paste
scrollAfter={5} // gallery scrolls after 5 files
maxHeight="320px" // height of the scroll region
// ───── Theming ─────
// `accent` drives borders, focus rings, progress fill, primary
// buttons, and hover states. Accepts hex (`'#10b981'`), an RGB
// triplet (`'16 185 129'`), or any CSS color. Scoped to THIS
// dropzone — multiple instances on one page can have different
// accents without leaking globally.
accent="#10b981"
accentFg="#ffffff" // optional — auto-derived from accent when omitted
// ───── Copy ─────
label="Drop your files here"
hint="PNG, JPG, or PDF — up to 10 MB each, max 5 files"
// ───── Events ─────
onDrop={(accepted, rejected) =>
console.log("dropped", { accepted, rejected })
}
onDropRejected={setRejected} // collect validation errors for UI
onUploadStart={(file: UploadFile) =>
console.log("uploading", file.name)
}
onUploadProgress={(file, progress) =>
console.log(`${file.name}: ${progress.percent}%`)
}
onUploadSuccess={(file) =>
console.log("✅ done →", file.url)
}
onUploadError={(file, err) =>
console.error("❌", file.name, err.message)
}
/>
{/* Toast/banner for rejected files. Replace with your own UI. */}
{rejected.length > 0 && (
<ul style={{ color: "crimson", marginTop: 12 }}>
{rejected.map((e, i) => (
<li key={i}>
{e.file?.name ?? "file"} — {e.message}
</li>
))}
</ul>
)}
</I18nProvider>
</ThemeProvider>
);
}What this gives you out of the box:
- 🖱️ Drag-drop, click-to-browse, paste from clipboard
- 🧪 Live validation with friendly error messages
- 📊 Per-file progress bars + speed + ETA
- 🔁 Automatic retries with exponential backoff
- 👁️ Fullscreen preview for images, PDFs, video, audio
- ✏️ Inline rename + metadata edit modal
- 🌗 Light / dark / auto themes with a single prop
- 🌐 23-locale i18n with RTL support
Step-by-step setup
1. Vite + React
# 1. Create a Vite app
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
# 2. Install the package
npm install react-upload-pro// src/App.tsx
import { Dropzone } from "react-upload-pro";
import "react-upload-pro/styles.css";
export default function App() {
return (
<div style={{ padding: 24 }}>
<Dropzone endpoint="/api/upload" maxSize={5 * 1024 * 1024} />
</div>
);
}npm run dev # → http://localhost:51732. Next.js (App Router)
npx create-next-app@latest my-app --typescript --app
cd my-app
npm install react-upload-pro// app/upload/page.tsx
"use client";
import { Dropzone, ThemeProvider } from "react-upload-pro";
import "react-upload-pro/styles.css";
export default function UploadPage() {
return (
<ThemeProvider defaultTheme="auto">
<Dropzone
endpoint="/api/upload"
accept="image/*"
maxSize={10 * 1024 * 1024}
/>
</ThemeProvider>
);
}// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get("file") as File;
// ...save to disk / S3 / Cloudinary / etc.
return NextResponse.json({ url: "/uploads/" + file.name });
}Why
'use client'? The component uses browser APIs (drag events,File,FileReader). The package emits a"use client"banner, so importing from a server component works — but pages that render it directly need the directive.
3. Create React App
npx create-react-app my-app --template typescript
cd my-app
npm install react-upload-proSame src/App.tsx as the Vite example — CRA just works.
Tailwind setup (optional)
You only need this section if your own app uses Tailwind and you want to
re-use react-upload-pro's design tokens (text-rup-accent, bg-rup-bg,
etc.) in your own components.
Not using Tailwind? Stop here —
import "react-upload-pro/styles.css"is already enough. The bundled stylesheet ships every utility the components need.
Plug in the preset:
// tailwind.config.js
module.exports = {
presets: [require("react-upload-pro/tailwind")],
content: [
"./src/**/*.{ts,tsx,js,jsx}",
"./node_modules/react-upload-pro/dist/**/*.{js,cjs}",
],
};Then anywhere in your app:
<button className="bg-rup-accent text-rup-accent-fg">Custom button</button>The accent color follows the same --rup-accent variable the dropzone uses,
so picking a new accent updates your custom UI too.
Core usage
Minimum
<Dropzone endpoint="/api/upload" />Real-world example
import { Dropzone, type UploadFile } from "react-upload-pro";
import "react-upload-pro/styles.css";
export function ProfilePictureUploader() {
return (
<Dropzone
endpoint="/api/avatar"
accept="image/*"
maxSize={2 * 1024 * 1024} // 2 MB
maxFiles={1}
multiple={false}
mode="instant" // upload immediately on drop
previewable // eye icon → fullscreen preview
editable // pencil icon → rename + tag
retries={2}
onUploadStart={(f: UploadFile) => console.log("uploading", f.name)}
onUploadSuccess={(f) => console.log("done", f.url)}
onUploadError={(f, e) => alert(e.message)}
/>
);
}Custom label / hint
<Dropzone
endpoint="/api/upload"
label="Drop your resume here"
hint="PDF or DOCX, up to 5 MB"
/>Manual control with the render prop
<Dropzone endpoint="/api/upload" maxSize={5e6}>
{({ getRootProps, getInputProps, files, start, isUploading }) => (
<div>
<div
{...getRootProps()}
className="border-2 border-dashed p-8 rounded-lg"
>
<input {...getInputProps()} />
Drop files or click
</div>
<p>{files.length} queued</p>
<button onClick={() => start()} disabled={isUploading}>
Upload
</button>
</div>
)}
</Dropzone>Pre-built variants
21+ designs grouped into 5 categories. Every variant accepts the same options as Dropzone, so any feature works on any look.
import {
MinimalGlass,
BusinessCRM,
EnterpriseDocs,
LayoutModal,
} from "react-upload-pro/variants";
<MinimalGlass endpoint="/api/upload" accent="#6366f1" />;| Category | Variants |
| -------------- | ---------------------------------------------------------------------------------------- |
| Minimal | MinimalModern, MinimalGlass, MinimalNeumorphic, MinimalMaterial, MinimalInline |
| Business | BusinessCRM, BusinessDashboard, BusinessSaaS |
| Creative | CreativeGradient, CreativeAnimated, CreativePremium, CreativeAvatar |
| Enterprise | EnterpriseDocs, EnterpriseTeam, EnterpriseMediaLibrary, EnterpriseFullscreen |
| Layouts | LayoutBox, LayoutCard, LayoutSidebar, LayoutModal, LayoutFloating |
Try them all live in the playground: npm run dev after cloning.
Hooks-first usage
For full control — no built-in UI, no gallery, just upload state and helpers — use useDropzone:
import { useDropzone, UploadGallery } from "react-upload-pro";
import "react-upload-pro/styles.css";
function MyUploader() {
const {
getRootProps,
getInputProps,
files,
isUploading,
isDragActive,
start,
pause,
resume,
remove,
retry,
clear,
} = useDropzone({
endpoint: "/api/upload",
accept: { "image/*": [".png", ".jpg", ".webp"] },
maxSize: 5 * 1024 * 1024,
mode: "manual", // wait for explicit start()
chunkSize: 5 * 1024 * 1024, // 5 MB chunks
strategy: "parallel",
concurrency: 3,
});
return (
<div>
<div
{...getRootProps()}
className={`border-2 border-dashed p-8 rounded-lg ${
isDragActive ? "border-blue-500 bg-blue-50" : "border-gray-300"
}`}
>
<input {...getInputProps()} />
{isDragActive ? "Drop here…" : "Drop files or click"}
</div>
<UploadGallery
files={files}
onRemove={(f) => remove(f.id)}
onRetry={(f) => retry(f.id)}
/>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => start()} disabled={isUploading}>
Upload
</button>
<button onClick={pause}>Pause</button>
<button onClick={resume}>Resume</button>
<button onClick={clear}>Clear</button>
</div>
</div>
);
}Cloud adapters
Upload directly to your cloud bucket without proxying through your server. Credentials never leave your backend — the adapter just consumes presigned URLs / signed tokens / SAS.
AWS S3
import { useDropzone } from "react-upload-pro";
import { createS3Adapter } from "react-upload-pro/cloud";
const s3 = createS3Adapter({
getPresignedUrl: async (file) => {
const res = await fetch("/api/s3/presign", {
method: "POST",
body: JSON.stringify({ name: file.name, type: file.type }),
});
return res.json(); // { url, method: 'PUT', headers? }
},
});
useDropzone({ cloud: s3, mode: "auto" });// /api/s3/presign (Next.js Route Handler example)
import { S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: process.env.AWS_REGION });
export async function POST(req: Request) {
const { name, type } = await req.json();
const cmd = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: name,
ContentType: type,
});
const url = await getSignedUrl(s3, cmd, { expiresIn: 60 });
return Response.json({ url, method: "PUT" });
}Other adapters
import {
createCloudinaryAdapter,
createFirebaseStorageAdapter,
createSupabaseAdapter,
createDigitalOceanAdapter,
createAzureBlobAdapter,
createGcsAdapter,
} from "react-upload-pro/cloud";Every adapter has the same shape — pass it to cloud: on Dropzone or useDropzone.
Validation
useDropzone({
accept: { "image/*": [".png", ".jpg"] },
minSize: 1024, // 1 KB
maxSize: 5 * 1024 * 1024, // 5 MB
maxFiles: 10,
rejectDuplicates: true, // same name + size + lastModified
validators: [
async (file) =>
file.name.includes(" ")
? { code: "custom", message: "No spaces in filename" }
: null,
],
});For security-sensitive flows, validate the real magic number instead of trusting the MIME the browser reports:
import { detectSignature } from "react-upload-pro";
const actual = await detectSignature(file); // → 'image/png' | 'application/pdf' | ...
if (!actual) throw new Error("unknown file type");Showing rejection errors as a modal
import {
Dropzone,
ValidationErrorsModal,
type ValidationError,
} from "react-upload-pro";
import { useState } from "react";
function App() {
const [errors, setErrors] = useState<ValidationError[]>([]);
return (
<>
<Dropzone endpoint="/api/upload" onDropRejected={setErrors} />
<ValidationErrorsModal
open={errors.length > 0}
errors={errors}
onClose={() => setErrors([])}
/>
</>
);
}Internationalization (i18n)
23 built-in locales — wrap with I18nProvider and the dropzone UI translates automatically:
import { I18nProvider, Dropzone } from "react-upload-pro";
<I18nProvider locale="ja">
<Dropzone endpoint="/api/upload" />
</I18nProvider>;Supported locales
| Code | Language | RTL |
| ---- | ---------------- | --- |
| en | English | |
| es | Español | |
| fr | Français | |
| de | Deutsch | |
| it | Italiano | |
| pt | Português | |
| nl | Nederlands | |
| pl | Polski | |
| ru | Русский | |
| tr | Türkçe | |
| zh | 中文 | |
| ja | 日本語 | |
| ko | 한국어 | |
| vi | Tiếng Việt | |
| th | ไทย | |
| id | Bahasa Indonesia | |
| hi | हिन्दी | |
| gu | ગુજરાતી | |
| bn | বাংলা | |
| ar | العربية | ✓ |
| ur | اردو | ✓ |
| he | עברית | ✓ |
| fa | فارسی | ✓ |
Custom messages
Override any string per-locale:
<I18nProvider
locale="en"
messages={{ dropHere: "Drop your resume here", browse: "Pick a PDF" }}
>
<Dropzone endpoint="/api/upload" />
</I18nProvider>Check the RTL set programmatically
import { rtlLocales } from "react-upload-pro";
const isRtl = rtlLocales.has(currentLocale);Theming
import { ThemeProvider, useTheme } from "react-upload-pro";
<ThemeProvider defaultTheme="auto">
{" "}
{/* 'light' | 'dark' | 'auto' */}
<Dropzone endpoint="/api/upload" />
</ThemeProvider>;Custom accent color
{/* Hex string — most common */}
<Dropzone endpoint="/api/upload" accent="#10b981" />
{/* RGB triplet (the format CSS variables use internally) */}
<Dropzone endpoint="/api/upload" accent="16 185 129" />
{/* Any CSS color the browser understands */}
<Dropzone endpoint="/api/upload" accent="rebeccapurple" />
{/* With a custom foreground (button text on accent background).
Auto-derived from luminance when omitted. */}
<Dropzone endpoint="/api/upload" accent="#facc15" accentFg="#000000" />Each accent is scoped to that dropzone instance — the value is applied
as an inline --rup-accent CSS variable on the component's outer wrapper,
so multiple dropzones on the same page can each have their own accent
without leaking globally.
Need the accent to apply page-wide (e.g. share it with your own buttons)? Set it as a CSS variable on any ancestor:
:root {
--rup-accent: 16 185 129; /* RGB triplet, no rgb() wrapper */
--rup-accent-fg: 255 255 255;
}Every variant also accepts the same prop:
import { MinimalGlass, BusinessCRM } from "react-upload-pro/variants";
<MinimalGlass accent="#6366f1" endpoint="/api/upload" />
<BusinessCRM accent="#10b981" endpoint="/api/upload" />The accent drives borders, buttons, progress fill, focus rings, hover states, and scrollbars.
Upload modes & strategies
| Option | Values | Default | Description |
| ---------------- | -------------------------------------------------------------- | ------------ | ------------------------- |
| mode | 'manual' | 'instant' | 'auto' | 'queue' | 'manual' | When to start uploading |
| strategy | 'parallel' | 'sequential' | 'parallel' | How to dispatch the queue |
| concurrency | number | 3 | Parallel upload slots |
| retries | number | 2 | Retry attempts per file |
| retryBackoffMs | number | 500 | Doubles each retry |
| chunkSize | number (bytes) | unset | Single-shot if unset |
Mode cheat sheet
manual— files queue up; user clicks an "Upload" button to startinstant— each file starts uploading the moment it's droppedauto— same asinstant, but adds a small debounce so multi-drop bursts batch nicelyqueue— strictly one at a time, in drop order, ignoringconcurrency
Props reference
<Dropzone> (most common)
| Prop | Type | Notes |
| ------------------ | -------------------------------------------- | ---------------------------------------------------------------- |
| endpoint | string | URL for the multipart POST. Use cloud: for direct-to-S3 etc. |
| cloud | CloudAdapter | Direct cloud upload (mutually exclusive with endpoint) |
| accept | string | Accept | "image/*", ".pdf,.docx", or { 'image/*': ['.png'] } |
| maxSize | number | Bytes |
| minSize | number | Bytes |
| maxFiles | number | |
| multiple | boolean | Default true |
| directory | boolean | Folder upload (recursive) |
| clipboard | boolean | Paste from clipboard. Default true |
| rejectDuplicates | boolean | Default false |
| disabled | boolean | |
| mode | 'manual' \| 'instant' \| 'auto' \| 'queue' | Default 'manual' |
| strategy | 'parallel' \| 'sequential' | Default 'parallel' |
| concurrency | number | Default 3 |
| retries | number | Default 2 |
| chunkSize | number | Bytes. Unset = single-shot upload |
| label | ReactNode | Replaces the default heading |
| hint | ReactNode | Small descriptive line under the label |
| previewable | boolean | Eye icon → fullscreen preview |
| editable | boolean | Pencil icon → rename + tag + describe |
| scrollAfter | number | List becomes scrollable above this count |
| maxHeight | string | CSS height of the scrollable region |
| width / height | string | Outer container CSS sizing |
| accent | string | Hex / RGB triplet / CSS color. Scoped to this instance. |
| accentFg | string | Foreground on accent surfaces. Auto-derived from accent. |
| onDrop | (accepted, rejected) => void | |
| onDropRejected | (errors) => void | |
| onUploadStart | (file) => void | |
| onUploadProgress | (file, progress) => void | |
| onUploadSuccess | (file) => void | |
| onUploadError | (file, error) => void | |
Full API surface
- Components —
Dropzone,UploadArea,UploadButton,UploadProgress,UploadPreview,UploadGallery,UploadModal,FilePreviewModal,FileEditModal,ValidationErrorsModal - Hooks —
useDropzone,useUploader,useUploadQueue,useUploadProgress,useFilePreview - Providers —
ThemeProvider,I18nProvider - Core —
UploadQueue,validateFile,validateBatch,matchesAccept - Cloud (subpath) —
createS3Adapter,createCloudinaryAdapter,createFirebaseStorageAdapter,createSupabaseAdapter,createDigitalOceanAdapter,createAzureBlobAdapter,createGcsAdapter - Variants (subpath) — 21 named exports (see Pre-built variants)
- Utilities —
formatBytes,formatSpeed,formatEta,formatPercent,getFileCategory,detectSignature,wrapFile,generatePreview,revokePreview,cn - i18n exports —
translations,rtlLocales, typeLocale, typeTranslations
See docs/API.md for the full reference (when published).
TypeScript
Fully typed — every public API has .d.ts. The package exports both ESM and CJS with separate type declarations so it works in any modern bundler.
import type {
DropzoneOptions,
DropzoneState,
UploadFile,
UploadStatus,
ValidationError,
CloudAdapter,
Locale,
Theme,
} from "react-upload-pro";SSR / Next.js notes
- Every public surface is SSR-safe — DOM APIs are only touched inside
useEffect. - The package emits a
"use client"directive, so importing from a server component works transparently. Pages that renderDropzonedirectly still need'use client'at the top. - The base CSS (
react-upload-pro/styles.css) is plain CSS — import it once in your root layout.
// app/layout.tsx
import "react-upload-pro/styles.css";Performance
- Default render path is O(n) on file count.
- For galleries beyond a few hundred files, use
scrollAfter={N}to cap the rendered region. For thousands of files, virtualize with your own list library — the gallery layout is intentionally pluggable. previewUrlis lazily generated and automatically revoked on remove / unmount, so no object-URL leaks.- Core is tree-shakeable.
framer-motionand cloud SDKs are not imported until you opt in.
Contributing / development
git clone https://github.com/yogeshgabani/react-upload-pro.git
cd react-upload-pro
npm install
# 🎮 Interactive playground — every variant, every option, live code preview
npm run dev # → http://localhost:5173
# Other scripts
npm run dev:lib # tsup --watch — rebuild library on every save
npm run storybook # individual component stories on :6006
npm test # vitest unit + component
npm run test:e2e # playwright smoke
npm run build # tsup → dist/ (ESM + CJS + .d.ts)
npm run typecheck # tsc --noEmit
npm run lint # eslintThe playground at npm run dev is the fastest way to explore the API — every prop is wired to a UI control and the generated code updates live as you tweak.
PRs welcome — please:
- Open an issue first for non-trivial changes
- Add a test for new behavior
- Run
npm run typecheck && npm run lint && npm testbefore pushing
Changelog
Every release is documented in CHANGELOG.md — features
added, bugs fixed, breaking changes called out. The live playground also has
a "Version history" panel under the variant gallery so you can review the
release timeline right from the demo.
License
MIT © Yogesh Gabani MIT License - Copyright (c) 2026 Yogesh Gabani
Built by Yogesh Gabani.
