diff --git a/app/components/component-tree-arg.js b/app/components/component-tree-arg.js
index 00c924fe22..9b7501e61a 100644
--- a/app/components/component-tree-arg.js
+++ b/app/components/component-tree-arg.js
@@ -12,6 +12,15 @@ export default class ComponentTreeArg extends Component {
 
   get displayValue() {
     if (this.isObject) {
+      if (this.args.value.inspect) {
+        if (this.args.value.type === 'function') {
+          return this.args.value.inspect
+            .replace(/"/g, '\\"')
+            .replace('bound ', '')
+            .replace('{ ... }', '');
+        }
+        return truncate(this.args.value.inspect.replace(/"/g, '\\"'));
+      }
       return '...';
     } else if (typeof this.args.value === 'string') {
       // Escape any interior quotes – we will add the surrounding quotes in the template
diff --git a/app/components/component-tree-item.hbs b/app/components/component-tree-item.hbs
index 450b7ce0b4..aa7ceab593 100644
--- a/app/components/component-tree-item.hbs
+++ b/app/components/component-tree-item.hbs
@@ -23,8 +23,10 @@
 
     <code
       class="component-tree-item__tag flex whitespace-no-wrap
+        {{if @item.isClosingTag 'component-tree-item__closing'}}
+        {{if @item.hasModifiers 'component-tree-item__has_modifier'}}
         {{if
-          @item.isComponent
+        (or @item.isComponent @item.isModifier @item.isHtmlTag)
           (if
             @item.isCurlyInvocation
             "component-tree-item-classic__bracket"
@@ -37,7 +39,7 @@
         }}"
     >
       {{!-- template-lint-disable no-unbalanced-curlies --}}
-      {{#if @item.isComponent}}
+      {{#if (or @item.isComponent @item.isModifier)}}
         {{#if @item.isCurlyInvocation}}
           <span class="component-name">
             {{@item.name}}
@@ -83,6 +85,20 @@
         \{{mount "{{@item.name}}"}}
       {{else if @item.isRouteTemplate}}
         {{@item.name}} route
+      {{else if @item.isHtmlTag}}
+        <span class="component-name">
+          {{@item.name}}
+        </span>
+        {{#each-in @item.args.named as |name value|}}
+          <div class="arg-token flex ml-2">
+              <span class="key-token">
+                {{name}}
+              </span>
+            ={{if (is-string value) "\""}}
+            <ComponentTreeArg @value={{value}} />
+            {{if (is-string value) "\""}}
+          </div>
+        {{/each-in}}
       {{/if}}
     </code>
   </div>
diff --git a/app/components/object-inspector/property.ts b/app/components/object-inspector/property.ts
index 27701979ff..82181b625c 100644
--- a/app/components/object-inspector/property.ts
+++ b/app/components/object-inspector/property.ts
@@ -121,8 +121,8 @@ export default class ObjectInspectorProperty extends Component<ObjectInspectorPr
   }
 
   get cannotEdit() {
-    if (this.args.model.name === '...' || !this.isCalculated) return true;
-    return this.isFunction || this.isOverridden || this.readOnly;
+    if (this.args.model.name === '...' || !this.isCalculated || this.readOnly) return true;
+    return this.args.model?.value?.type !== 'type-string' && this.args.model?.value?.type !== 'type-number';
   }
 
   @action
diff --git a/app/controllers/component-tree.js b/app/controllers/component-tree.js
index e5b8352fcd..dd29d7275b 100644
--- a/app/controllers/component-tree.js
+++ b/app/controllers/component-tree.js
@@ -48,6 +48,21 @@ export default class ComponentTreeController extends Controller {
 
         renderItems.push(item);
 
+        if (
+          item.isHtmlTag &&
+          renderNode.children.some((c) => c.type === 'modifier')
+        ) {
+          const idx = renderNode.children.findLastIndex(
+            (c) => c.type === 'modifier'
+          );
+          renderNode.children.splice(idx + 1, 0, {
+            type: 'placeholder-closing-tag',
+            id: renderNode.id + '-closing-tag',
+            name: '',
+            children: [],
+          });
+        }
+
         renderNode.children.forEach((node) => flatten(item, node));
       }
     };
@@ -301,7 +316,32 @@ class RenderItem {
   }
 
   get isComponent() {
-    return this.renderNode.type === 'component';
+    return (
+      this.renderNode.type === 'component' ||
+      this.renderNode.type === 'remote-element'
+    );
+  }
+
+  get isModifier() {
+    return this.renderNode.type === 'modifier';
+  }
+
+  get hasModifiers() {
+    return this.childItems.some((item) => item.isModifier);
+  }
+
+  get isLastModifier() {
+    return (
+      this.parentItem.childItems.findLast((item) => item.isModifier) === this
+    );
+  }
+
+  get isHtmlTag() {
+    return this.renderNode.type === 'html-element';
+  }
+
+  get isClosingTag() {
+    return this.renderNode.type === 'placeholder-closing-tag';
   }
 
   get name() {
@@ -313,12 +353,15 @@ class RenderItem {
   }
 
   get isCurlyInvocation() {
+    if (this.isModifier) {
+      return true;
+    }
     return this.renderNode.args && this.renderNode.args.positional;
   }
 
   get hasInstance() {
     let { instance } = this.renderNode;
-    return typeof instance === 'object' && instance !== null;
+    return typeof instance === 'object' && instance;
   }
 
   get instance() {
@@ -330,7 +373,7 @@ class RenderItem {
   }
 
   get hasBounds() {
-    return this.renderNode.bounds !== null;
+    return this.renderNode.bounds;
   }
 
   get isRoot() {
@@ -400,6 +443,7 @@ class RenderItem {
   }
 
   @action showPreview() {
+    if (this.isClosingTag) return;
     this.controller.previewing = this.id;
   }
 
@@ -410,6 +454,7 @@ class RenderItem {
   }
 
   @action toggleInspection() {
+    if (this.isClosingTag) return;
     if (this.isPinned) {
       this.controller.pinned = undefined;
     } else {
diff --git a/app/styles/component_tree.scss b/app/styles/component_tree.scss
index bb399c8e1f..2b92b88c5d 100644
--- a/app/styles/component_tree.scss
+++ b/app/styles/component_tree.scss
@@ -141,6 +141,19 @@
   color: var(--base09);
 }
 
+.component-tree-item__has_modifier:after {
+  content: '' !important;
+}
+
+.component-tree-item__closing:before {
+  content: '>';
+  margin-left: -13px;
+}
+
+.component-tree-item__self-closing:before {
+  content: '/>';
+}
+
 .component-tree-item__bracket:before {
   content: '<';
 }
diff --git a/ember-cli-build.js b/ember-cli-build.js
index f383accfb7..0b92b165eb 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -381,19 +381,18 @@ module.exports = function (defaults) {
   if (env === 'test') {
     // `ember test` expects the index.html file to be in the
     // output directory.
-    output = mergeTrees([dists.basic, dists.chrome]);
-  } else {
     // Change base tag for running tests in development env.
     dists.basic = replace(dists.basic, {
       files: ['tests/index.html'],
       patterns: [
         {
           match: /<base.*\/>/,
-          replacement: '<base href="../" />',
+          replacement: '',
         },
       ],
     });
-
+    output = mergeTrees([dists.basic, dists.chrome]);
+  } else {
     dists.testing = mergeTrees([dists.basic, dists.chrome]);
 
     output = mergeTrees([
diff --git a/ember_debug/adapters/web-extension.js b/ember_debug/adapters/web-extension.js
index d1749b632d..fb98e2a882 100644
--- a/ember_debug/adapters/web-extension.js
+++ b/ember_debug/adapters/web-extension.js
@@ -28,7 +28,11 @@ export default class extends BasicAdapter {
     // "clone" them through postMessage unless they are converted to a
     // native array.
     options = deepClone(options);
-    this._chromePort.postMessage(options);
+    try {
+      this._chromePort.postMessage(options);
+    } catch (e) {
+      console.log('failed to send message', e);
+    }
   }
 
   /**
diff --git a/ember_debug/libs/render-tree.js b/ember_debug/libs/render-tree.js
index fab369ff55..64fed9239b 100644
--- a/ember_debug/libs/render-tree.js
+++ b/ember_debug/libs/render-tree.js
@@ -1,6 +1,9 @@
 import captureRenderTree from './capture-render-tree';
 import { guidFor } from 'ember-debug/utils/ember/object/internals';
-import { EmberLoader } from 'ember-debug/utils/ember/loader';
+import { EmberLoader, emberSafeRequire } from 'ember-debug/utils/ember/loader';
+import { inspect } from 'ember-debug/utils/type-check';
+import { isInVersionSpecifier } from 'ember-debug/utils/version';
+import { VERSION } from 'ember-debug/utils/ember';
 
 class InElementSupportProvider {
   constructor(owner) {
@@ -14,6 +17,12 @@ class InElementSupportProvider {
       // nope
     }
 
+    this.DESTROY = emberSafeRequire('@glimmer/util')?.DESTROY;
+    this.registerDestructor =
+      emberSafeRequire('@glimmer/destroyable')?.registerDestructor ||
+      emberSafeRequire('@ember/destroyable')?.registerDestructor ||
+      emberSafeRequire('@ember/runtime')?.registerDestructor;
+
     this.debugRenderTree =
       owner.lookup('renderer:-dom')?.debugRenderTree ||
       owner.lookup('service:-glimmer-environment')._debugRenderTree;
@@ -31,9 +40,10 @@ class InElementSupportProvider {
     const self = this;
 
     const NewElementBuilder = this.NewElementBuilder;
-    const remoteStack = [];
     const componentStack = [];
 
+    const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION);
+
     function createRef(value) {
       if (self.reference.createUnboundRef) {
         return self.reference.createUnboundRef(value);
@@ -42,6 +52,16 @@ class InElementSupportProvider {
       }
     }
 
+    function createArgs(args) {
+      if (self.reference.createUnboundRef) {
+        return args;
+      } else {
+        return {
+          value: () => args,
+        };
+      }
+    }
+
     const appendChild = this.debugRenderTree.appendChild;
     this.debugRenderTree.appendChild = function (node, state) {
       if (node.type === 'component') {
@@ -56,7 +76,7 @@ class InElementSupportProvider {
       if (node?.type === 'component') {
         componentStack.pop();
       }
-      exit.call(this, state);
+      return exit.call(this, state);
     };
 
     const didAppendNode = NewElementBuilder.prototype.didAppendNode;
@@ -71,13 +91,91 @@ class InElementSupportProvider {
       args[0].__emberInspectorParentNode = componentStack.at(-1);
     };
 
+    const pushModifiers = NewElementBuilder.prototype.pushModifiers;
+    if (enableModifierSupport) {
+      NewElementBuilder.prototype.pushModifiers = function (modifiers) {
+        const debugRenderTree = self.debugRenderTree;
+        if (debugRenderTree) {
+          modifiers = modifiers || [];
+          const modifier = modifiers[0];
+          let element = null;
+          if (modifiers.length) {
+            element = modifier[1]?.element || modifier.state.element;
+          }
+          for (const modifier of modifiers) {
+            const state = {};
+            const modifierState =
+              modifier.state?.instance || modifier.state || modifier[1];
+            const instance = modifierState?.instance || modifierState?.delegate;
+            let name =
+              modifier.definition?.resolvedName ||
+              modifierState?.debugName ||
+              instance?.name;
+            if (!name) {
+              try {
+                name = modifier.manager?.getDebugName?.();
+              } catch (e) {
+                // failed
+              }
+              name = name || 'unknown-modifier';
+            }
+            const args = {
+              positional: [],
+              named: {},
+            };
+            const positional =
+              modifierState?.args?.positional?.references ||
+              modifierState?.args?.positional ||
+              [];
+            for (const value of positional) {
+              if (value && value[self.reference.REFERENCE]) {
+                args.positional.push(value);
+              } else {
+                args.positional.push(createRef(value));
+              }
+            }
+            let named = modifierState?.args?.named;
+            if (!self.reference.createUnboundRef) {
+              try {
+                named = modifierState?.args?.named?.constructor;
+              } catch (e) {
+                //
+              }
+              try {
+                named = named || modifierState?.args?.named?.map;
+              } catch (e) {
+                //
+              }
+            }
+            for (const [key, value] of Object.entries(named || {})) {
+              args.named[key] = createRef(value);
+            }
+            debugRenderTree?.create(state, {
+              type: 'modifier',
+              name,
+              args: createArgs(args),
+              instance: instance,
+            });
+            debugRenderTree?.didRender(state, {
+              parentElement: () => element.parentElement,
+              firstNode: () => element,
+              lastNode: () => element,
+            });
+            self.registerDestructor(modifier.state, () => {
+              debugRenderTree?.willDestroy(state);
+            });
+          }
+        }
+        return pushModifiers.call(this, modifiers);
+      };
+    }
+
     const pushRemoteElement = NewElementBuilder.prototype.pushRemoteElement;
     NewElementBuilder.prototype.pushRemoteElement = function (
       element,
       guid,
       insertBefore
     ) {
-      remoteStack.push({ element });
       const ref = createRef(element);
       const capturedArgs = {
         positional: [ref],
@@ -86,18 +184,27 @@ class InElementSupportProvider {
       if (insertBefore) {
         capturedArgs.named.insertBefore = insertBefore;
       }
-      const inElementArgs = self.reference.createUnboundRef
-        ? capturedArgs
-        : {
-            value() {
-              return capturedArgs;
-            },
-          };
       const debugRenderTree = self.debugRenderTree;
-      debugRenderTree?.create(remoteStack.at(-1), {
+
+      const r = pushRemoteElement.call(this, element, guid, insertBefore);
+      const block = this.blockStack.current;
+
+      if (this.DESTROY) {
+        const destructor = block[this.DESTROY];
+        block[this.DESTROY] = function () {
+          self.debugRenderTree?.willDestroy(block);
+          destructor.call(this);
+        };
+      } else {
+        self.registerDestructor?.(block, () => {
+          self.debugRenderTree?.willDestroy(block);
+        });
+      }
+
+      debugRenderTree?.create(block, {
         type: 'keyword',
         name: 'in-element',
-        args: inElementArgs,
+        args: createArgs(capturedArgs),
         instance: {
           args: {
             named: {
@@ -110,21 +217,20 @@ class InElementSupportProvider {
           },
         },
       });
-      return pushRemoteElement.call(this, element, guid, insertBefore);
+      return r;
     };
 
     const popRemoteElement = NewElementBuilder.prototype.popRemoteElement;
     NewElementBuilder.prototype.popRemoteElement = function (...args) {
-      const element = this.element;
+      const block = this.blockStack.current;
       popRemoteElement.call(this, ...args);
       const parentElement = this.element;
       const debugRenderTree = self.debugRenderTree;
-      debugRenderTree?.didRender(remoteStack.at(-1), {
+      debugRenderTree?.didRender(block, {
         parentElement: () => parentElement,
-        firstNode: () => element,
-        lastNode: () => element,
+        firstNode: () => block.firstNode(),
+        lastNode: () => block.lastNode(),
       });
-      remoteStack.pop();
     };
 
     this.debugRenderTreeFunctions = {
@@ -136,6 +242,7 @@ class InElementSupportProvider {
       pushRemoteElement,
       popRemoteElement,
       didAppendNode,
+      pushModifiers,
     };
   }
 
@@ -148,6 +255,7 @@ class InElementSupportProvider {
       this.NewElementBuilder.prototype,
       this.NewElementBuilderFunctions
     );
+    this.NewElementBuilderFunctions = null;
   }
 
   require(req) {
@@ -175,7 +283,8 @@ export default class RenderTree {
     try {
       this.inElementSupport = new InElementSupportProvider(owner);
     } catch (e) {
-      console.error('failed to setup in element support', e);
+      console.error('failed to setup in element support');
+      console.error(e);
       // not supported
     }
 
@@ -408,18 +517,47 @@ export default class RenderTree {
     this.retainedObjects = new Map();
   }
 
-  _createTemplateOnlyComponent(args) {
+  _createSimpleInstance(name, args) {
     const obj = Object.create(null);
     obj.args = args;
     obj.constructor = {
-      name: 'TemplateOnlyComponent',
+      name: name,
       comment: 'fake constructor',
     };
     return obj;
   }
 
+  _insertHtmlElementNode(node, parentNode) {
+    const element = node.bounds.firstNode;
+    const htmlNode = {
+      id: node.id + 'html-element',
+      type: 'html-element',
+      name: element.tagName.toLowerCase(),
+      instance: element,
+      template: null,
+      bounds: {
+        firstNode: element,
+        lastNode: element,
+        parentElement: element.parentElement,
+      },
+      args: {
+        named: {},
+        positional: [],
+      },
+      children: [],
+    };
+    const idx = parentNode.children.indexOf(node);
+    parentNode.children.splice(idx, 0, htmlNode);
+    return this._serializeRenderNode(htmlNode, parentNode);
+  }
+
   _serializeRenderNodes(nodes, parentNode = null) {
-    return nodes.map((node) => this._serializeRenderNode(node, parentNode));
+    const mapped = [];
+    // nodes can be mutated during serialize, which is why we use indexing instead of .map
+    for (let i = 0; i < nodes.length; i++) {
+      mapped.push(this._serializeRenderNode(nodes[i], parentNode));
+    }
+    return mapped;
   }
 
   _serializeRenderNode(node, parentNode = null) {
@@ -460,15 +598,49 @@ export default class RenderTree {
         this.parentNodes[node.id] = parentNode;
       }
 
+      if (node.type === 'html-element') {
+        // show set attributes in inspector
+        Array.from(node.instance.attributes).forEach((attr) => {
+          node.args.named[attr.nodeName] = attr.nodeValue;
+        });
+        // move modifiers and components into the element children
+        parentNode.children.forEach((child) => {
+          if (
+            child.bounds.parentElement === node.instance ||
+            (child.type === 'modifier' &&
+              child.bounds.firstNode === node.instance)
+          ) {
+            node.children.push(child);
+          }
+        });
+        node.children.forEach((child) => {
+          const idx = parentNode.children.indexOf(child);
+          if (idx >= 0) {
+            parentNode.children.splice(idx, 1);
+          }
+        });
+      }
+
+      if (node.type === 'component' && !node.instance) {
+        node.instance = this._createSimpleInstance(
+          'TemplateOnlyComponent',
+          node.args.named
+        );
+      }
+
+      if (node.type === 'modifier') {
+        node.instance =
+          node.instance || this._createSimpleInstance(node.name, node.args);
+        node.instance.toString = () => node.name;
+        if (parentNode.instance !== node.bounds.firstNode) {
+          return this._insertHtmlElementNode(node, parentNode);
+        }
+      }
+
       this.serialized[node.id] = serialized = {
         ...node,
         args: this._serializeArgs(node.args),
-        instance: this._serializeItem(
-          node.instance ||
-            (node.type === 'component'
-              ? this._createTemplateOnlyComponent(node.args.named)
-              : undefined)
-        ),
+        instance: this._serializeItem(node.instance),
         bounds: this._serializeBounds(node.bounds),
         children: this._serializeRenderNodes(node.children, node),
       };
@@ -535,7 +707,7 @@ export default class RenderTree {
 
     this.retainedObjects.set(object, id);
 
-    return { id };
+    return { id, type: typeof object, inspect: inspect(object) };
   }
 
   _releaseStaleObjects() {
@@ -578,8 +750,15 @@ export default class RenderTree {
     while (candidates.length > 0) {
       let candidate = candidates.shift();
       let range = this.getRange(candidate.id);
+      const isAllowed =
+        candidate.type !== 'modifier' && candidate.type !== 'html-element';
+
+      if (!isAllowed) {
+        candidates.push(...candidate.children);
+        continue;
+      }
 
-      if (range && range.isPointInRange(dom, 0)) {
+      if (isAllowed && range && range.isPointInRange(dom, 0)) {
         // We may be able to find a more exact match in one of the children.
         return (
           this._matchRenderNodes(candidate.children, dom, false) || candidate
diff --git a/ember_debug/object-inspector.js b/ember_debug/object-inspector.js
index a266c4f1fa..fda102f50c 100644
--- a/ember_debug/object-inspector.js
+++ b/ember_debug/object-inspector.js
@@ -5,9 +5,9 @@ import {
   isComputed,
   getDescriptorFor,
   typeOf,
+  inspect,
 } from 'ember-debug/utils/type-check';
 import { compareVersion } from 'ember-debug/utils/version';
-import { inspect as emberInspect } from 'ember-debug/utils/ember/debug';
 import {
   EmberObject,
   meta as emberMeta,
@@ -113,6 +113,12 @@ function inspectValue(object, key, computedValue) {
 
   // TODO: this is not very clean. We should refactor calculateCP, etc, rather than passing computedValue
   if (computedValue !== undefined) {
+    if (value instanceof HTMLElement) {
+      return {
+        type: 'type-object',
+        inspect: `<${value.tagName.toLowerCase()}>`,
+      };
+    }
     return { type: `type-${typeOf(value)}`, inspect: inspect(value) };
   }
 
@@ -123,83 +129,13 @@ function inspectValue(object, key, computedValue) {
     return { type: 'type-descriptor', inspect: string };
   } else if (value?.isDescriptor) {
     return { type: 'type-descriptor', inspect: value.toString() };
+  } else if (value instanceof HTMLElement) {
+    return { type: 'type-object', inspect: value.tagName.toLowerCase() };
   } else {
     return { type: `type-${typeOf(value)}`, inspect: inspect(value) };
   }
 }
 
-function inspect(value) {
-  if (typeof value === 'function') {
-    return 'function() { ... }';
-  } else if (value instanceof EmberObject) {
-    return value.toString();
-  } else if (typeOf(value) === 'array') {
-    if (value.length === 0) {
-      return '[]';
-    } else if (value.length === 1) {
-      return `[ ${inspect(value[0])} ]`;
-    } else {
-      return `[ ${inspect(value[0])}, ... ]`;
-    }
-  } else if (value instanceof Error) {
-    return `Error: ${value.message}`;
-  } else if (value === null) {
-    return 'null';
-  } else if (typeOf(value) === 'date') {
-    return value.toString();
-  } else if (typeof value === 'object') {
-    // `Ember.inspect` is able to handle this use case,
-    // but it is very slow as it loops over all props,
-    // so summarize to just first 2 props
-    // if it defines a toString, we use that instead
-    if (
-      typeof value.toString === 'function' &&
-      value.toString !== Object.prototype.toString &&
-      value.toString !== Function.prototype.toString
-    ) {
-      try {
-        return `<Object:${value.toString()}>`;
-      } catch (e) {
-        //
-      }
-    }
-    let ret = [];
-    let v;
-    let count = 0;
-    let broken = false;
-
-    for (let key in value) {
-      if (!('hasOwnProperty' in value) || value.hasOwnProperty(key)) {
-        if (count++ > 1) {
-          broken = true;
-          break;
-        }
-        v = value[key];
-        if (v === 'toString') {
-          continue;
-        } // ignore useless items
-        if (typeOf(v).includes('function')) {
-          v = 'function() { ... }';
-        }
-        if (typeOf(v) === 'array') {
-          v = `[Array : ${v.length}]`;
-        }
-        if (typeOf(v) === 'object') {
-          v = '[Object]';
-        }
-        ret.push(`${key}: ${v}`);
-      }
-    }
-    let suffix = ' }';
-    if (broken) {
-      suffix = ' ...}';
-    }
-    return `{ ${ret.join(', ')}${suffix}`;
-  } else {
-    return emberInspect(value);
-  }
-}
-
 function isMandatorySetter(descriptor) {
   if (
     descriptor.set &&
diff --git a/ember_debug/utils/get-object-name.js b/ember_debug/utils/get-object-name.js
index c70504c2fa..a01173daf3 100644
--- a/ember_debug/utils/get-object-name.js
+++ b/ember_debug/utils/get-object-name.js
@@ -7,6 +7,10 @@ export default function getObjectName(object) {
       (emberNames.get(object.constructor) || object.constructor.name)) ||
     '';
 
+  if (object instanceof Function) {
+    return 'Function ' + object.name;
+  }
+
   // check if object is a primitive value
   if (object !== Object(object)) {
     return typeof object;
diff --git a/ember_debug/utils/type-check.js b/ember_debug/utils/type-check.js
index de0f129028..9c71aa48bc 100644
--- a/ember_debug/utils/type-check.js
+++ b/ember_debug/utils/type-check.js
@@ -1,5 +1,9 @@
-import Debug from 'ember-debug/utils/ember/debug';
-import { meta as emberMeta, ComputedProperty } from 'ember-debug/utils/ember';
+import Debug, { inspect as emberInspect } from 'ember-debug/utils/ember/debug';
+import {
+  ComputedProperty,
+  EmberObject,
+  meta as emberMeta,
+} from 'ember-debug/utils/ember';
 import { emberSafeRequire } from 'ember-debug/utils/ember/loader';
 
 /**
@@ -53,3 +57,77 @@ export function typeOf(obj) {
     .match(/\s([a-zA-Z]+)/)[1]
     .toLowerCase();
 }
+
+export function inspect(value) {
+  if (typeof value === 'function') {
+    return `${value.name || 'function'}() { ... }`;
+  } else if (value instanceof EmberObject) {
+    return value.toString();
+  } else if (value instanceof HTMLElement) {
+    return `<${value.tagName.toLowerCase()}>`;
+  } else if (typeOf(value) === 'array') {
+    if (value.length === 0) {
+      return '[]';
+    } else if (value.length === 1) {
+      return `[ ${inspect(value[0])} ]`;
+    } else {
+      return `[ ${inspect(value[0])}, ... ]`;
+    }
+  } else if (value instanceof Error) {
+    return `Error: ${value.message}`;
+  } else if (value === null) {
+    return 'null';
+  } else if (typeOf(value) === 'date') {
+    return value.toString();
+  } else if (typeof value === 'object') {
+    // `Ember.inspect` is able to handle this use case,
+    // but it is very slow as it loops over all props,
+    // so summarize to just first 2 props
+    // if it defines a toString, we use that instead
+    if (
+      typeof value.toString === 'function' &&
+      value.toString !== Object.prototype.toString &&
+      value.toString !== Function.prototype.toString
+    ) {
+      try {
+        return `<Object:${value.toString()}>`;
+      } catch (e) {
+        //
+      }
+    }
+    let ret = [];
+    let v;
+    let count = 0;
+    let broken = false;
+
+    for (let key in value) {
+      if (!('hasOwnProperty' in value) || value.hasOwnProperty(key)) {
+        if (count++ > 1) {
+          broken = true;
+          break;
+        }
+        v = value[key];
+        if (v === 'toString') {
+          continue;
+        } // ignore useless items
+        if (typeOf(v).includes('function')) {
+          v = `function ${v.name}() { ... }`;
+        }
+        if (typeOf(v) === 'array') {
+          v = `[Array : ${v.length}]`;
+        }
+        if (typeOf(v) === 'object') {
+          v = '[Object]';
+        }
+        ret.push(`${key}: ${v}`);
+      }
+    }
+    let suffix = ' }';
+    if (broken) {
+      suffix = ' ...}';
+    }
+    return `{ ${ret.join(', ')}${suffix}`;
+  } else {
+    return emberInspect(value);
+  }
+}
diff --git a/ember_debug/utils/version.js b/ember_debug/utils/version.js
index 45eb3020db..d07b125267 100644
--- a/ember_debug/utils/version.js
+++ b/ember_debug/utils/version.js
@@ -23,6 +23,43 @@ export function compareVersion(version1, version2) {
   return 0;
 }
 
+/**
+ *
+ * @param specifier e.g. ^5.12.0
+ * @param version 5.13
+ * @return {boolean}
+ */
+export function isInVersionSpecifier(specifier, version) {
+  let compared, i, version2;
+  let operator = specifier[0];
+  if (Number.isNaN(+operator)) {
+    specifier = specifier.slice(1);
+  }
+  specifier = cleanupVersion(specifier).split('.');
+  version2 = cleanupVersion(version).split('.');
+  if (operator === '~' && specifier[1] !== version2[1]) {
+    return false;
+  }
+  if (operator === '^' && specifier[0] !== version2[0]) {
+    return false;
+  }
+
+  if (operator === '>' && specifier[0] > version2[0]) {
+    return false;
+  }
+
+  for (i = 0; i < 3; i++) {
+    compared = compare(+specifier[i], +version2[i]);
+    if (compared < 0) {
+      return true;
+    }
+    if (compared > 0) {
+      return false;
+    }
+  }
+  return true;
+}
+
 /**
  * Remove -alpha, -beta, etc from versions
  *
diff --git a/tests/ember_debug/view-debug-test.js b/tests/ember_debug/view-debug-test.js
index 53796f7807..3a58d7c860 100644
--- a/tests/ember_debug/view-debug-test.js
+++ b/tests/ember_debug/view-debug-test.js
@@ -12,10 +12,13 @@ import EmberComponent from '@ember/component';
 import EmberRoute from '@ember/routing/route';
 import EmberObject from '@ember/object';
 import Controller from '@ember/controller';
+import didInsert from '@ember/render-modifiers/modifiers/did-insert';
 import QUnit, { module, test } from 'qunit';
 import { hbs } from 'ember-cli-htmlbars';
 import EmberDebug from 'ember-debug/main';
 import setupEmberDebugTest from '../helpers/setup-ember-debug-test';
+import { isInVersionSpecifier } from 'ember-debug/utils/version';
+import { VERSION } from 'ember-debug/utils/ember';
 
 let templateOnlyComponent = null;
 try {
@@ -265,6 +268,47 @@ function Component(
   );
 }
 
+function Modifier(
+  {
+    name,
+    instance = Serialized(),
+    template = null,
+    bounds = 'single',
+    ...options
+  },
+  ...children
+) {
+  return RenderNode(
+    { name, instance, template, bounds, ...options, type: 'modifier' },
+    ...children
+  );
+}
+
+function HtmlElement(
+  {
+    name,
+    instance = Serialized(),
+    args = Args(),
+    template = null,
+    bounds = 'single',
+    ...options
+  },
+  ...children
+) {
+  return RenderNode(
+    {
+      name,
+      instance,
+      args,
+      template,
+      bounds,
+      ...options,
+      type: 'html-element',
+    },
+    ...children
+  );
+}
+
 function Route(
   {
     name,
@@ -430,6 +474,7 @@ module('Ember Debug - View', function (hooks) {
     this.owner.register(
       'controller:simple',
       Controller.extend({
+        foo() {},
         get elementTarget() {
           return document.querySelector('#target');
         },
@@ -501,7 +546,11 @@ module('Ember Debug - View', function (hooks) {
     this.owner.register(
       'template:simple',
       hbs(
-        'Simple {{test-foo}} {{test-bar value=(hash x=123 [x.y]=456)}} {{#in-element this.elementTarget}}<TestComponentInInElement />{{/in-element}}',
+        `
+        <div {{did-insert this.foo}}>
+          Simple {{test-foo}} {{test-bar value=(hash x=123 [x.y]=456)}} {{#in-element this.elementTarget}}<TestComponentInInElement />{{/in-element}}
+        </div>
+        `,
         {
           moduleName: 'my-app/templates/simple.hbs',
         }
@@ -587,6 +636,8 @@ module('Ember Debug - View', function (hooks) {
                 {{/in-element}}
               `)
     );
+
+    this.owner.register('modifier:did-insert', didInsert);
   });
 
   test('Simple Inputs Tree', async function () {
@@ -596,6 +647,41 @@ module('Ember Debug - View', function (hooks) {
 
     const inputChildren = [];
     // https://github.com/emberjs/ember.js/commit/e6cf1766f8e02ddb24bf67833c148e7d7c93182f
+    const modifiers = [
+      Modifier({
+        name: 'on',
+        args: Args({ positionals: 2 }),
+      }),
+      Modifier({
+        name: 'on',
+        args: Args({ positionals: 2 }),
+      }),
+      Modifier({
+        name: 'on',
+        args: Args({ positionals: 2 }),
+      }),
+      Modifier({
+        name: 'on',
+        args: Args({ positionals: 2 }),
+      }),
+      Modifier({
+        name: 'on',
+        args: Args({ positionals: 2 }),
+      }),
+    ];
+    if (hasEmberVersion(3, 28) && !hasEmberVersion(4, 0)) {
+      modifiers.push(
+        Modifier({
+          name: 'deprecated-event-handlers',
+          args: Args({ positionals: 1 }),
+        })
+      );
+    }
+    const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION);
+    if (!enableModifierSupport) {
+      modifiers.length = 0;
+    }
+
     if (!hasEmberVersion(3, 26)) {
       inputChildren.push(
         Component({
@@ -606,6 +692,19 @@ module('Ember Debug - View', function (hooks) {
       );
     }
 
+    if (enableModifierSupport) {
+      const htmlElement = HtmlElement(
+        {
+          name: 'input',
+          args: Args({ names: ['id', 'class', 'type'] }),
+        },
+        ...modifiers
+      );
+      if (hasEmberVersion(3, 26)) {
+        inputChildren.push(htmlElement);
+      }
+    }
+
     matchTree(tree, [
       TopLevel(
         Route(
@@ -634,55 +733,77 @@ module('Ember Debug - View', function (hooks) {
 
     let argsTestPromise;
 
+    const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION);
+
+    const children = [
+      Component({ name: 'test-foo', bounds: 'single' }),
+      Component({
+        name: 'test-bar',
+        bounds: 'range',
+        args: Args({ names: ['value'], positionals: 0 }),
+        instance: (actual) => {
+          async function testArgsValue() {
+            const value = await digDeeper(actual.id, 'args');
+            QUnit.assert.equal(
+              value.details[0].properties[0].value.inspect,
+              '{ x: 123, x.y: 456 }',
+              'test-bar args value inspect should be correct'
+            );
+          }
+          argsTestPromise = testArgsValue();
+        },
+      }),
+      Component(
+        {
+          name: 'in-element',
+          args: (actual) => {
+            QUnit.assert.ok(actual.positional[0]);
+            async function testArgsValue() {
+              const value = await inspectById(actual.positional[0].id);
+              QUnit.assert.equal(
+                value.details[1].name,
+                'HTMLDivElement',
+                'in-element args value inspect should be correct'
+              );
+            }
+            argsTestPromise = testArgsValue();
+          },
+          template: null,
+        },
+        Component({
+          name: 'test-component-in-in-element',
+          template: () => null,
+        })
+      ),
+    ];
+
+    const root = [];
+
+    if (enableModifierSupport) {
+      root.push(
+        ...[
+          HtmlElement(
+            {
+              name: 'div',
+            },
+            Modifier({
+              name: 'did-insert',
+              args: Args({ positionals: 1 }),
+            }),
+            ...children
+          ),
+        ]
+      );
+    } else {
+      root.push(...children);
+    }
+
     matchTree(tree, [
       TopLevel(
-        Route(
-          { name: 'application' },
-          Route(
-            { name: 'simple' },
-            Component({ name: 'test-foo', bounds: 'single' }),
-            Component({
-              name: 'test-bar',
-              bounds: 'range',
-              args: Args({ names: ['value'], positionals: 0 }),
-              instance: (actual) => {
-                async function testArgsValue() {
-                  const value = await digDeeper(actual.id, 'args');
-                  QUnit.assert.equal(
-                    value.details[0].properties[0].value.inspect,
-                    '{ x: 123, x.y: 456 }',
-                    'value inspect should be correct'
-                  );
-                }
-                argsTestPromise = testArgsValue();
-              },
-            }),
-            Component(
-              {
-                name: 'in-element',
-                args: (actual) => {
-                  QUnit.assert.ok(actual.positional[0]);
-                  async function testArgsValue() {
-                    const value = await inspectById(actual.positional[0].id);
-                    QUnit.assert.equal(
-                      value.details[1].name,
-                      'HTMLDivElement',
-                      'in-element args value inspect should be correct'
-                    );
-                  }
-                  argsTestPromise = testArgsValue();
-                },
-                template: null,
-              },
-              Component({
-                name: 'test-component-in-in-element',
-                template: () => null,
-              })
-            )
-          )
-        )
+        Route({ name: 'application' }, Route({ name: 'simple' }, ...root))
       ),
     ]);
+
     QUnit.assert.ok(
       argsTestPromise instanceof Promise,
       'args should be tested'
@@ -716,31 +837,52 @@ module('Ember Debug - View', function (hooks) {
 
     let tree = await getRenderTree();
 
+    const root = [];
+
+    const children = [
+      Component({ name: 'test-foo', bounds: 'single' }),
+      Component({
+        name: 'test-bar',
+        bounds: 'range',
+        args: Args({ names: ['value'], positionals: 0 }),
+      }),
+      Component(
+        {
+          name: 'in-element',
+          args: Args({ names: [], positionals: 1 }),
+          template: null,
+        },
+        Component({
+          name: 'test-component-in-in-element',
+          template: () => null,
+        })
+      ),
+    ];
+
+    const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION);
+
+    if (enableModifierSupport) {
+      root.push(
+        ...[
+          HtmlElement(
+            {
+              name: 'div',
+            },
+            Modifier({
+              name: 'did-insert',
+              args: Args({ positionals: 1 }),
+            }),
+            ...children
+          ),
+        ]
+      );
+    } else {
+      root.push(...children);
+    }
+
     matchTree(tree, [
       TopLevel(
-        Route(
-          { name: 'application' },
-          Route(
-            { name: 'simple' },
-            Component({ name: 'test-foo', bounds: 'single' }),
-            Component({
-              name: 'test-bar',
-              bounds: 'range',
-              args: Args({ names: ['value'], positionals: 0 }),
-            }),
-            Component(
-              {
-                name: 'in-element',
-                args: Args({ names: [], positionals: 1 }),
-                template: null,
-              },
-              Component({
-                name: 'test-component-in-in-element',
-                template: () => null,
-              })
-            )
-          )
-        )
+        Route({ name: 'application' }, Route({ name: 'simple' }, ...root))
       ),
     ]);
   });
diff --git a/tests/index.html b/tests/index.html
index 83f808c012..daf58d259a 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -5,6 +5,7 @@
     <title>EmberInspector Tests</title>
     <meta name="description" content="">
     <meta name="viewport" content="width=device-width, initial-scale=1">
+    <base href="../" />
     <style>
       /* Smoke and mirrors requires the container
       to be visible all the time since rendering