diff --git a/src/elements/a-gltf-entity.js b/src/components/gltf-model-plus.js
similarity index 58%
rename from src/elements/a-gltf-entity.js
rename to src/components/gltf-model-plus.js
index e88f7a76bf6ee4218f37997d9497e576b3bed55a..bcc9429adcac9b2bf5b76992d6def8f2a285c5bb 100644
--- a/src/elements/a-gltf-entity.js
+++ b/src/components/gltf-model-plus.js
@@ -187,133 +187,95 @@ function cachedLoadGLTF(src, onProgress) {
   });
 }
 
-AFRAME.registerElement("a-gltf-entity", {
-  prototype: Object.create(AFRAME.AEntity.prototype, {
-    load: {
-      async value() {
-        if (this.hasLoaded || !this.parentEl) {
-          return;
-        }
-
-        // The code above and below this are from AEntity.prototype.load, we need to monkeypatch in gltf loading mid function
-        this.loadTemplates();
-        await this.applySrc(this.getAttribute("src"));
-        //
-
-        AFRAME.ANode.prototype.load.call(this, () => {
-          // Check if entity was detached while it was waiting to load.
-          if (!this.parentEl) {
-            return;
-          }
+AFRAME.registerComponent("gltf-model-plus", {
+  schema: {
+    src: { type: "string" },
+    inflate: { default: false }
+  },
 
-          this.updateComponents();
-          if (this.isScene || this.parentEl.isPlaying) {
-            this.play();
-          }
-        });
-      }
-    },
-
-    loadTemplates: {
-      value() {
-        this.templates = [];
-        this.querySelectorAll(":scope > template").forEach(templateEl =>
-          this.templates.push({
-            selector: templateEl.getAttribute("data-selector"),
-            templateRoot: document.importNode(templateEl.content.firstElementChild, true)
-          })
-        );
-      }
-    },
-
-    applySrc: {
-      async value(src) {
-        try {
-          // If the src attribute is a selector, get the url from the asset item.
-          if (src && src.charAt(0) === "#") {
-            const assetEl = document.getElementById(src.substring(1));
-
-            const fallbackSrc = assetEl.getAttribute("src");
-            const highSrc = assetEl.getAttribute("high-src");
-            const lowSrc = assetEl.getAttribute("low-src");
-
-            if (highSrc && window.APP.quality === "high") {
-              src = highSrc;
-            } else if (lowSrc && window.APP.quality === "low") {
-              src = lowSrc;
-            } else {
-              src = fallbackSrc;
-            }
-          }
+  init() {
+    this.loadTemplates();
+  },
 
-          if (src === this.lastSrc) return;
-          this.lastSrc = src;
+  update() {
+    this.applySrc(this.data.src);
+  },
 
-          if (!src) {
-            if (this.inflatedEl) {
-              console.warn("gltf-entity set to an empty source, unloading inflated model.");
-              this.removeInflatedEl();
-            }
-            return;
-          }
+  loadTemplates() {
+    this.templates = [];
+    this.el.querySelectorAll(":scope > template").forEach(templateEl =>
+      this.templates.push({
+        selector: templateEl.getAttribute("data-selector"),
+        templateRoot: document.importNode(templateEl.content.firstElementChild, true)
+      })
+    );
+  },
 
-          const model = await cachedLoadGLTF(src);
+  async applySrc(src) {
+    try {
+      // If the src attribute is a selector, get the url from the asset item.
+      if (src && src.charAt(0) === "#") {
+        const assetEl = document.getElementById(src.substring(1));
+
+        const fallbackSrc = assetEl.getAttribute("src");
+        const highSrc = assetEl.getAttribute("high-src");
+        const lowSrc = assetEl.getAttribute("low-src");
+
+        if (highSrc && window.APP.quality === "high") {
+          src = highSrc;
+        } else if (lowSrc && window.APP.quality === "low") {
+          src = lowSrc;
+        } else {
+          src = fallbackSrc;
+        }
+      }
 
-          // If we started loading something else already
-          // TODO: there should be a way to cancel loading instead
-          if (src != this.lastSrc) return;
+      if (src === this.lastSrc) return;
+      this.lastSrc = src;
 
-          // If we had inflated something already before, clean that up
+      if (!src) {
+        if (this.inflatedEl) {
+          console.warn("gltf-model-plus set to an empty source, unloading inflated model.");
           this.removeInflatedEl();
+        }
+        return;
+      }
 
-          this.model = model.scene || model.scenes[0];
-          this.model.animations = model.animations;
+      const model = await cachedLoadGLTF(src);
 
-          this.setObject3D("mesh", this.model);
+      // If we started loading something else already
+      // TODO: there should be a way to cancel loading instead
+      if (src != this.lastSrc) return;
 
-          if (this.getAttribute("inflate")) {
-            this.inflatedEl = inflateEntities(this, this.model);
-            // TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more
-            // Wait one tick for the appended custom elements to be connected before attaching templates
-            await nextTick();
-            if (src != this.lastSrc) return; // TODO: there must be a nicer pattern for this
-            this.templates.forEach(attachTemplate.bind(null, this));
-          }
+      // If we had inflated something already before, clean that up
+      this.removeInflatedEl();
 
-          this.emit("model-loaded", { format: "gltf", model: this.model });
-        } catch (e) {
-          const message = (e && e.message) || "Failed to load glTF model";
-          console.error(message);
-          this.emit("model-error", { format: "gltf", src });
-        }
-      }
-    },
+      this.model = model.scene || model.scenes[0];
+      this.model.animations = model.animations;
 
-    removeInflatedEl: {
-      value() {
-        if (this.inflatedEl) {
-          this.inflatedEl.parentNode.removeChild(this.inflatedEl);
-          delete this.inflatedEl;
-        }
-      }
-    },
+      this.el.setObject3D("mesh", this.model);
 
-    attributeChangedCallback: {
-      value(attr, oldVal, newVal) {
-        if (attr === "src") {
-          this.applySrc(newVal);
-        }
-        AFRAME.AEntity.prototype.attributeChangedCallback.call(this, attr, oldVal, newVal);
+      if (this.data.inflate) {
+        this.inflatedEl = inflateEntities(this.el, this.model);
+        // TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more
+        // Wait one tick for the appended custom elements to be connected before attaching templates
+        await nextTick();
+        if (src != this.lastSrc) return; // TODO: there must be a nicer pattern for this
+        this.templates.forEach(attachTemplate.bind(null, this.el));
       }
-    },
 
-    setAttribute: {
-      value(attr, arg1, arg2) {
-        if (attr === "src") {
-          this.applySrc(arg1);
-        }
-        AFRAME.AEntity.prototype.setAttribute.call(this, attr, arg1, arg2);
-      }
+      this.el.emit("model-loaded", { format: "gltf", model: this.model });
+    } catch (e) {
+      const message = (e && e.message) || "Failed to load glTF model";
+      console.error(message);
+      this.el.emit("model-error", { format: "gltf", src });
     }
-  })
+  },
+
+  removeInflatedEl() {
+    if (this.inflatedEl) {
+      this.inflatedEl.parentNode.removeChild(this.inflatedEl);
+      delete this.inflatedEl;
+    }
+  }
 });
diff --git a/src/components/player-info.js b/src/components/player-info.js
index 8cb265a67b9c67ac26510d6b8a03efb59703c931..57d459862a843bd9dd78e65071e2cb3f62b19dab 100644
--- a/src/components/player-info.js
+++ b/src/components/player-info.js
@@ -25,7 +25,7 @@ AFRAME.registerComponent("player-info", {
 
     const modelEl = this.el.querySelector(".model");
     if (this.data.avatar && modelEl) {
-      modelEl.setAttribute("src", this.data.avatar);
+      modelEl.setAttribute("gltf-model-plus", "src", this.data.avatar);
     }
   }
 });
diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js
index e4cf086733ad1e0dc547abafe3fb37ed025a5608..bde7f06975d11cf32b0626466bb03f2ce649c526 100644
--- a/src/gltf-component-mappings.js
+++ b/src/gltf-component-mappings.js
@@ -1,4 +1,4 @@
-import "./elements/a-gltf-entity";
+import "./components/gltf-model-plus";
 
 AFRAME.AGLTFEntity.registerComponent("scale-audio-feedback", "scale-audio-feedback");
 AFRAME.AGLTFEntity.registerComponent("loop-animation", "loop-animation");
diff --git a/src/room.html b/src/room.html
index 132e6af23c42d61553fc1c78c43ff0afc0cf188b..d4e915a77250ad81c5e1e15390de19c83c4f5b3c 100644
--- a/src/room.html
+++ b/src/room.html
@@ -60,7 +60,7 @@
 
                     <a-entity class="right-controller"></a-entity>
 
-                    <a-gltf-entity class="model" inflate="true">
+                    <a-entity class="model" gltf-model-plus="inflate: true">
                         <template data-selector=".RootScene">
                             <a-entity ik-controller animation-mixer></a-entity>
                         </template>
@@ -93,13 +93,13 @@
                         <template data-selector=".RightHand">
                             <a-entity personal-space-invader ></a-entity>
                         </template>
-                    </a-gltf-entity>
+                    </a-entity>
                 </a-entity>
             </template>
 
             <template id="interactable-template">
                 <a-entity
-                    gltf-model="#interactable-duck"
+                    gltf-model-plus="src: #interactable-duck"
                     scale="2 2 2"
                     class="interactable" 
                     super-networked-interactable="counter: #counter; mass: 5;"
@@ -125,7 +125,7 @@
         <a-entity id="counter" networked-counter="max: 3; ttl: 120"></a-entity>
 
         <a-entity 
-            gltf-model="#interactable-duck"
+            gltf-model-plus="src: #interactable-duck"
             scale="2 2 2"
             class="interactable" 
             super-spawner="template: #interactable-template;" 
@@ -212,7 +212,7 @@
                 app-mode-toggle-attribute__line="mode: hud; property: visible;"
             ></a-entity>
 
-            <a-gltf-entity class="model" inflate="true">
+            <a-entity gltf-model-plus="inflate: true;" class="model">
                 <template data-selector=".RootScene">
                     <a-entity
                         ik-controller 
@@ -233,14 +233,14 @@
 
                 <template data-selector=".LeftHand">
                     <a-entity>
-                        <a-gltf-entity
+                        <a-entity
                             id="watch"
-                            src="#watch-model"
+                            gltf-model-plus="src: #watch-model"
                             bone-mute-state-indicator
                             scale="1.5 1.5 1.5"
                             rotation="0 -90 90"
                             position="0 -0.04 0"
-                        ></a-gltf-entity>
+                        ></a--entity>
                         <a-entity
                             event-repeater="events: action_grab, action_release; eventSource: #player-left-controller"
                             static-body="shape: sphere; sphereRadius: 0.02"
@@ -261,7 +261,7 @@
                     </a-entity>
                 </template>
 
-            </a-gltf-entity>
+            </a-entity>
         </a-entity>
 
         <!-- Lights -->
@@ -272,34 +272,34 @@
         ></a-entity>
 
         <!-- Environment -->
-        <a-gltf-entity
+        <a-entity
             id="meeting-space"
-            src="#meeting-space1-mesh"
+            gltf-model-plus="src: #meeting-space1-mesh"
             position="0 0 0"
-        ></a-gltf-entity>
+        ></a-entity>
 
-        <a-gltf-entity
+        <a-entity
             id="outdoor-facade"
-            src="#outdoor-facade-mesh"
+            gltf-model-plus="src: #outdoor-facade-mesh"
             position="0 0 0"
             xr="ar: false"
-        ></a-gltf-entity>
+        ></a-ntity>
 
-        <a-gltf-entity
+        <a-entity
             id="floor-nav"
-            src="#floor-nav-mesh"
+            gltf-model-plus="src: #floor-nav-mesh"
             visible="false"
             position="0 0 0"
             xr="ar: false"
-        ></a-gltf-entity>
+        ></a-entity>
 
-        <a-gltf-entity
+        <a-entity
             id="cliff-vista"
-            src="#cliff-vista-mesh"
+            gltf-model-plus="src: #cliff-vista-mesh"
             layers="reflection:true"
             position="0 0 0"
             xr="ar: false"
-        ></a-gltf-entity>
+        ></a-entity>
 
         <a-entity id="skybox"
             id="skybox"
diff --git a/src/room.js b/src/room.js
index 909e8977f7bd4ce6d30e6ce4ddfc183a7e685c3b..739aa6258c3cc6188803ef1ebc8d58af4d4328e6 100644
--- a/src/room.js
+++ b/src/room.js
@@ -41,6 +41,7 @@ import "./components/player-info";
 import "./components/debug";
 import "./components/animation-mixer";
 import "./components/loop-animation";
+import "./components/gltf-model-plus";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -49,8 +50,6 @@ import UIRoot from "./react-components/ui-root";
 import "./systems/personal-space-bubble";
 import "./systems/app-mode";
 
-import "./elements/a-gltf-entity";
-
 import "./gltf-component-mappings";
 
 import { App } from "./App";
@@ -176,8 +175,7 @@ async function enterScene(mediaStream, enterInVR) {
       for (const track of videoTracks) {
         mediaStream.addTrack(track);
       }
-    }
-    else {
+    } else {
       for (const track of mediaStream.getVideoTracks()) {
         mediaStream.removeTrack(track);
       }