diff --git a/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.js b/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.js
new file mode 100644
index 00000000000..273ae74aaab
--- /dev/null
+++ b/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.js
@@ -0,0 +1,42 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { _lt } from "@web/core/l10n/translation";
+import { standardFieldProps } from "../standard_field_props";
+const { Component, useEffect, useRef } = owl;
+
+export class IframeWrapperField extends Component {
+ setup() {
+ this.iframeRef = useRef("iframe");
+
+ useEffect(
+ (value) => {
+ /**
+ * The document.write is not recommended. It is better to manipulate the DOM through $.appendChild and
+ * others. In our case though, we deal with an iframe without src attribute and with metadata to put in
+ * head tag. If we use the usual dom methods, the iframe is automatically created with its document
+ * component containing html > head & body. Therefore, if we want to make it work that way, we would
+ * need to receive each piece at a time to append it to this document (with this.record.data and extra
+ * model fields or with an rpc). It also cause other difficulties getting attribute on the most parent
+ * nodes, parsing to HTML complex elements, etc.
+ * Therefore, document.write makes it much more trivial in our situation.
+ */
+ const iframeDoc = this.iframeRef.el.contentDocument;
+ iframeDoc.open();
+ iframeDoc.write(value);
+ iframeDoc.close();
+ },
+ () => [this.props.value]
+ );
+ }
+}
+
+IframeWrapperField.template = "web.IframeWrapperField";
+IframeWrapperField.props = {
+ ...standardFieldProps,
+};
+IframeWrapperField.displayName = _lt("Wrap raw html within an iframe");
+// If HTML, don't forget to adjust the sanitize options to avoid stripping most of the metadata
+IframeWrapperField.supportedTypes = ["text", "html"];
+
+registry.category("fields").add("iframe_wrapper", IframeWrapperField);
diff --git a/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.scss b/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.scss
new file mode 100644
index 00000000000..82d6fef37d5
--- /dev/null
+++ b/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.scss
@@ -0,0 +1,43 @@
+$preview_height: 1123 + 32;
+$preview_width: 794;
+$preview_scale: 0.50;
+
+@mixin o_preview_iframe_styling($scale) {
+
+ .o_preview_iframe_wrapper {
+ padding: 0;
+ overflow: hidden;
+ width: ($preview_width * $scale) + 0px;
+ height: ($preview_height * $scale) + 0px;
+ position: relative;
+ }
+
+ .o_preview_iframe {
+ width: $preview_width + 0px;
+ height: $preview_height + 0px;
+ border: 2px solid lightgrey;
+ overflow: hidden;
+
+ padding-top: 16px;
+ padding-bottom: 16px;
+
+ -ms-zoom: 0.5;
+ -moz-transform: scale($scale);
+ -moz-transform-origin: 0 0;
+ -o-transform: scale($scale);
+ -o-transform-origin: 0 0;
+ -webkit-transform: scale($scale);
+ -webkit-transform-origin: 0 0;
+ }
+}
+
+
+@include o_preview_iframe_styling($preview_scale)
+
+@media (max-width: 1488px) {
+ @include o_preview_iframe_styling($preview_scale * 0.80)
+}
+
+@media (max-width: 600px) {
+ @include o_preview_iframe_styling($preview_scale * 0.60)
+}
diff --git a/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.xml b/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.xml
new file mode 100644
index 00000000000..0d60c22235e
--- /dev/null
+++ b/addons/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.xml
@@ -0,0 +1,10 @@
+
+
Some content
Some content
"); + assert.strictEqual($(iframeDoc).find(".nice_div p").css("color"), "rgb(255, 0, 0)"); + }); + + QUnit.test("IframeWrapperField in form view with onchange", async function (assert) { + serverData.models.report.onchanges = { + int_field(record) { + record.html_field = record.html_field.replace("Some content", "New content"); + }, + }; + await makeView({ + type: "form", + resModel: "report", + serverData, + resId: 1, + arch: ` + `, + }); + + const iframeDoc = target.querySelector("iframe").contentDocument; + assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "Some content
"); + assert.strictEqual($(iframeDoc).find(".nice_div p").css("color"), "rgb(255, 0, 0)"); + + await editInput(target, ".o_field_widget[name=int_field] input", 264); + assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "New content
"); + }); +});