diff --git a/crates/engine/src/dev/codegen/html.rs b/crates/engine/src/dev/codegen/html.rs
index d1b144cae..ac23e6a5b 100644
--- a/crates/engine/src/dev/codegen/html.rs
+++ b/crates/engine/src/dev/codegen/html.rs
@@ -90,7 +90,10 @@ pub(super) fn html(doc: &Document, id: NodeId) -> String {
let ctx = Ctx::new(doc, id);
let mut out = Markup::new(&ctx, CodeLang::Html);
out.node(id, None, 0, "");
- format!("{}\n\n", out.markup, out.tokens.root_rule(), out.styles)
+ // Document text can reach the stylesheet through any property: `<\/` reads back as `` inside a CSS
+ // string and can never close the element early.
+ let css = format!("{}{}", out.tokens.root_rule(), out.styles).replace("", "<\\/");
+ format!("{}\n\n", out.markup)
}
/// Markup being built for a layer tree: HTML (`class`), React JSX (`className`), Vue
diff --git a/crates/engine/src/html_codegen_tests.rs b/crates/engine/src/html_codegen_tests.rs
index cd1318ee0..ab7115554 100644
--- a/crates/engine/src/html_codegen_tests.rs
+++ b/crates/engine/src/html_codegen_tests.rs
@@ -163,3 +163,16 @@ fn rem_preserves_text_property_default() {
}
}
}
+
+#[test]
+fn the_stylesheet_never_contains_a_closing_style_tag_from_document_text() {
+ let mut s = Session::new();
+ let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap());
+ let t = id(s.execute("text.create", &json!({"text": "x", "x": 0, "y": 0})).unwrap());
+ s.execute("node.set", &json!({"id": t, "props": {"fontFamily": "a"}})).unwrap();
+ s.execute("node.reparent", &json!({"ids": [t], "parent": f})).unwrap();
+ let code = code_for(s.doc().unwrap(), f, CodeLang::Html);
+ let (_, css) = code.split_once("").count(), 1, "{code}");
+ assert!(!code.contains("