diff --git a/public/index.html b/public/index.html
index 6bcedf148c094a516dbc77f4d56c6e8f1e5f766b..718096f2444c7959d9fe7c39c2d57ad2cdb9c79d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -35,11 +35,12 @@
                     matcolor-audio-feedback="objectName: DodecAvatar_Head_0"
                     scale-audio-feedback
                     avatar-customization
+                    personal-space-bubble
                 ></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-bubble scale="0.2 0.1 0.3"></a-box>
             </script>
 
             <script id="nametag-template" type="text/html">
diff --git a/src/index.js b/src/index.js
index fabb513a4751cda114a9ed2d5d30d8c6403f42d6..d0f67158a83c6dc1b2a15f7978ee72c35f3cd0fc 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,6 +11,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..5b8121ad9a6b84317da914601e29f2703508d01c
--- /dev/null
+++ b/src/systems/personal-space-bubble.js
@@ -0,0 +1,73 @@
+var posA = new AFRAME.THREE.Vector3();
+var posB = new AFRAME.THREE.Vector3();
+
+function distance(entityA, entityB) {
+  entityA.object3D.getWorldPosition(posA);
+  entityB.object3D.getWorldPosition(posB);
+  return posA.distanceTo(posB);
+}
+
+AFRAME.registerSystem("personal-space-bubble", {
+  init() {
+    this.myEntities = [];
+    this.entities = [];
+  },
+
+  registerEntity(el) {
+    var networkedEl = NAF.utils.getNetworkedEntity(el);
+    var owner = NAF.utils.getNetworkOwner(networkedEl);
+
+    if (owner !== NAF.clientId) {
+      this.entities.push(el);
+    } else {
+      this.myEntities.push(el);
+    }
+  },
+
+  unregisterEntity(el) {
+    var networkedEl = NAF.utils.getNetworkedEntity(el);
+    var owner = NAF.utils.getNetworkOwner(networkedEl);
+
+    if (owner !== NAF.clientId) {
+      var index = this.entities.indexOf(el);
+      this.entities.splice(index, 1);
+    } else {
+      var index = this.myEntities.indexOf(el);
+      this.myEntities.splice(index, 1);
+    }
+  },
+
+  tick() {
+    for (var j = 0; j < this.entities.length; j++) {
+      var otherEntity = this.entities[j];
+
+      var visible = true;
+
+      for (var i = 0; i < this.myEntities.length; i++) {
+        var myEntity = this.myEntities[i];
+
+        var d = distance(myEntity, otherEntity);
+
+        if (d < myEntity.components["personal-space-bubble"].data.radius) {
+          visible = false;
+          break;
+        }
+      }
+
+      otherEntity.object3D.visible = visible;
+    }
+  }
+});
+
+AFRAME.registerComponent("personal-space-bubble", {
+  schema: {
+    radius: { type: "number", default: 0.8 }
+  },
+  init() {
+    this.system.registerEntity(this.el);
+  },
+
+  remove() {
+    this.system.unregisterEntity(this.el);
+  }
+});