diff --git a/site/src/docs/components/toast.mdx b/site/src/docs/components/toast.mdx
index 6a94839..b9e8bb6 100644
--- a/site/src/docs/components/toast.mdx
+++ b/site/src/docs/components/toast.mdx
@@ -164,6 +164,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
### Config object
+
+ `title`, `description`, and action labels are inserted as plain text. With `html: true` they are inserted as HTML, so never pass untrusted user input with it. `onclick` is JavaScript code and must never contain user input. `icon` is always inserted as raw HTML (it is meant for SVG markup).
+
+
durationOptional
Timeout in milliseconds. Defaults to 3000, or 5000 for error toasts. Use -1 to keep it open.
@@ -177,4 +181,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
Primary action. Supports label with either href or onclick.
cancelOptional
Secondary action. Supports label and optional onclick.
+
iconOptional
+
Custom icon markup, usually an SVG. Inserted as raw HTML. Defaults to the category icon.
+
htmlOptional
+
Set to true to insert title, description, and action labels as HTML instead of plain text. Defaults to false. Only use it with trusted content.
+
+The Jinja and Nunjucks `toast()` macros render on your server, so your template engine's autoescaping applies to `title`, `description`, and labels there when it is enabled. Nunjucks enables it by default. With Jinja, make sure your environment's `autoescape` setting covers the Basecoat template files (for example `.html.jinja`). `icon` is output with `safe`, and `onclick` is output as an attribute, so neither should contain user input.
diff --git a/src/js/toast.js b/src/js/toast.js
index 10c0959..88cdf58 100644
--- a/src/js/toast.js
+++ b/src/js/toast.js
@@ -131,6 +131,13 @@
element.addEventListener('transitionend', () => element.remove(), { once: true });
}
+ const escapeHtml = (value) => String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+
function createToast(config) {
const {
category = 'info',
@@ -140,18 +147,22 @@
cancel,
duration,
icon,
+ html: allowHtml = false,
} = config;
+ // Text is escaped unless `html: true`. `icon` is always raw markup (SVG).
+ const text = (value) => (allowHtml ? String(value) : escapeHtml(value));
+
const iconHtml = icon || (category && ICONS[category]) || '';
- const titleHtml = title ? `