diff --git a/src/components/hover-visuals.js b/src/components/hover-visuals.js
index 775f51dcab7b986e8446a2673caf3f06695fa905..0e34b061ed514baa8359ca5467f4f4a4b98c3da8 100644
--- a/src/components/hover-visuals.js
+++ b/src/components/hover-visuals.js
@@ -4,6 +4,10 @@
  * @component hover-visuals
  */
 AFRAME.registerComponent("hover-visuals", {
+  schema: {
+    hand: { type: "string" },
+    controller: { type: "selector" }
+  },
   init: function() {
     // uniforms are set from the component responsible for loading the mesh.
     this.uniforms = null;
@@ -12,15 +16,20 @@ AFRAME.registerComponent("hover-visuals", {
   remove() {
     this.interactorTransform = null;
   },
-  tick(time) {
+  tick() {
     if (!this.uniforms) return;
 
     this.el.object3D.matrixWorld.toArray(this.interactorTransform);
+    const hovering = this.data.controller.components["super-hands"].state.has("hover-start");
 
     for (const uniform of this.uniforms) {
-      uniform.hubs_HighlightInteractorOne.value = !!this.el.components["super-hands"].state.has("hover-start");
-      uniform.hubs_InteractorOneTransform.value = this.interactorTransform;
-      uniform.hubs_Time.value = time;
+      if (this.data.hand === "left") {
+        uniform.hubs_HighlightInteractorOne.value = hovering;
+        uniform.hubs_InteractorOneTransform.value = this.interactorTransform;
+      } else {
+        uniform.hubs_HighlightInteractorTwo.value = hovering;
+        uniform.hubs_InteractorTwoTransform.value = this.interactorTransform;
+      }
     }
   }
 });
diff --git a/src/components/hoverable-visuals.js b/src/components/hoverable-visuals.js
index 0728c95bd1825dbaf1dfd23b345c3c5ddf6d9a38..9f70e7b92d59e4350c2b18f8bc273c25c7a01108 100644
--- a/src/components/hoverable-visuals.js
+++ b/src/components/hoverable-visuals.js
@@ -8,12 +8,17 @@ AFRAME.registerComponent("hoverable-visuals", {
     cursorController: { type: "selector" }
   },
   init: function() {
-    // uniforms are set from the component responsible for loading the mesh.
+    // uniforms and boundingSphere are set from the component responsible for loading the mesh.
     this.uniforms = null;
+    this.boundingSphere = new THREE.Sphere();
+
     this.interactorOneTransform = [];
     this.interactorTwoTransform = [];
+    this.sweepParams = [];
   },
   remove() {
+    this.uniforms = null;
+    this.boundingBox = null;
     this.interactorOneTransform = null;
     this.interactorTwoTransform = null;
   },
@@ -42,7 +47,16 @@ AFRAME.registerComponent("hoverable-visuals", {
       interactorTwo.matrixWorld.toArray(this.interactorTwoTransform);
     }
 
+    if (interactorOne || interactorTwo) {
+      const worldY = this.el.object3D.matrixWorld.elements[13];
+      const scaledRadius = this.el.object3D.scale.y * this.boundingSphere.radius;
+      this.sweepParams[0] = worldY - scaledRadius;
+      this.sweepParams[1] = worldY + scaledRadius;
+    }
+
     for (const uniform of this.uniforms) {
+      uniform.hubs_EnableSweepingEffect.value = true;
+      uniform.hubs_SweepParams.value = this.sweepParams;
       uniform.hubs_HighlightInteractorOne.value = !!interactorOne;
       uniform.hubs_InteractorOneTransform.value = this.interactorOneTransform;
       uniform.hubs_HighlightInteractorTwo.value = !!interactorTwo;
diff --git a/src/components/media-loader.js b/src/components/media-loader.js
index aac0915769903d96776e49f8a0ab2dd67721f48a..6545ca9f7cdcdf09309c17cee6df4683a4d83be2 100644
--- a/src/components/media-loader.js
+++ b/src/components/media-loader.js
@@ -19,6 +19,8 @@ const fetchMaxContentIndex = url => {
   return fetch(url).then(r => parseInt(r.headers.get("x-max-content-index")));
 };
 
+const boundingBox = new THREE.Box3();
+
 AFRAME.registerComponent("media-loader", {
   schema: {
     src: { type: "string" },
@@ -102,13 +104,19 @@ AFRAME.registerComponent("media-loader", {
     delete this.showLoaderTimeout;
   },
 
-  onMediaLoaded() {
-    this.clearLoadingTimeout();
-    if (this.el.components["hoverable-visuals"]) {
-      this.el.components["hoverable-visuals"].uniforms = injectCustomShaderChunks(this.el.object3D);
+  setupHoverableVisuals() {
+    const hoverableVisuals = this.el.components["hoverable-visuals"];
+    if (hoverableVisuals) {
+      hoverableVisuals.uniforms = injectCustomShaderChunks(this.el.object3D);
+      boundingBox.setFromObject(this.el.object3DMap.mesh);
+      boundingBox.getBoundingSphere(hoverableVisuals.boundingSphere);
     }
   },
 
+  onMediaLoaded() {
+    this.setupHoverableVisuals();
+  },
+
   async update(oldData) {
     try {
       const { src } = this.data;
@@ -174,9 +182,10 @@ AFRAME.registerComponent("media-loader", {
         this.el.addEventListener(
           "model-loaded",
           () => {
-            this.onMediaLoaded();
+            this.clearLoadingTimeout();
             this.hasBakedShapes = !!(this.el.body && this.el.body.shapes.length > (this.shapeAdded ? 1 : 0));
             this.setShapeAndScale(this.data.resize);
+            this.setupHoverableVisuals();
             addAnimationComponents(this.el);
           },
           { once: true }
diff --git a/src/components/player-info.js b/src/components/player-info.js
index 612386b4182e22f98356c2f00894d90feecab9c1..7c5cbd89e1f97cd0a61cda24187d24a01858284e 100644
--- a/src/components/player-info.js
+++ b/src/components/player-info.js
@@ -35,8 +35,9 @@ AFRAME.registerComponent("player-info", {
       modelEl.setAttribute("gltf-model-plus", "src", this.data.avatarSrc);
     }
 
+    const uniforms = injectCustomShaderChunks(this.el.object3D);
     this.el.querySelectorAll("[hover-visuals]").forEach(el => {
-      el.components["hover-visuals"].uniforms = injectCustomShaderChunks(this.el.object3D);
+      el.components["hover-visuals"].uniforms = uniforms;
     });
   }
 });
diff --git a/src/hub.html b/src/hub.html
index 462512136f0cc2dc7c5d1d2755b91221cb6dbfe3..b3935f397b6d137f6f879796dd8ad8f3ffa6b438 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -377,7 +377,6 @@
                   missOpacity: 0.1;
                   curveShootingSpeed: 12;"
               haptic-feedback
-              hover-visuals
               body="type: static; shape: none;"
               mixin="controller-super-hands"
               controls-shape-offset
@@ -404,7 +403,6 @@
                   missOpacity: 0.1;
                   curveShootingSpeed: 12;"
               haptic-feedback
-              hover-visuals
               body="type: static; shape: none;"
               mixin="controller-super-hands"
               controls-shape-offset
@@ -438,11 +436,11 @@
             </template>
 
             <template data-name="LeftHand">
-              <a-entity bone-visibility></a-entity>
+              <a-entity bone-visibility hover-visuals="hand: left; controller: #player-left-controller"></a-entity>
             </template>
 
             <template data-name="RightHand">
-              <a-entity bone-visibility></a-entity>
+              <a-entity bone-visibility hover-visuals="hand: right; controller: #player-right-controller"></a-entity>
             </template>
 
           </a-entity>
diff --git a/src/utils/media-highlight-frag.glsl b/src/utils/media-highlight-frag.glsl
index e47db1ac09a41506fe2efb290cc169bcb0de7e05..c953d39ab48c834b3c3f669bff977b1214e2a853 100644
--- a/src/utils/media-highlight-frag.glsl
+++ b/src/utils/media-highlight-frag.glsl
@@ -15,24 +15,22 @@ if (hubs_HighlightInteractorOne || hubs_HighlightInteractorTwo) {
     dist2 = distance(hubs_WorldPosition, ip);
   }
 
+  float size = hubs_SweepParams.t - hubs_SweepParams.s;
+  float line = mod(hubs_Time / 3000.0 * size, size * 2.0) + hubs_SweepParams.s - size / 2.0;
+
   float ratio = 0.0;
+  if (hubs_EnableSweepingEffect && hubs_WorldPosition.y < line) {
+    // Highlight with an sweeping gradient
+    ratio = max(0.0, 1.0 - (line - hubs_WorldPosition.y) / size * 3.0);
+  }
+
   float pulse = sin(hubs_Time / 1000.0) + 1.0;
-  float spacing = 0.5;
-  float line = spacing * pulse - spacing / 2.0;
-  float lineWidth= 0.01;
-  float mody = mod(hubs_WorldPosition.y, spacing);
-
-  if (-lineWidth + line < mody && mody < lineWidth + line) {
-    // Highlight with an animated line effect
-    ratio = 0.5;
-  } else {
-    // Highlight with a gradient falling off with distance.
-    if (hubs_HighlightInteractorOne) {
-      ratio = -min(1.0, pow(dist1 * (9.0 + 3.0 * pulse), 3.0)) + 1.0;
-    } 
-    if (hubs_HighlightInteractorTwo) {
-      ratio += -min(1.0, pow(dist2 * (9.0 + 3.0 * pulse), 3.0)) + 1.0;
-    }
+  // Highlight with a gradient falling off with distance.
+  if (hubs_HighlightInteractorOne) {
+    ratio += -min(1.0, pow(dist1 * (9.0 + 3.0 * pulse), 3.0)) + 1.0;
+  } 
+  if (hubs_HighlightInteractorTwo) {
+    ratio += -min(1.0, pow(dist2 * (9.0 + 3.0 * pulse), 3.0)) + 1.0;
   }
 
   ratio = min(1.0, ratio);
diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js
index c192c11107af1ada6bf184e164d9981c221bbcbc..7db272e7e61a07e067dd063aef29f06d71bc2ecb 100644
--- a/src/utils/media-utils.js
+++ b/src/utils/media-utils.js
@@ -140,7 +140,7 @@ export const addMedia = (src, template, contentOrigin, resolve = false, resize =
 };
 
 export function injectCustomShaderChunks(obj) {
-  const vertexRegex = /\bbegin_vertex\b/;
+  const vertexRegex = /\bskinning_vertex\b/;
   const fragRegex = /\bgl_FragColor\b/;
 
   const materialsSeen = new Set();
@@ -157,6 +157,8 @@ export function injectCustomShaderChunks(obj) {
       shader.uniforms.hubs_InteractorOneTransform = { value: [] };
       shader.uniforms.hubs_InteractorTwoTransform = { value: [] };
       shader.uniforms.hubs_InteractorTwoPos = { value: [] };
+      shader.uniforms.hubs_EnableSweepingEffect = { value: false };
+      shader.uniforms.hubs_SweepParams = { value: [] };
       shader.uniforms.hubs_HighlightInteractorOne = { value: false };
       shader.uniforms.hubs_HighlightInteractorTwo = { value: false };
       shader.uniforms.hubs_Time = { value: 0 };
@@ -182,6 +184,8 @@ export function injectCustomShaderChunks(obj) {
       const findex = flines.findIndex(line => fragRegex.test(line));
       flines.splice(findex + 1, 0, mediaHighlightFrag);
       flines.unshift("varying vec3 hubs_WorldPosition;");
+      flines.unshift("uniform bool hubs_EnableSweepingEffect;");
+      flines.unshift("uniform vec2 hubs_SweepParams;");
       flines.unshift("uniform bool hubs_HighlightInteractorOne;");
       flines.unshift("uniform mat4 hubs_InteractorOneTransform;");
       flines.unshift("uniform bool hubs_HighlightInteractorTwo;");