diff --git a/.github/badges/core-size.json b/.github/badges/core-size.json
new file mode 100644
index 0000000..54497fb
--- /dev/null
+++ b/.github/badges/core-size.json
@@ -0,0 +1,7 @@
+{
+ "schemaVersion": 1,
+ "label": "bundle size",
+ "message": "385 B gzip",
+ "color": "brightgreen",
+ "cacheSeconds": 3600
+}
diff --git a/README.md b/README.md
index 8214d73..a4f6de9 100644
--- a/README.md
+++ b/README.md
@@ -13,9 +13,15 @@
Zero runtime, zero React re-renders, and a simple developer experience. Say goodbye to context and prop-drilling.
-

-
-[See the proof](/docs/demo.md) [Quick Start](#quick-start) [API Reference](/docs/api-reference.md) [Usage Examples](/docs/usage-examples.md) [Migration Guide](/docs/migration-guide.md) [FAQ](/docs/faq.md) [Contributing](#contributing)
+

+
+[See the proof](/docs/demo.md) |
+[Quick Start](#quick-start) |
+[API Reference](/docs/api-reference.md) |
+[Usage Examples](/docs/usage-examples.md) |
+[Migration Guide](/docs/migration-guide.md) |
+[FAQ](/docs/faq.md) |
+[Contributing](#contributing)
diff --git a/docs/size.md b/docs/size.md
new file mode 100644
index 0000000..ce11131
--- /dev/null
+++ b/docs/size.md
@@ -0,0 +1,29 @@
+# Bundle Size Proof
+
+This repo measures bundle size from the built `@react-zero-ui/core` entry.
+
+## Command
+
+```bash
+pnpm build
+pnpm size
+pnpm size:badge
+```
+
+The `size` script in [`package.json`](../package.json) runs:
+
+```bash
+npx esbuild ./packages/core/dist/index.js --bundle --minify --format=esm --external:react --define:process.env.NODE_ENV='"production"' | gzip -c | wc -c
+```
+
+That produces the gzipped byte count used for the README badge.
+
+## Badge File
+
+Run `pnpm size:badge` after a build when you want to refresh the badge JSON.
+
+That writes:
+
+- [`core-size.json`](../.github/badges/core-size.json)
+
+The README badge reads from that committed file through a Shields endpoint.
diff --git a/package.json b/package.json
index e260490..831b41a 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,8 @@
"prepack:core": "pnpm -F @react-zero-ui/core pack --pack-destination ./dist",
"reset": "git clean -fdx && pnpm install --frozen-lockfile && pnpm prepack:core && pnpm i-tarball",
"size": "npx esbuild ./packages/core/dist/index.js --bundle --minify --format=esm --external:react --define:process.env.NODE_ENV='\"production\"' | gzip -c | wc -c",
+ "size:badge": "node scripts/write-size-badge.mjs",
+ "size:experimental-runtime": "sh -c 'out=$(mktemp); npx esbuild ./packages/core/dist/experimental/runtime.js --bundle --minify --format=esm --external:react --define:process.env.NODE_ENV=\"production\" > \"$out\" && cat \"$out\" && printf \"\\nExperimental runtime size: %s bytes\\n\" \"$(gzip -c \"$out\" | wc -c | tr -d \" \")\"'",
"test": "eslint . && cd packages/core && pnpm test:all && pnpm smoke",
"test:cli": "cd packages/core && pnpm test:cli",
"test:integration": "cd packages/core && pnpm test:integration",
diff --git a/scripts/write-size-badge.mjs b/scripts/write-size-badge.mjs
new file mode 100644
index 0000000..ea6c7fa
--- /dev/null
+++ b/scripts/write-size-badge.mjs
@@ -0,0 +1,34 @@
+import { execSync } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
+
+const outputPath = path.resolve(".github/badges/core-size.json");
+const rawBytes = execSync("pnpm size", { encoding: "utf8" }).trim();
+const match = rawBytes.match(/(\d+)\s*$/);
+const bytes = match ? Number.parseInt(match[1], 10) : Number.NaN;
+
+if (!Number.isFinite(bytes)) {
+ throw new Error(`Expected pnpm size to return an integer byte count, got: ${rawBytes}`);
+}
+
+const formatBytes = (value) => {
+ if (value < 1024) return `${value} B gzip`;
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1).replace(/\\.0$/, "")} kB gzip`;
+ return `${(value / (1024 * 1024)).toFixed(1).replace(/\\.0$/, "")} MB gzip`;
+};
+
+const color = bytes < 512 ? "brightgreen" : bytes < 1024 ? "green" : bytes < 2048 ? "yellowgreen" : "yellow";
+
+const badge = {
+ schemaVersion: 1,
+ label: "bundle size",
+ message: formatBytes(bytes),
+ color,
+ cacheSeconds: 3600,
+};
+
+fs.mkdirSync(path.dirname(outputPath), { recursive: true });
+fs.writeFileSync(outputPath, `${JSON.stringify(badge, null, 2)}\n`);
+
+console.log(`Wrote ${outputPath}`);
+console.log(JSON.stringify(badge));