diff --git a/public/index.html b/public/index.html
index e12e192910bbe15d4c69eab76a9772b18d8b999b..d7d57d9c635af8ecc5281fc8e6554bf282d98b17 100644
--- a/public/index.html
+++ b/public/index.html
@@ -36,11 +36,12 @@
                     matcolor-audio-feedback="objectName: DodecAvatar_Head_0"
                     scale-audio-feedback
                     avatar-customization
+                    personal-space-invader
                 ></a-entity>
             </script>
 
             <script id="hand-template" type="text/html">
-                <a-box class="hand" scale="0.2 0.1 0.3"></a-box>
+                <a-box class="hand" personal-space-invader scale="0.2 0.1 0.3"></a-box>
             </script>
 
             <script id="nametag-template" type="text/html">
@@ -55,7 +56,7 @@
 
         <a-entity id="player-rig" networked wasd-controls snap-rotation="pivotSrc: #head">
             <a-sphere scale="0.1 0.1 0.1"></a-sphere>
-            <a-entity id="head" camera="userHeight: 1.6" look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
+            <a-entity id="head" camera="userHeight: 1.6" personal-space-bubble look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
 
             <a-entity id="nametag" networked="template:#nametag-template;showLocalTemplate:false;"></a-entity>
 
diff --git a/src/index.js b/src/index.js
index 6889117a313f18fd2871130643b771437b0f0ffe..a54e9b8f13e73e18495e42ad1c65e7858df3c4a7 100644
--- a/src/index.js
+++ b/src/index.js
@@ -13,6 +13,8 @@ import "./components/nametag-transform";
 import "./components/avatar-customization";
 import "./components/mute-state-indicator";
 
+import "./systems/personal-space-bubble";
+
 import { generateName } from "./utils";
 
 NAF.schemas.add({
diff --git a/src/systems/personal-space-bubble.js b/src/systems/personal-space-bubble.js
new file mode 100644
index 0000000000000000000000000000000000000000..9087ec5a4614c0d80a400e13cca0382bf69236a0
--- /dev/null
+++ b/src/systems/personal-space-bubble.js
@@ -0,0 +1,93 @@
+var invaderPos = new AFRAME.THREE.Vector3();
+var bubblePos = new AFRAME.THREE.Vector3();
+
+AFRAME.registerSystem("personal-space-bubble", {
+  init() {
+    this.invaders = [];
+    this.bubbles = [];
+  },
+
+  registerBubble(el) {
+    this.bubbles.push(el);
+  },
+
+  unregisterBubble(el) {
+    var index = this.bubbles.indexOf(el);
+
+    if (index !== -1) {
+      this.bubbles.splice(index, 1);
+    }
+  },
+
+  registerInvader(el) {
+    var networkedEl = NAF.utils.getNetworkedEntity(el);
+    var owner = NAF.utils.getNetworkOwner(networkedEl);
+
+    if (owner !== NAF.clientId) {
+      this.invaders.push(el);
+    }
+  },
+
+  unregisterInvader(el) {
+    var index = this.invaders.indexOf(el);
+
+    if (index !== -1) {
+      this.invaders.splice(index, 1);
+    }
+  },
+
+  tick() {
+    // Update matrix positions once for each space bubble and space invader
+    for (var i = 0; i < this.bubbles.length; i++) {
+      this.bubbles[i].object3D.updateMatrixWorld(true);
+    }
+
+    for (var i = 0; i < this.invaders.length; i++) {
+      this.invaders[i].object3D.updateMatrixWorld(true);
+    }
+
+    // Loop through all of the space bubbles (usually one)
+    for (var i = 0; i < this.bubbles.length; i++) {
+      var bubble = this.bubbles[i];
+
+      bubblePos.setFromMatrixPosition(bubble.object3D.matrixWorld);
+
+      var radius = bubble.components["personal-space-bubble"].data.radius;
+      var radiusSquared = radius * radius;
+
+      // Hide the invader if inside the bubble
+      for (var j = 0; j < this.invaders.length; j++) {
+        var invader = this.invaders[j];
+
+        invaderPos.setFromMatrixPosition(invader.object3D.matrixWorld);
+
+        var distanceSquared = bubblePos.distanceTo(invaderPos);
+
+        invader.object3D.visible = distanceSquared > radiusSquared;
+      }
+    }
+  }
+});
+
+AFRAME.registerComponent("personal-space-invader", {
+  init() {
+    this.el.sceneEl.systems["personal-space-bubble"].registerInvader(this.el);
+  },
+
+  remove() {
+    this.el.sceneEl.systems["personal-space-bubble"].unregisterInvader(this.el);
+  }
+});
+
+AFRAME.registerComponent("personal-space-bubble", {
+  schema: {
+    radius: { type: "number", default: 0.8 }
+  },
+  init() {
+    this.system.registerBubble(this.el);
+  },
+
+  remove() {
+    this.system.unregisterBubble(this.el);
+  }
+});