diff --git a/lib/jsdom/living/events/SubmitEvent-impl.js b/lib/jsdom/living/events/SubmitEvent-impl.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a9886e1369c7f6ee55abe697cb3b870034bee06
--- /dev/null
+++ b/lib/jsdom/living/events/SubmitEvent-impl.js
@@ -0,0 +1,13 @@
+"use strict";
+
+const EventImpl = require("./Event-impl").implementation;
+
+const SubmitEventInit = require("../generated/SubmitEventInit");
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-submitevent-interface
+class SubmitEventImpl extends EventImpl {}
+SubmitEventImpl.defaultInit = SubmitEventInit.convert(undefined, undefined);
+
+module.exports = {
+  implementation: SubmitEventImpl
+};
diff --git a/lib/jsdom/living/generated/SubmitEvent.js b/lib/jsdom/living/generated/SubmitEvent.js
new file mode 100644
index 0000000000000000000000000000000000000000..c35c4083d4ca58a3bedffd5621cc602ff97043e7
--- /dev/null
+++ b/lib/jsdom/living/generated/SubmitEvent.js
@@ -0,0 +1,144 @@
+"use strict";
+
+const conversions = require("webidl-conversions");
+const utils = require("./utils.js");
+
+const SubmitEventInit = require("./SubmitEventInit.js");
+const implSymbol = utils.implSymbol;
+const ctorRegistrySymbol = utils.ctorRegistrySymbol;
+const Event = require("./Event.js");
+
+const interfaceName = "SubmitEvent";
+
+exports.is = value => {
+  return utils.isObject(value) && utils.hasOwn(value, implSymbol) && value[implSymbol] instanceof Impl.implementation;
+};
+exports.isImpl = value => {
+  return utils.isObject(value) && value instanceof Impl.implementation;
+};
+exports.convert = (globalObject, value, { context = "The provided value" } = {}) => {
+  if (exports.is(value)) {
+    return utils.implForWrapper(value);
+  }
+  throw new globalObject.TypeError(`${context} is not of type 'SubmitEvent'.`);
+};
+
+function makeWrapper(globalObject, newTarget) {
+  let proto;
+  if (newTarget !== undefined) {
+    proto = newTarget.prototype;
+  }
+
+  if (!utils.isObject(proto)) {
+    proto = globalObject[ctorRegistrySymbol]["SubmitEvent"].prototype;
+  }
+
+  return Object.create(proto);
+}
+
+exports.create = (globalObject, constructorArgs, privateData) => {
+  const wrapper = makeWrapper(globalObject);
+  return exports.setup(wrapper, globalObject, constructorArgs, privateData);
+};
+
+exports.createImpl = (globalObject, constructorArgs, privateData) => {
+  const wrapper = exports.create(globalObject, constructorArgs, privateData);
+  return utils.implForWrapper(wrapper);
+};
+
+exports._internalSetup = (wrapper, globalObject) => {
+  Event._internalSetup(wrapper, globalObject);
+};
+
+exports.setup = (wrapper, globalObject, constructorArgs = [], privateData = {}) => {
+  privateData.wrapper = wrapper;
+
+  exports._internalSetup(wrapper, globalObject);
+  Object.defineProperty(wrapper, implSymbol, {
+    value: new Impl.implementation(globalObject, constructorArgs, privateData),
+    configurable: true
+  });
+
+  wrapper[implSymbol][utils.wrapperSymbol] = wrapper;
+  if (Impl.init) {
+    Impl.init(wrapper[implSymbol]);
+  }
+  return wrapper;
+};
+
+exports.new = (globalObject, newTarget) => {
+  const wrapper = makeWrapper(globalObject, newTarget);
+
+  exports._internalSetup(wrapper, globalObject);
+  Object.defineProperty(wrapper, implSymbol, {
+    value: Object.create(Impl.implementation.prototype),
+    configurable: true
+  });
+
+  wrapper[implSymbol][utils.wrapperSymbol] = wrapper;
+  if (Impl.init) {
+    Impl.init(wrapper[implSymbol]);
+  }
+  return wrapper[implSymbol];
+};
+
+const exposed = new Set(["Window"]);
+
+exports.install = (globalObject, globalNames) => {
+  if (!globalNames.some(globalName => exposed.has(globalName))) {
+    return;
+  }
+
+  const ctorRegistry = utils.initCtorRegistry(globalObject);
+  class SubmitEvent extends globalObject.Event {
+    constructor(type) {
+      if (arguments.length < 1) {
+        throw new globalObject.TypeError(
+          `Failed to construct 'SubmitEvent': 1 argument required, but only ${arguments.length} present.`
+        );
+      }
+      const args = [];
+      {
+        let curArg = arguments[0];
+        curArg = conversions["DOMString"](curArg, {
+          context: "Failed to construct 'SubmitEvent': parameter 1",
+          globals: globalObject
+        });
+        args.push(curArg);
+      }
+      {
+        let curArg = arguments[1];
+        curArg = SubmitEventInit.convert(globalObject, curArg, {
+          context: "Failed to construct 'SubmitEvent': parameter 2"
+        });
+        args.push(curArg);
+      }
+      return exports.setup(Object.create(new.target.prototype), globalObject, args);
+    }
+
+    get submitter() {
+      const esValue = this !== null && this !== undefined ? this : globalObject;
+
+      if (!exports.is(esValue)) {
+        throw new globalObject.TypeError(
+          "'get submitter' called on an object that is not a valid instance of SubmitEvent."
+        );
+      }
+
+      return utils.tryWrapperForImpl(esValue[implSymbol]["submitter"]);
+    }
+  }
+  Object.defineProperties(SubmitEvent.prototype, {
+    submitter: { enumerable: true },
+    [Symbol.toStringTag]: { value: "SubmitEvent", configurable: true }
+  });
+  ctorRegistry[interfaceName] = SubmitEvent;
+
+  Object.defineProperty(globalObject, interfaceName, {
+    configurable: true,
+    writable: true,
+    value: SubmitEvent
+  });
+};
+
+const Impl = require("../events/SubmitEvent-impl.js");
diff --git a/lib/jsdom/living/generated/SubmitEventInit.js b/lib/jsdom/living/generated/SubmitEventInit.js
new file mode 100644
index 0000000000000000000000000000000000000000..a911318c72571a1e4c8e866cb74e997c7744d461
--- /dev/null
+++ b/lib/jsdom/living/generated/SubmitEventInit.js
@@ -0,0 +1,36 @@
+"use strict";
+
+const conversions = require("webidl-conversions");
+const utils = require("./utils.js");
+
+const HTMLElement = require("./HTMLElement.js");
+const EventInit = require("./EventInit.js");
+
+exports._convertInherit = (globalObject, obj, ret, { context = "The provided value" } = {}) => {
+  EventInit._convertInherit(globalObject, obj, ret, { context });
+
+  {
+    const key = "submitter";
+    let value = obj === undefined || obj === null ? undefined : obj[key];
+    if (value !== undefined) {
+      if (value === null || value === undefined) {
+        value = null;
+      } else {
+        value = HTMLElement.convert(globalObject, value, { context: context + " has member 'submitter' that" });
+      }
+      ret[key] = value;
+    } else {
+      ret[key] = null;
+    }
+  }
+};
+
+exports.convert = (globalObject, obj, { context = "The provided value" } = {}) => {
+  if (obj !== undefined && typeof obj !== "object" && typeof obj !== "function") {
+    throw new globalObject.TypeError(`${context} is not an object.`);
+  }
+
+  const ret = Object.create(null);
+  exports._convertInherit(globalObject, obj, ret, { context });
+  return ret;
+};
diff --git a/lib/jsdom/living/interfaces.js b/lib/jsdom/living/interfaces.js
index d64eafb729fbe99e75ccc7697ecdd818d5a20f00..9c766b0dd08b13a322e8efe57ab137e84504b723 100644
--- a/lib/jsdom/living/interfaces.js
+++ b/lib/jsdom/living/interfaces.js
@@ -130,6 +130,7 @@ const generatedInterfaces = {
   StorageEvent: require("./generated/StorageEvent"),
   ProgressEvent: require("./generated/ProgressEvent"),
   PageTransitionEvent: require("./generated/PageTransitionEvent"),
+  SubmitEvent: require("./generated/SubmitEvent"),
 
   UIEvent: require("./generated/UIEvent"),
   FocusEvent: require("./generated/FocusEvent"),
diff --git a/lib/jsdom/living/nodes/HTMLFormElement-impl.js b/lib/jsdom/living/nodes/HTMLFormElement-impl.js
index 1ab508a6f27445171b8143374ff1fae24f3bbfa6..d611fa3243e3c452d8625ba2c79c29a2b82e5e82 100644
--- a/lib/jsdom/living/nodes/HTMLFormElement-impl.js
+++ b/lib/jsdom/living/nodes/HTMLFormElement-impl.js
@@ -9,6 +9,7 @@ const { formOwner, isListed, isSubmittable, isSubmitButton } = require("../helpe
 const HTMLFormControlsCollection = require("../generated/HTMLFormControlsCollection");
 const notImplemented = require("../../browser/not-implemented");
 const { parseURLToResultingURLRecord } = require("../helpers/document-base-url");
+const SubmitEvent = require("../generated/SubmitEvent");
 
 const encTypes = new Set([
   "application/x-www-form-urlencoded",
@@ -87,8 +88,8 @@ class HTMLFormElementImpl extends HTMLElementImpl {
     notImplemented("HTMLFormElement.prototype.submit", this._ownerDocument._defaultView);
   }
 
-  requestSubmit(submitter = undefined) {
-    if (submitter !== undefined) {
+  requestSubmit(submitter = null) {
+    if (submitter !== null) {
       if (!isSubmitButton(submitter)) {
         throw new TypeError("The specified element is not a submit button");
       }
@@ -106,7 +107,7 @@ class HTMLFormElementImpl extends HTMLElementImpl {
       return;
     }
 
-    if (!fireAnEvent("submit", this, undefined, { bubbles: true, cancelable: true })) {
+    if (!fireAnEvent("submit", this, SubmitEvent, { bubbles: true, cancelable: true, submitter })) {
       return;
     }
 
