diff --git a/packages/react/src/__snapshots__/golden-template.test.tsx.snap b/packages/react/src/__snapshots__/golden-template.test.tsx.snap
index ca192c0..306834e 100644
--- a/packages/react/src/__snapshots__/golden-template.test.tsx.snap
+++ b/packages/react/src/__snapshots__/golden-template.test.tsx.snap
@@ -46,7 +46,7 @@ exports[`Golden Template: Marketing Email > snapshot locks the full email HTML 1
-
+
@@ -183,7 +183,7 @@ exports[`Golden Template: Marketing Email > snapshot locks the full email HTML 1
-
+
@@ -346,7 +346,7 @@ exports[`Golden Template: Marketing Email > snapshot locks the full email HTML 1
-
+
@@ -408,7 +408,7 @@ exports[`Golden Template: Marketing Email > snapshot locks the full email HTML 1
-
+
diff --git a/packages/react/src/components/Button.tsx b/packages/react/src/components/Button.tsx
index 70cdfd6..7a825ed 100644
--- a/packages/react/src/components/Button.tsx
+++ b/packages/react/src/components/Button.tsx
@@ -44,7 +44,8 @@ const DEFAULT_VALUES = {
*
* @example Flat Props (Simple - most common)
* ```tsx
- *
+ * // `href` accepts a plain URL string (the ergonomic form).
+ *
* Click me
*
* ```
diff --git a/packages/react/src/components/Html.stories.test.tsx b/packages/react/src/components/Html.stories.test.tsx
new file mode 100644
index 0000000..2abe199
--- /dev/null
+++ b/packages/react/src/components/Html.stories.test.tsx
@@ -0,0 +1,32 @@
+import { describe, it, expect } from "vitest";
+import * as stories from "./Html.stories";
+
+// The component is a raw passthrough (no default sanitizer), so its story
+// HTML must itself be valid and safe. Two things have bitten us:
+// - inline event handlers (onmouseover, …) — never run in a rendered email and
+// are an XSS pattern; bad examples to ship.
+// - a `url('…')` whose content has a raw `"` (e.g. an SVG data URI with
+// xmlns="…") sits inside a double-quoted style="…", so the first inner `"`
+// closes the attribute and the tail (`'); opacity: …">`) leaks as visible
+// text. URL-encode the data URI or avoid raw double quotes.
+const htmlStories = Object.entries(stories).filter(
+ ([, s]) => s && typeof s === "object" && (s as any).args && typeof (s as any).args.html === "string"
+) as [string, { args: { html: string } }][];
+
+describe("Html stories ship valid, safe HTML", () => {
+ it("there are several html stories to check", () => {
+ expect(htmlStories.length).toBeGreaterThan(3);
+ });
+
+ for (const [name, story] of htmlStories) {
+ const html = story.args.html;
+
+ it(`${name}: has no inline event handlers`, () => {
+ expect(html).not.toMatch(/\son[a-z]+\s*=/i);
+ });
+
+ it(`${name}: has no url() containing a raw double quote (style break-out)`, () => {
+ expect(html).not.toMatch(/url\(\s*'[^']*"[^']*'\s*\)/);
+ });
+ }
+});
diff --git a/packages/react/src/components/Html.stories.tsx b/packages/react/src/components/Html.stories.tsx
index 992173a..dee6409 100644
--- a/packages/react/src/components/Html.stories.tsx
+++ b/packages/react/src/components/Html.stories.tsx
@@ -88,7 +88,7 @@ export const StyledCard: Story = {
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
- " onmouseover="this.style.background='rgba(255,255,255,0.3)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
+ ">
Learn More
@@ -172,8 +172,9 @@ export const CallToAction: Story = {
left: 0;
right: 0;
bottom: 0;
- background: url('data:image/svg+xml, ');
- opacity: 0.1;
+ background-image: radial-gradient(rgba(255, 255, 255, 0.18) 1px, transparent 1px);
+ background-size: 18px 18px;
+ opacity: 0.5;
">
@@ -213,7 +214,7 @@ export const CallToAction: Story = {
cursor: pointer;
transition: transform 0.2s ease;
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.4);
- " onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
+ ">
Start Free Trial
@@ -227,7 +228,7 @@ export const CallToAction: Story = {
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
- " onmouseover="this.style.borderColor='rgba(255,255,255,0.6)'; this.style.background='rgba(255,255,255,0.1)'" onmouseout="this.style.borderColor='rgba(255,255,255,0.3)'; this.style.background='transparent'">
+ ">
Learn More
diff --git a/packages/react/src/components/Html.tsx b/packages/react/src/components/Html.tsx
index 854d335..46e4bcc 100644
--- a/packages/react/src/components/Html.tsx
+++ b/packages/react/src/components/Html.tsx
@@ -13,6 +13,13 @@ const DEFAULT_VALUES = {
/**
* Html - Universal SSR/Client Component for custom HTML with Automatic Semantic Props
*
+ * ⚠️ Renders the HTML verbatim — it is NOT sanitized by default. Only pass HTML
+ * you trust, and make sure it is valid: notably, an inline SVG inside a `url(...)`
+ * must be URL-encoded, because a raw `"` inside a double-quoted `style="…"`
+ * closes the attribute and the rest leaks out as text. To sanitize (matching the
+ * editor's HTML block, which strips scripts/event handlers), pass a `toSafeHtml`
+ * function via the `UnlayerProvider` config.
+ *
* @example Flat Props
* ```tsx
*