diff --git a/packages/core/src/figma/nodeToHtml.test.ts b/packages/core/src/figma/nodeToHtml.test.ts
index 7f70a61dc..367311474 100644
--- a/packages/core/src/figma/nodeToHtml.test.ts
+++ b/packages/core/src/figma/nodeToHtml.test.ts
@@ -84,6 +84,24 @@ describe("nodeToHtml", () => {
expect(out.html).not.toContain("left: 520px");
});
+ it("prefixes digit-leading slugs so ids stay CSS-selector-safe", () => {
+ const out = nodeToHtml(
+ frame([
+ {
+ id: "1:2",
+ name: "3D Object - Headphones",
+ type: "RECTANGLE",
+ absoluteBoundingBox: BOX(120, 220, 100, 40),
+ fills: [SOLID_BLUE],
+ },
+ ]),
+ { resolved: [], unresolved: [] },
+ );
+ // "#3d-object-headphones" would throw in querySelector/GSAP targeting
+ expect(out.html).toContain('id="n3d-object-headphones"');
+ expect(out.html).not.toContain('id="3d-object-headphones"');
+ });
+
it("emits var() with literal fallback for resolved bindings", () => {
const out = nodeToHtml(
frame([
diff --git a/packages/core/src/figma/nodeToHtml.ts b/packages/core/src/figma/nodeToHtml.ts
index 6ce0c81d8..393af42c4 100644
--- a/packages/core/src/figma/nodeToHtml.ts
+++ b/packages/core/src/figma/nodeToHtml.ts
@@ -162,7 +162,12 @@ interface RenderContext {
}
function uniqueSlug(ctx: RenderContext, name: string): string {
- const base = slugify(name);
+ const raw = slugify(name);
+ // A digit-leading id ("3D Object" → "3d-object") is valid HTML but not a
+ // valid CSS selector — querySelector("#3d-object") throws, which breaks
+ // GSAP targeting and figma-motion translation. Prefix so ids stay
+ // selector-safe.
+ const base = /^[0-9]/.test(raw) ? `n${raw}` : raw;
let slug = base;
let n = 2;
while (ctx.usedSlugs.has(slug)) slug = `${base}-${n++}`;