diff --git a/docs/concepts/compositions.mdx b/docs/concepts/compositions.mdx
index b74c75a20..5c9dbfa26 100644
--- a/docs/concepts/compositions.mdx
+++ b/docs/concepts/compositions.mdx
@@ -127,13 +127,54 @@ Every composition has two layers:
## Variables
-Compositions can expose variables for dynamic content:
+HyperFrames does not automatically bind `data-var-*` attributes into your composition DOM or CSS.
+
+Today, the supported pattern is:
+
+1. Pass per-instance values on the composition host with `data-variable-values`
+2. Read those values inside the composition and apply them in your own script
+
+```html index.html
+
+
+
+
Fallback
+
+
+
+
+
+
```
-Variables make compositions reusable as [examples](/examples) -- the same composition can render different content by injecting variable values at render time.
+If you are building tooling on top of `@hyperframes/core`, you can also declare variable metadata separately with `data-composition-variables` and read it via `extractCompositionMetadata()`. That metadata is descriptive only; you still apply the actual values manually inside the composition.
## Listing Compositions
diff --git a/docs/concepts/data-attributes.mdx b/docs/concepts/data-attributes.mdx
index 0b7b66e3c..dc1c3fc1f 100644
--- a/docs/concepts/data-attributes.mdx
+++ b/docs/concepts/data-attributes.mdx
@@ -29,6 +29,7 @@ Hyperframes uses HTML data attributes to control timing, media playback, and [co
| `data-width` | `"1920"` | Composition width in pixels |
| `data-height` | `"1080"` | Composition height in pixels |
| `data-composition-src` | `"./intro.html"` | Path to external [composition](/concepts/compositions) HTML file |
+| `data-variable-values` | `'{"title":"Hello"}'` | JSON object of values passed to a nested composition. HyperFrames carries these values through, but your composition script must read and apply them manually. |
## Element Visibility
diff --git a/docs/packages/core.mdx b/docs/packages/core.mdx
index 896d89987..b7dbd543a 100644
--- a/docs/packages/core.mdx
+++ b/docs/packages/core.mdx
@@ -122,6 +122,13 @@ const parsed: ParsedHtml = parseHtml(htmlString);
import { extractCompositionMetadata } from '@hyperframes/core';
const meta: CompositionMetadata = extractCompositionMetadata(htmlString);
// meta.id, meta.duration, meta.width, meta.height, meta.variables
+//
+// Variable metadata is declared on the document root, for example:
+//
// Generate HTML from structured data
const html = generateHyperframesHtml(elements, {
diff --git a/docs/reference/html-schema.mdx b/docs/reference/html-schema.mdx
index dd8877867..498a8821c 100644
--- a/docs/reference/html-schema.mdx
+++ b/docs/reference/html-schema.mdx
@@ -58,6 +58,7 @@ Common sizes:
| `data-volume` | audio, video | No | Volume level from `0` to `1`. Default: `1`. |
| `data-composition-id` | div | On compositions | Unique composition ID. Must match the key used in `window.__timelines`. |
| `data-composition-src` | div | No | Path to external composition HTML file (for [nested compositions](#composition-clips)). |
+| `data-variable-values` | div | No | JSON object of values passed to a nested composition. The framework carries the values through, but your composition script must read and apply them manually. |
| `data-width` | div | On compositions | Composition width in pixels. |
| `data-height` | div | On compositions | Composition height in pixels. |
@@ -151,6 +152,7 @@ Common sizes:
- Each nested composition has its own `window.__timelines` entry, registered by its own `