diff --git a/src/components/animated-robot-hands.js b/src/components/animated-robot-hands.js
deleted file mode 100644
index 1b26402848a8cb641560d76307708c4d77d53352..0000000000000000000000000000000000000000
--- a/src/components/animated-robot-hands.js
+++ /dev/null
@@ -1,94 +0,0 @@
-// Global THREE, AFRAME
-const POSES = {
-  open: "allOpen",
-  thumbDown: "thumbDown",
-  indexDown: "indexDown",
-  mrpDown: "mrpDown",
-  thumbUp: "thumbsUp",
-  point: "point",
-  fist: "allGrip",
-  pinch: "pinch"
-};
-
-// TODO: When we have analog values of index-finger triggers or middle-finger grips,
-//       it would be nice to animate the hands proportionally to those analog values.
-AFRAME.registerComponent("animated-robot-hands", {
-  dependencies: ["animation-mixer"],
-  schema: {
-    leftHand: { type: "selector", default: "#player-left-controller" },
-    rightHand: { type: "selector", default: "#player-right-controller" }
-  },
-
-  init: function() {
-    this.playAnimation = this.playAnimation.bind(this);
-
-    this.mixer = this.el.components["animation-mixer"].mixer;
-
-    const object3DMap = this.el.object3DMap;
-    const rootObj = object3DMap.mesh || object3DMap.scene;
-    this.clipActionObject = rootObj.parent;
-
-    // Set hands to open pose because the bind pose is funky dues
-    // to the workaround for FBX2glTF animations.
-    this.openL = this.mixer.clipAction(POSES.open + "_L", this.clipActionObject);
-    this.openR = this.mixer.clipAction(POSES.open + "_R", this.clipActionObject);
-    this.openL.play();
-    this.openR.play();
-  },
-
-  play: function() {
-    this.data.leftHand.addEventListener("hand-pose", this.playAnimation);
-    this.data.rightHand.addEventListener("hand-pose", this.playAnimation);
-  },
-
-  pause: function() {
-    this.data.leftHand.removeEventListener("hand-pose", this.playAnimation);
-    this.data.rightHand.removeEventListener("hand-pose", this.playAnimation);
-  },
-
-  // Animate from pose to pose.
-  // TODO: Transition from current pose (which may be BETWEEN two other poses)
-  //       to the target pose, rather than stopping previous actions altogether.
-  playAnimation: function(evt) {
-    const isLeft = evt.target === this.data.leftHand;
-    // Stop the initial animations we started when the model loaded.
-    if (!this.openLStopped && isLeft) {
-      this.openL.stop();
-      this.openLStopped = true;
-    } else if (!this.openRStopped && !isLeft) {
-      this.openR.stop();
-      this.openRStopped = true;
-    }
-
-    const { current, previous } = evt.detail;
-    const mixer = this.mixer;
-    const suffix = isLeft ? "_L" : "_R";
-    const prevPose = POSES[previous] + suffix;
-    const currPose = POSES[current] + suffix;
-
-    // STOP previous actions playing for this hand.
-    if (this["pose" + suffix + "_to"] !== undefined) {
-      this["pose" + suffix + "_to"].stop();
-    }
-    if (this["pose" + suffix + "_from"] !== undefined) {
-      this["pose" + suffix + "_from"].stop();
-    }
-
-    const duration = 0.065;
-    //    console.log(
-    //      `Animating ${isLeft ? "left" : "right"} hand from ${prevPose} to ${currPose} over ${duration} seconds.`
-    //    );
-    const from = mixer.clipAction(prevPose, this.clipActionObject);
-    const to = mixer.clipAction(currPose, this.clipActionObject);
-    from.fadeOut(duration);
-    to.fadeIn(duration);
-    to.play();
-    from.play();
-    // Update the mixer slightly to prevent one frame of the default pose
-    // from appearing. TODO: Find out why that happens
-    this.mixer.update(0.001);
-
-    this["pose" + suffix + "_to"] = to;
-    this["pose" + suffix + "_from"] = from;
-  }
-});
diff --git a/src/components/hand-poses.js b/src/components/hand-poses.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5df031698e2730eeb066300acab53c5c5e1a0cb
--- /dev/null
+++ b/src/components/hand-poses.js
@@ -0,0 +1,121 @@
+const POSES = {
+  open: "allOpen",
+  thumbDown: "thumbDown",
+  indexDown: "indexDown",
+  mrpDown: "mrpDown",
+  thumbUp: "thumbsUp",
+  point: "point",
+  fist: "allGrip",
+  pinch: "pinch"
+};
+
+AFRAME.registerComponent("hand-poses", {
+  schema: {
+    leftPose: { type: "string", default: "allOpen" },
+    rightPose: { type: "string", default: "allOpen" },
+    mixer: { type: "string" },
+    gltfEntity: { type: "string", default: "a-gltf-entity" }
+  },
+
+  init() {
+    this.animatePose = this.animatePose.bind(this);
+    this.animatePoses = this.animatePoses.bind(this);
+    this.mixer = this.el.querySelector(this.data.mixer).components["animation-mixer"];
+
+    let onLoad;
+    onLoad = () => {
+      console.log("loaded!"); // TODO: This isn't getting called
+      this.mixer = this.el.querySelector(this.data.mixer).components["animation-mixer"];
+      //     this.animatePoses();
+      this.el.querySelector(this.data.gltfEntity).removeEventListener("loaded", onLoad);
+    };
+    this.el.querySelector(this.data.gltfEntity).addEventListener("loaded", onLoad);
+  },
+
+  update(oldData) {
+    if (!this.mixer) return;
+    if (!this.clipActionObject) {
+      const object3DMap = this.mixer.el.object3DMap;
+      const rootObj = object3DMap.mesh || object3DMap.scene;
+      this.clipActionObject = rootObj.parent;
+    }
+    if (!oldData.leftPose) {
+      // first update
+      this.leftClipFrom = this.leftClipTo = this.mixer.mixer.clipAction(POSES.open + "_L", this.clipActionObject);
+      this.rightClipFrom = this.rightClipTo = this.mixer.mixer.clipAction(POSES.open + "_R", this.clipActionObject);
+      this.leftClipTo.play();
+      this.rightClipTo.play();
+    } else {
+      this.animatePoses(oldData);
+    }
+  },
+
+  animatePose(hand, prev, curr) {
+    this[`${hand}ClipFrom`].stop();
+    this[`${hand}ClipTo`].stop();
+
+    const duration = 0.065;
+    const suffix = hand == "left" ? "_L" : "_R";
+    const from = (this[`${hand}ClipFrom`] = this.mixer.mixer.clipAction(prev + suffix, this.clipActionObject));
+    const to = (this[`${hand}ClipTo`] = this.mixer.mixer.clipAction(curr + suffix, this.clipActionObject));
+
+    from.fadeOut(duration);
+    to.fadeIn(duration);
+    to.play();
+    from.play();
+
+    this.mixer.update(0.001);
+  },
+
+  animatePoses(oldData) {
+    if (oldData.leftPose != this.data.leftPose) {
+      this.animatePose("left", oldData.leftPose, this.data.leftPose);
+    }
+    if (oldData.rightPose != this.data.rightPose) {
+      this.animatePose("right", oldData.rightPose, this.data.rightPose);
+    }
+  }
+});
+
+//TODO: Should I use the previous pose?
+//      Argument against: The previous pose should already be given to
+//                        hand-poses update function, except in cases where
+//                        a remote user has updated their poses locally at
+//                        a faster rate than the network tick. In this case,
+//                        I don't know whether knowing the most-recent previous
+//                        pose is better than using the most-recently known pose.
+//                        In either case, the resulting animation seems fine, and
+//                        passing around only the most recent pose info saves
+//                        bandwidth and makes the code shorter.
+AFRAME.registerComponent("hand-poses-controller", {
+  schema: {
+    left: { type: "selector", default: "#player-left-controller" },
+    right: { type: "selector", default: "#player-right-controller" }
+  },
+
+  init: function() {
+    this.setHandPose = this.setHandPose.bind(this);
+
+    this.el.setAttribute("hand-poses", {
+      leftPose: POSES.open,
+      rightPose: POSES.open
+    });
+  },
+
+  play: function() {
+    this.data.left.addEventListener("hand-pose", this.setHandPose);
+    this.data.right.addEventListener("hand-pose", this.setHandPose);
+  },
+
+  pause: function() {
+    this.data.left.removeEventListener("hand-pose", this.setHandPose);
+    this.data.right.removeEventListener("hand-pose", this.setHandPose);
+  },
+
+  setHandPose: function(evt) {
+    const { current, previous } = evt.detail;
+    const isLeft = evt.target === this.data.left;
+    const pose = POSES[current];
+    this.el.setAttribute("hand-poses", `${isLeft ? "left" : "right"}Pose`, pose);
+  }
+});
diff --git a/src/network-schemas.js b/src/network-schemas.js
index 4632a5933807b1e97132c726fcc335afa6b68964..688ce7835c83cd67661e68472ee8f40c5b20a3a9 100644
--- a/src/network-schemas.js
+++ b/src/network-schemas.js
@@ -5,6 +5,14 @@ function registerNetworkSchemas() {
       "position",
       "rotation",
       "scale",
+      {
+        component: "hand-poses",
+        property: "leftPose"
+      },
+      {
+        component: "hand-poses",
+        property: "rightPose"
+      },
       {
         selector: ".camera",
         component: "position"
diff --git a/src/room.html b/src/room.html
index 52fd9ddd2a9ab2cdc4b54dba0f0093c55dee0d7f..47b7ad6654725184906b9f2dc8c78491bc9b82df 100644
--- a/src/room.html
+++ b/src/room.html
@@ -48,7 +48,7 @@
             </template>
 
             <template id="remote-avatar-template">
-                <a-entity ik-root>
+                <a-entity ik-root hand-poses="mixer: .RootScene">
                     <a-entity class="camera"></a-entity>
 
                     <a-entity class="left-controller"></a-entity>
@@ -57,7 +57,7 @@
 
                     <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
                         <template data-selector=".RootScene">
-                            <a-entity animation-mixer ></a-entity>
+                            <a-entity animation-mixer></a-entity>
                         </template>
 
                         <template data-selector=".Neck">
@@ -146,6 +146,8 @@
             spawn-controller="radius: 4;"
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
+            hand-poses="mixer: .RootScene"
+            hand-poses-controller
             ik-root
         >
             <a-entity
@@ -183,7 +185,7 @@
 
             <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
                 <template data-selector=".RootScene">
-                    <a-entity animation-mixer animated-robot-hands ></a-entity>
+                    <a-entity animation-mixer ></a-entity>
                 </template>
 
                 <template data-selector=".Neck">
diff --git a/src/room.js b/src/room.js
index b09ff896a09145e54bb76ea45f9f659606eaa022..354c601f1b7ea95430ba962e4123300cadb2a38c 100644
--- a/src/room.js
+++ b/src/room.js
@@ -35,10 +35,10 @@ import "./components/water";
 import "./components/skybox";
 import "./components/layers";
 import "./components/spawn-controller";
-import "./components/animated-robot-hands";
 import "./components/hide-when-quality";
 import "./components/animation-mixer";
 import "./components/loop-animation";
+import "./components/hand-poses";
 
 import ReactDOM from "react-dom";
 import React from "react";