diff --git a/interface/resources/config/keyboard.json b/interface/resources/config/keyboard.json
index b3688ef06e..e16c6156ab 100644
--- a/interface/resources/config/keyboard.json
+++ b/interface/resources/config/keyboard.json
@@ -1,4 +1,22 @@
 {
+    "backPlate": {
+        "dimensions": {
+            "x": 0.723600000888109207,
+            "y": 0.022600000724196434,
+            "z": 0.2474999976158142
+        },
+        "position": {
+            "x": -0.3292800903320312,
+            "y": 0.004300000742077827,
+            "z": -0.055427663803100586
+        },
+        "rotation": {
+            "w": 1.000,
+            "x": 0.000,
+            "y": 0.000,
+            "z": 0.000
+        }
+    },
     "anchor": {
         "dimensions": {
             "x": 0.023600000888109207,
diff --git a/interface/resources/qml/dialogs/PreferencesDialog.qml b/interface/resources/qml/dialogs/PreferencesDialog.qml
index 9df1d0b963..63fde5ec64 100644
--- a/interface/resources/qml/dialogs/PreferencesDialog.qml
+++ b/interface/resources/qml/dialogs/PreferencesDialog.qml
@@ -70,6 +70,15 @@ ScrollingWindow {
                 }
             }
 
+            var useKeyboardPreference = findPreference("User Interface", "Use Virtual Keyboard");
+            var keyboardInputPreference = findPreference("User Interface", "Keyboard laser / mallets");
+            if (useKeyboardPreference && keyboardInputPreference) {
+                keyboardInputPreference.visible = useKeyboardPreference.value;
+                useKeyboardPreference.valueChanged.connect(function() {
+                    keyboardInputPreference.visible = useKeyboardPreference.value;
+                });
+            }
+
             if (sections.length) {
                 // Default sections to expanded/collapsed as appropriate for dialog.
                 if (sections.length === 1) {
@@ -112,4 +121,32 @@ ScrollingWindow {
             onClicked: root.restoreAll()
         }
     }
+
+    function findPreference(category, name) {
+        var section = null;
+        var preference = null;
+        var i;
+
+        // Find category section.
+        i = 0;
+        while (!section && i < sections.length) {
+            if (sections[i].name === category) {
+                section = sections[i];
+            }
+            i++;
+        }
+
+        // Find named preference.
+        if (section) {
+            i = 0;
+            while (!preference && i < section.preferences.length) {
+                if (section.preferences[i].preference && section.preferences[i].preference.name === name) {
+                    preference = section.preferences[i];
+                }
+                i++;
+            }
+        }
+
+        return preference;
+    }
 }
diff --git a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml
index f6f840bbe8..0791a491ff 100644
--- a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml
+++ b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml
@@ -16,9 +16,10 @@ import controlsUit 1.0
 Preference {
     id: root
     height: spacer.height + Math.max(hifi.dimensions.controlLineHeight, checkBox.implicitHeight)
-
+    property bool value: false
     Component.onCompleted: {
         checkBox.checked = preference.value;
+        value = checkBox.checked;
         preference.value = Qt.binding(function(){ return checkBox.checked; });
     }
 
@@ -47,6 +48,7 @@ Preference {
 
         onClicked: {
             Tablet.playSound(TabletEnums.ButtonClick);
+            value = checked;
         }
 
         anchors {
diff --git a/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml b/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml
index 0a09d8d609..1e7d92a138 100644
--- a/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml
+++ b/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml
@@ -20,6 +20,11 @@ Preference {
 
     property int value: 0
 
+    readonly property int visibleBottomPadding: 3
+    readonly property int invisibleBottomPadding: 0
+    readonly property int indentLeftMargin: 20
+    readonly property int nonindentLeftMargin: 0
+
     Component.onCompleted: {
         value = preference.value;
         repeater.itemAt(preference.value).checked = true;
@@ -46,24 +51,24 @@ Preference {
         preference.save();
     }
 
+    RalewaySemiBold {
+        id: heading
+        size: hifi.fontSizes.inputLabel
+        text: preference.heading
+        color: hifi.colors.lightGrayText
+        visible: text !== ""
+        bottomPadding: heading.visible ? visibleBottomPadding : invisibleBottomPadding
+    }
+
     Column {
         id: control
         anchors {
             left: parent.left
             right: parent.right
-            bottom: parent.bottom
+            top: heading.visible ? heading.bottom : heading.top
+            leftMargin: preference.indented ? indentLeftMargin : nonindentLeftMargin
         }
         spacing: 3
-
-        RalewaySemiBold {
-            id: heading
-            size: hifi.fontSizes.inputLabel
-            text: preference.heading
-            color: hifi.colors.lightGrayText
-            visible: text !== ""
-            bottomPadding: 3
-        }
-
         Repeater {
             id: repeater
             model: preference.items.length
diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml
index a5d7b23df6..d526c9a3cd 100644
--- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml
+++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml
@@ -138,6 +138,15 @@ Item {
                         }
                     }
 
+                    var useKeyboardPreference = findPreference("User Interface", "Use Virtual Keyboard");
+                    var keyboardInputPreference = findPreference("User Interface", "Keyboard laser / mallets");
+                    if (useKeyboardPreference && keyboardInputPreference) {
+                        keyboardInputPreference.visible = useKeyboardPreference.value;
+                        useKeyboardPreference.valueChanged.connect(function() {
+                            keyboardInputPreference.visible = useKeyboardPreference.value;
+                        });
+                    }
+
                     if (sections.length) {
                         // Default sections to expanded/collapsed as appropriate for dialog.
                         if (sections.length === 1) {
diff --git a/interface/resources/sounds/keySound.mp3 b/interface/resources/sounds/keySound.mp3
new file mode 100644
index 0000000000..87c7b00807
Binary files /dev/null and b/interface/resources/sounds/keySound.mp3 differ
diff --git a/interface/src/scripting/KeyboardScriptingInterface.cpp b/interface/src/scripting/KeyboardScriptingInterface.cpp
index d86bb56e64..ccf123efed 100644
--- a/interface/src/scripting/KeyboardScriptingInterface.cpp
+++ b/interface/src/scripting/KeyboardScriptingInterface.cpp
@@ -12,7 +12,7 @@
 #include "KeyboardScriptingInterface.h"
 #include "ui/Keyboard.h"
 
-bool KeyboardScriptingInterface::isRaised() {
+bool KeyboardScriptingInterface::isRaised() const {
     return DependencyManager::get<Keyboard>()->isRaised();
 }
 
@@ -20,8 +20,7 @@ void KeyboardScriptingInterface::setRaised(bool raised) {
     DependencyManager::get<Keyboard>()->setRaised(raised);
 }
 
-
-bool KeyboardScriptingInterface::isPassword() {
+bool KeyboardScriptingInterface::isPassword() const {
     return DependencyManager::get<Keyboard>()->isPassword();
 }
 
@@ -33,6 +32,38 @@ void KeyboardScriptingInterface::loadKeyboardFile(const QString& keyboardFile) {
     DependencyManager::get<Keyboard>()->loadKeyboardFile(keyboardFile);
 }
 
-bool KeyboardScriptingInterface::getUse3DKeyboard() {
+bool KeyboardScriptingInterface::getUse3DKeyboard() const {
     return DependencyManager::get<Keyboard>()->getUse3DKeyboard();
 }
+
+void KeyboardScriptingInterface::disableRightMallet() {
+    DependencyManager::get<Keyboard>()->disableRightMallet();
+}
+
+void KeyboardScriptingInterface::disableLeftMallet() {
+    DependencyManager::get<Keyboard>()->disableLeftMallet();
+}
+
+void KeyboardScriptingInterface::enableRightMallet() {
+    DependencyManager::get<Keyboard>()->enableRightMallet();
+}
+
+void KeyboardScriptingInterface::enableLeftMallet() {
+    DependencyManager::get<Keyboard>()->enableLeftMallet();
+}
+
+void KeyboardScriptingInterface::setLeftHandLaser(unsigned int leftHandLaser) {
+    DependencyManager::get<Keyboard>()->setLeftHandLaser(leftHandLaser);
+}
+
+void KeyboardScriptingInterface::setRightHandLaser(unsigned int rightHandLaser) {
+    DependencyManager::get<Keyboard>()->setRightHandLaser(rightHandLaser);
+}
+
+bool KeyboardScriptingInterface::getPreferMalletsOverLasers() const {
+    return DependencyManager::get<Keyboard>()->getPreferMalletsOverLasers();
+}
+
+bool KeyboardScriptingInterface::containsID(OverlayID overlayID) const {
+    return DependencyManager::get<Keyboard>()->containsID(overlayID);
+}
diff --git a/interface/src/scripting/KeyboardScriptingInterface.h b/interface/src/scripting/KeyboardScriptingInterface.h
index 709dfe01de..52d9e821bb 100644
--- a/interface/src/scripting/KeyboardScriptingInterface.h
+++ b/interface/src/scripting/KeyboardScriptingInterface.h
@@ -15,6 +15,7 @@
 #include <QtCore/QObject>
 
 #include "DependencyManager.h"
+#include "ui/overlays/Overlay.h"
 
 /**jsdoc
  * The Keyboard API provides facilities to use 3D Physical keyboard.
@@ -26,21 +27,33 @@
  * @property {bool} raised - <code>true</code> If the keyboard is visible <code>false</code> otherwise
  * @property {bool} password - <code>true</code> Will show * instead of characters in the text display <code>false</code> otherwise
  */
+
 class KeyboardScriptingInterface : public QObject, public Dependency {
     Q_OBJECT
     Q_PROPERTY(bool raised READ isRaised WRITE setRaised)
     Q_PROPERTY(bool password READ isPassword WRITE setPassword)
-    Q_PROPERTY(bool use3DKeyboard READ getUse3DKeyboard);
+    Q_PROPERTY(bool use3DKeyboard READ getUse3DKeyboard CONSTANT);
+    Q_PROPERTY(bool preferMalletsOverLasers READ getPreferMalletsOverLasers CONSTANT)
 
 public:
+    KeyboardScriptingInterface() = default;
+    ~KeyboardScriptingInterface() = default;
     Q_INVOKABLE void loadKeyboardFile(const QString& string);
+    Q_INVOKABLE void enableLeftMallet();
+    Q_INVOKABLE void enableRightMallet();
+    Q_INVOKABLE void disableLeftMallet();
+    Q_INVOKABLE void disableRightMallet();
+    Q_INVOKABLE void setLeftHandLaser(unsigned int leftHandLaser);
+    Q_INVOKABLE void setRightHandLaser(unsigned int rightHandLaser);
+    Q_INVOKABLE bool containsID(OverlayID overlayID) const;
 private:
-    bool isRaised();
+    bool getPreferMalletsOverLasers() const;
+    bool isRaised() const;
     void setRaised(bool raised);
 
-    bool isPassword();
+    bool isPassword() const;
     void setPassword(bool password);
 
-    bool getUse3DKeyboard();
+    bool getUse3DKeyboard() const;
 };
 #endif
diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp
index d647851a80..40894f1121 100644
--- a/interface/src/ui/Keyboard.cpp
+++ b/interface/src/ui/Keyboard.cpp
@@ -349,6 +349,12 @@ void Keyboard::raiseKeyboardAnchor(bool raise) const {
         };
 
         overlays.editOverlay(_textDisplay.overlayID, textDisplayProperties);
+
+        auto backPlateOverlay = std::dynamic_pointer_cast<Cube3DOverlay>(overlays.getOverlay(_backPlate.overlayID));
+
+        if (backPlateOverlay) {
+            backPlateOverlay->setVisible(raise);
+        }
     }
 }
 
@@ -380,6 +386,17 @@ void Keyboard::scaleKeyboard(float sensorToWorldScale) {
     };
 
     overlays.editOverlay(_textDisplay.overlayID, textDisplayProperties);
+
+
+    glm::vec3 backPlateScaledDimensions = _backPlate.dimensions * sensorToWorldScale;
+    glm::vec3 backPlateScaledLocalPosition = _backPlate.localPosition * sensorToWorldScale;
+
+    QVariantMap backPlateProperties {
+        { "localPosition", vec3toVariant(backPlateScaledLocalPosition) },
+        { "dimensions", vec3toVariant(backPlateScaledDimensions) }
+    };
+
+    overlays.editOverlay(_backPlate.overlayID, backPlateProperties);
 }
 
 void Keyboard::startLayerSwitchTimer() {
@@ -425,6 +442,18 @@ void Keyboard::setPassword(bool password) {
     updateTextDisplay();
 }
 
+void Keyboard::setPreferMalletsOverLasers(bool preferMalletsOverLasers) {
+    _preferMalletsOverLasersSettingLock.withWriteLock([&] {
+        _preferMalletsOverLasers.set(preferMalletsOverLasers);
+    });
+}
+
+bool Keyboard::getPreferMalletsOverLasers() const {
+    return _preferMalletsOverLasersSettingLock.resultWithReadLock<bool>([&] {
+        return _preferMalletsOverLasers.get();
+    });
+}
+
 void Keyboard::switchToLayer(int layerIndex) {
     if (layerIndex >= 0 && layerIndex < (int)_keyboardLayers.size()) {
         Overlays& overlays = qApp->getOverlays();
@@ -459,15 +488,22 @@ void Keyboard::switchToLayer(int layerIndex) {
     }
 }
 
+bool Keyboard::shouldProcessPointerEvent(const PointerEvent& event) const {
+    bool preferMalletsOverLasers = getPreferMalletsOverLasers();
+    unsigned int pointerID = event.getID();
+    bool isStylusEvent = (pointerID == _leftHandStylus || pointerID == _rightHandStylus);
+    bool isLaserEvent = (pointerID == _leftHandLaser || pointerID == _rightHandLaser);
+    qDebug() << isLaserEvent;
+    return ((isStylusEvent && preferMalletsOverLasers) || (isLaserEvent && !preferMalletsOverLasers));
+}
+
 void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent& event) {
-    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) {
+    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished() || overlayID == _backPlate.overlayID) {
         return;
     }
 
-    auto pointerID = event.getID();
     auto buttonType = event.getButton();
-
-    if ((pointerID != _leftHandStylus && pointerID != _rightHandStylus) || buttonType != PointerEvent::PrimaryButton) {
+    if (!shouldProcessPointerEvent(event) || buttonType != PointerEvent::PrimaryButton) {
         return;
     }
 
@@ -481,8 +517,10 @@ void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent
     Key& key = search.value();
 
     if (key.timerFinished()) {
+        unsigned int pointerID = event.getID();
+        auto handIndex = (pointerID == _leftHandStylus || pointerID == _leftHandLaser)
+            ? controller::Hand::LEFT : controller::Hand::RIGHT;
 
-        auto handIndex = (pointerID == _leftHandStylus) ? controller::Hand::LEFT : controller::Hand::RIGHT;
         auto userInputMapper = DependencyManager::get<UserInputMapper>();
         userInputMapper->triggerHapticPulse(PULSE_STRENGTH, PULSE_DURATION, handIndex);
 
@@ -550,19 +588,32 @@ void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent
         QCoreApplication::postEvent(QCoreApplication::instance(), pressEvent);
         QCoreApplication::postEvent(QCoreApplication::instance(), releaseEvent);
 
-        key.startTimer(KEY_PRESS_TIMEOUT_MS);
+        if (!getPreferMalletsOverLasers()) {
+            key.startTimer(KEY_PRESS_TIMEOUT_MS);
+        }
         auto selection = DependencyManager::get<SelectionScriptingInterface>();
         selection->addToSelectedItemsList(KEY_PRESSED_HIGHLIGHT, "overlay", overlayID);
     }
 }
 
+void Keyboard::setLeftHandLaser(unsigned int leftHandLaser) {
+    _handLaserLock.withWriteLock([&] {
+        _leftHandLaser = leftHandLaser;
+    });
+}
+
+void Keyboard::setRightHandLaser(unsigned int rightHandLaser) {
+    _handLaserLock.withWriteLock([&] {
+        _rightHandLaser = rightHandLaser;
+    });
+}
+
 void Keyboard::handleTriggerEnd(const OverlayID& overlayID, const PointerEvent& event) {
-    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) {
+    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished() || overlayID == _backPlate.overlayID) {
         return;
     }
 
-    auto pointerID = event.getID();
-    if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) {
+    if (!shouldProcessPointerEvent(event)) {
         return;
     }
 
@@ -582,7 +633,7 @@ void Keyboard::handleTriggerEnd(const OverlayID& overlayID, const PointerEvent&
     }
 
     key.setIsPressed(false);
-    if (key.timerFinished()) {
+    if (key.timerFinished() && getPreferMalletsOverLasers()) {
         key.startTimer(KEY_PRESS_TIMEOUT_MS);
     }
 
@@ -591,13 +642,11 @@ void Keyboard::handleTriggerEnd(const OverlayID& overlayID, const PointerEvent&
 }
 
 void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEvent& event) {
-    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) {
+    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished() || overlayID == _backPlate.overlayID) {
         return;
     }
 
-    auto pointerID = event.getID();
-
-    if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) {
+    if (!shouldProcessPointerEvent(event)) {
         return;
     }
 
@@ -611,10 +660,11 @@ void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEv
     Key& key = search.value();
     Overlays& overlays = qApp->getOverlays();
 
-    if (!key.isPressed()) {
+    if (!key.isPressed() && getPreferMalletsOverLasers()) {
         auto base3DOverlay = std::dynamic_pointer_cast<Base3DOverlay>(overlays.getOverlay(overlayID));
 
         if (base3DOverlay) {
+            unsigned int pointerID = event.getID();
             auto pointerManager = DependencyManager::get<PointerManager>();
             auto pickResult = pointerManager->getPrevPickResult(pointerID);
             auto stylusPickResult = std::dynamic_pointer_cast<StylusPickResult>(pickResult);
@@ -635,13 +685,11 @@ void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEv
 }
 
 void Keyboard::handleHoverBegin(const OverlayID& overlayID, const PointerEvent& event) {
-    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) {
+    if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished() || overlayID == _backPlate.overlayID) {
         return;
     }
 
-    auto pointerID = event.getID();
-
-    if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) {
+    if (!shouldProcessPointerEvent(event)) {
         return;
     }
 
@@ -657,13 +705,11 @@ void Keyboard::handleHoverBegin(const OverlayID& overlayID, const PointerEvent&
 }
 
 void Keyboard::handleHoverEnd(const OverlayID& overlayID, const PointerEvent& event) {
-      if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) {
+      if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished() || overlayID == _backPlate.overlayID) {
         return;
     }
 
-    auto pointerID = event.getID();
-
-    if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) {
+    if (!shouldProcessPointerEvent(event)) {
         return;
     }
 
@@ -755,6 +801,27 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) {
         anchor.originalDimensions = dimensions;
         _anchor = anchor;
 
+        QJsonObject backPlateObject = jsonObject["backPlate"].toObject();
+
+        QVariantMap backPlateProperties {
+            { "name", "backPlate"},
+            { "isSolid", true },
+            { "visible", true },
+            { "grabbable", false },
+            { "alpha", 0.0 },
+            { "ignoreRayIntersection", false},
+            { "dimensions", backPlateObject["dimensions"].toVariant() },
+            { "position", backPlateObject["position"].toVariant() },
+            { "orientation", backPlateObject["rotation"].toVariant() },
+            { "parentID", _anchor.overlayID }
+        };
+
+        BackPlate backPlate;
+        backPlate.overlayID = overlays.addOverlay("cube", backPlateProperties);
+        backPlate.dimensions = vec3FromVariant(backPlateObject["dimensions"].toVariant());
+        backPlate.localPosition = vec3FromVariant(overlays.getProperty(backPlate.overlayID, "localPosition").value);
+        _backPlate = backPlate;
+
         const QJsonArray& keyboardLayers = jsonObject["layers"].toArray();
         int keyboardLayerCount = keyboardLayers.size();
         _keyboardLayers.reserve(keyboardLayerCount);
@@ -878,6 +945,7 @@ void Keyboard::clearKeyboardKeys() {
 
     overlays.deleteOverlay(_anchor.overlayID);
     overlays.deleteOverlay(_textDisplay.overlayID);
+    overlays.deleteOverlay(_backPlate.overlayID);
 
     _keyboardLayers.clear();
 
@@ -887,10 +955,42 @@ void Keyboard::clearKeyboardKeys() {
 }
 
 void Keyboard::enableStylus() {
-    auto pointerManager = DependencyManager::get<PointerManager>();
-    pointerManager->setRenderState(_leftHandStylus, "events on");
-    pointerManager->enablePointer(_leftHandStylus);
-    pointerManager->setRenderState(_rightHandStylus, "events on");
-    pointerManager->enablePointer(_rightHandStylus);
+    if (getPreferMalletsOverLasers()) {
+        auto pointerManager = DependencyManager::get<PointerManager>();
+        pointerManager->setRenderState(_leftHandStylus, "events on");
+        pointerManager->enablePointer(_leftHandStylus);
+        pointerManager->setRenderState(_rightHandStylus, "events on");
+        pointerManager->enablePointer(_rightHandStylus);
+    }
 
 }
+
+void Keyboard::enableRightMallet() {
+    auto pointerManager = DependencyManager::get<PointerManager>();
+    pointerManager->setRenderState(_rightHandStylus, "events on");
+    pointerManager->enablePointer(_rightHandStylus);
+}
+
+void Keyboard::enableLeftMallet() {
+     auto pointerManager = DependencyManager::get<PointerManager>();
+     pointerManager->setRenderState(_leftHandStylus, "events on");
+     pointerManager->enablePointer(_leftHandStylus);
+}
+
+void Keyboard::disableLeftMallet() {
+    auto pointerManager = DependencyManager::get<PointerManager>();
+    pointerManager->setRenderState(_leftHandStylus, "events off");
+    pointerManager->disablePointer(_leftHandStylus);
+}
+
+void Keyboard::disableRightMallet() {
+    auto pointerManager = DependencyManager::get<PointerManager>();
+    pointerManager->setRenderState(_rightHandStylus, "events off");
+    pointerManager->disablePointer(_rightHandStylus);
+}
+
+bool Keyboard::containsID(OverlayID overlayID) const {
+    return resultWithReadLock<bool>([&] {
+        return _itemsToIgnore.contains(overlayID) || _backPlate.overlayID == overlayID;
+    });
+}
diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h
index 9c0c8c40f2..b917b60eb4 100644
--- a/interface/src/ui/Keyboard.h
+++ b/interface/src/ui/Keyboard.h
@@ -87,7 +87,8 @@ private:
     std::shared_ptr<QTimer> _timer { std::make_shared<QTimer>() };
 };
 
-class Keyboard : public Dependency, public QObject, public ReadWriteLockable {
+class Keyboard : public QObject, public Dependency, public ReadWriteLockable {
+    Q_OBJECT
 public:
     Keyboard();
     void createKeyboard();
@@ -97,9 +98,20 @@ public:
 
     bool isPassword() const;
     void setPassword(bool password);
+    void enableRightMallet();
+    void enableLeftMallet();
+    void disableRightMallet();
+    void disableLeftMallet();
+
+    void setLeftHandLaser(unsigned int leftHandLaser);
+    void setRightHandLaser(unsigned int rightHandLaser);
+
+    void setPreferMalletsOverLasers(bool preferMalletsOverLasers);
+    bool getPreferMalletsOverLasers() const;
 
     bool getUse3DKeyboard() const;
     void setUse3DKeyboard(bool use);
+    bool containsID(OverlayID overlayID) const;
 
     void loadKeyboardFile(const QString& keyboardFile);
     QVector<OverlayID> getKeysID();
@@ -118,6 +130,12 @@ private:
         glm::vec3 originalDimensions;
     };
 
+    struct BackPlate {
+        OverlayID overlayID;
+        glm::vec3 dimensions;
+        glm::vec3 localPosition;
+    };
+
     struct TextDisplay {
         float lineHeight;
         OverlayID overlayID;
@@ -127,10 +145,10 @@ private:
 
     void raiseKeyboard(bool raise) const;
     void raiseKeyboardAnchor(bool raise) const;
-
-    void setLayerIndex(int layerIndex);
     void enableStylus();
     void disableStylus();
+
+    void setLayerIndex(int layerIndex);
     void clearKeyboardKeys();
     void switchToLayer(int layerIndex);
     void updateTextDisplay();
@@ -138,23 +156,31 @@ private:
     void startLayerSwitchTimer();
     bool isLayerSwitchTimerFinished();
 
+    bool shouldProcessPointerEvent(const PointerEvent& event) const;
+
     bool _raised { false };
     bool _password { false };
     bool _capsEnabled { false };
     int _layerIndex { 0 };
+    Setting::Handle<bool> _preferMalletsOverLasers { "preferMalletsOverLaser", true };
     unsigned int _leftHandStylus { 0 };
     unsigned int _rightHandStylus { 0 };
+    unsigned int _leftHandLaser { 0 };
+    unsigned int _rightHandLaser { 0 };
     SharedSoundPointer _keySound { nullptr };
     std::shared_ptr<QTimer> _layerSwitchTimer { std::make_shared<QTimer>() };
 
     mutable ReadWriteLockable _use3DKeyboardLock;
+    mutable ReadWriteLockable _handLaserLock;
+    mutable ReadWriteLockable _preferMalletsOverLasersSettingLock;
+    mutable ReadWriteLockable _ignoreItemsLock;
     Setting::Handle<bool> _use3DKeyboard { "use3DKeyboard", true };
 
     QString _typedCharacters;
     TextDisplay _textDisplay;
     Anchor _anchor;
+    BackPlate _backPlate;
 
-    mutable ReadWriteLockable _ignoreItemsLock;
     QVector<OverlayID> _itemsToIgnore;
     std::vector<QHash<OverlayID, Key>> _keyboardLayers;
 };
diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp
index d1fbe02759..5bb1945c64 100644
--- a/interface/src/ui/PreferencesDialog.cpp
+++ b/interface/src/ui/PreferencesDialog.cpp
@@ -107,12 +107,6 @@ void setupPreferences() {
         preferences->addPreference(preference);
     }
 
-    {
-        auto getter = []()->bool { return qApp->getPreferStylusOverLaser(); };
-        auto setter = [](bool value) { qApp->setPreferStylusOverLaser(value); };
-        preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Stylus Over Laser", getter, setter));
-    }
-
     {
         static const QString RETICLE_ICON_NAME = { Cursor::Manager::getIconName(Cursor::Icon::RETICLE) };
         auto getter = []()->bool { return qApp->getPreferredCursor() == RETICLE_ICON_NAME; };
@@ -121,15 +115,38 @@ void setupPreferences() {
     }
 
     {
-        auto getter = []()->bool { return DependencyManager::get<Keyboard>()->getUse3DKeyboard(); };
-        auto setter = [](bool value) { DependencyManager::get<Keyboard>()->setUse3DKeyboard(value); };
+        auto getter = []()->bool { return qApp->getMiniTabletEnabled(); };
+        auto setter = [](bool value) { qApp->setMiniTabletEnabled(value); };
+        preferences->addPreference(new CheckPreference(UI_CATEGORY, "Use mini tablet", getter, setter));
+    }
+
+    {
+        auto getter = []()->int { return DependencyManager::get<Keyboard>()->getUse3DKeyboard(); };
+        auto setter = [](int value) { DependencyManager::get<Keyboard>()->setUse3DKeyboard(value); };
         preferences->addPreference(new CheckPreference(UI_CATEGORY, "Use Virtual Keyboard", getter, setter));
     }
 
     {
-        auto getter = []()->bool { return qApp->getMiniTabletEnabled(); };
-        auto setter = [](bool value) { qApp->setMiniTabletEnabled(value); };
-        preferences->addPreference(new CheckPreference(UI_CATEGORY, "Use mini tablet", getter, setter));
+        auto getter = []()->bool { return DependencyManager::get<Keyboard>()->getPreferMalletsOverLasers() ? 1 : 0; };
+        auto setter = [](bool value) { return DependencyManager::get<Keyboard>()->setPreferMalletsOverLasers((bool)value); };
+        auto preference = new RadioButtonsPreference(UI_CATEGORY, "Keyboard laser / mallets", getter, setter);
+        QStringList items;
+        items << "Lasers" << "Mallets";
+        preference->setItems(items);
+        preference->setIndented(true);
+        preferences->addPreference(preference);
+    }
+
+
+    {
+        auto getter = []()->int { return qApp->getPreferStylusOverLaser() ? 1 : 0; };
+        auto setter = [](int value) { qApp->setPreferStylusOverLaser((bool)value); };
+        auto preference = new RadioButtonsPreference(UI_CATEGORY, "Tablet stylys / laser", getter, setter);
+        QStringList items;
+        items << "Lasers" << "Stylus";
+        preference->setHeading("Tablet Input Mechanism");
+        preference->setItems(items);
+        preferences->addPreference(preference);
     }
 
     static const QString VIEW_CATEGORY{ "View" };
@@ -151,15 +168,14 @@ void setupPreferences() {
         preferences->addPreference(preference);
     }
 
-
-    // FIXME: Remove setting completely or make available through JavaScript API?
     /*
+    // FIXME: Remove setting completely or make available through JavaScript API?
     {
         auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); };
         auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); };
         preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter));
-    }
-    */
+        }*/
+
     // Snapshots
     static const QString SNAPSHOTS { "Snapshots" };
     {
diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp
index 19bdfce2b3..805832760e 100644
--- a/interface/src/ui/overlays/ModelOverlay.cpp
+++ b/interface/src/ui/overlays/ModelOverlay.cpp
@@ -556,6 +556,7 @@ void ModelOverlay::locationChanged(bool tellPhysics) {
     if (_model && _model->isActive()) {
         _model->setRotation(getWorldOrientation());
         _model->setTranslation(getWorldPosition());
+        _updateModel = true;
     }
 }
 
diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp
index 17b0895f47..754c8d26a9 100644
--- a/interface/src/ui/overlays/Overlays.cpp
+++ b/interface/src/ui/overlays/Overlays.cpp
@@ -539,7 +539,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionVector(const PickRay
     bool bestIsFront = false;
     bool bestIsTablet = false;
     auto tabletIDs = qApp->getTabletIDs();
-    const QVector<OverlayID> keyboardKeysToDiscard = DependencyManager::get<Keyboard>()->getKeysID();
+
     QMutexLocker locker(&_mutex);
     RayToOverlayIntersectionResult result;
     QMapIterator<OverlayID, Overlay::Pointer> i(_overlaysWorld);
@@ -549,8 +549,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionVector(const PickRay
         auto thisOverlay = std::dynamic_pointer_cast<Base3DOverlay>(i.value());
 
         if ((overlaysToDiscard.size() > 0 && overlaysToDiscard.contains(thisID)) ||
-            (overlaysToInclude.size() > 0 && !overlaysToInclude.contains(thisID)) ||
-            (keyboardKeysToDiscard.size() > 0 && keyboardKeysToDiscard.contains(thisID))) {
+            (overlaysToInclude.size() > 0 && !overlaysToInclude.contains(thisID))) {
             continue;
         }
 
diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h
index 4a58d71c54..0fec8708e8 100644
--- a/libraries/shared/src/Preferences.h
+++ b/libraries/shared/src/Preferences.h
@@ -364,6 +364,7 @@ class RadioButtonsPreference : public IntPreference {
     Q_OBJECT
     Q_PROPERTY(QString heading READ getHeading CONSTANT)
     Q_PROPERTY(QStringList items READ getItems CONSTANT)
+    Q_PROPERTY(bool indented READ getIndented CONSTANT)
 public:
     RadioButtonsPreference(const QString& category, const QString& name, Getter getter, Setter setter)
         : IntPreference(category, name, getter, setter) { }
@@ -373,10 +374,13 @@ public:
     const QStringList& getItems() { return _items; }
     void setHeading(const QString& heading) { _heading = heading; }
     void setItems(const QStringList& items) { _items = items; }
+    bool getIndented() { return _indented; }
+    void setIndented(const bool indented) { _indented = indented; }
 
 protected:
     QString _heading;
     QStringList _items;
+    bool _indented { false };
 };
 #endif
 
diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js
index 2658f11989..a579fee0d9 100644
--- a/scripts/system/controllers/controllerDispatcher.js
+++ b/scripts/system/controllers/controllerDispatcher.js
@@ -12,7 +12,7 @@
    LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES,
    getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers,
    PointerManager, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers,
-   PointerManager, print, Selection, DISPATCHER_HOVERING_LIST, DISPATCHER_HOVERING_STYLE
+   PointerManager, print, Selection, DISPATCHER_HOVERING_LIST, DISPATCHER_HOVERING_STYLE, Keyboard
 */
 
 controllerDispatcherPlugins = {};
@@ -453,6 +453,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             distanceScaleEnd: true,
             hand: LEFT_HAND
         });
+
+        Keyboard.setLeftHandLaser(this.leftPointer);
         this.rightPointer = this.pointerManager.createPointer(false, PickType.Ray, {
             joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND",
             filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
@@ -463,6 +465,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             distanceScaleEnd: true,
             hand: RIGHT_HAND
         });
+        Keyboard.setRightHandLaser(this.rightPointer);
         this.leftHudPointer = this.pointerManager.createPointer(true, PickType.Ray, {
             joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
             filter: Picks.PICK_HUD,
diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js
index 57066fb2dd..c4aa9efd50 100644
--- a/scripts/system/controllers/controllerModules/stylusInput.js
+++ b/scripts/system/controllers/controllerModules/stylusInput.js
@@ -128,7 +128,7 @@ Script.include("/~/system/libraries/controllers.js");
                 }
             }
 
-            var WEB_DISPLAY_STYLUS_DISTANCE = 0.5;
+            const WEB_DISPLAY_STYLUS_DISTANCE = (Keyboard.raised && Keyboard.preferMalletsOverLasers) ? 0.2 : 0.5;
             var nearStylusTarget = isNearStylusTarget(stylusTargets, WEB_DISPLAY_STYLUS_DISTANCE * sensorScaleFactor);
 
             if (nearStylusTarget.length !== 0) {
@@ -152,9 +152,13 @@ Script.include("/~/system/libraries/controllers.js");
 
             if (isUsingStylus && this.processStylus(controllerData)) {
                 Pointers.enablePointer(this.pointer);
+                this.hand === RIGHT_HAND ? Keyboard.disableRightMallet() : Keyboard.disableLeftMallet();
                 return makeRunningValues(true, [], []);
             } else {
                 Pointers.disablePointer(this.pointer);
+                if (Keyboard.raised && Keyboard.preferMalletsOverLasers) {
+                    this.hand === RIGHT_HAND ? Keyboard.enableRightMallet() : Keyboard.enableLeftMallet();
+                }
                 return makeRunningValues(false, [], []);
             }
         };
diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
index d2cb7fffd1..c1b0658af2 100644
--- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
+++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
@@ -14,11 +14,21 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
 Script.include("/~/system/libraries/controllers.js");
 
 (function() {
+    const intersectionType = {
+        None: 0,
+        WebOverlay: 1,
+        WebEntity: 2,
+        HifiKeyboard: 3,
+        Overlay: 4,
+        HifiTablet: 5,
+    };
+
     function WebSurfaceLaserInput(hand) {
         this.hand = hand;
         this.otherHand = this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND;
         this.running = false;
         this.ignoredObjects = [];
+        this.intersectedType = intersectionType["None"];
 
         this.parameters = makeDispatcherModuleParameters(
             160,
@@ -124,18 +134,29 @@ Script.include("/~/system/libraries/controllers.js");
                 if ((HMD.tabletID && objectID === HMD.tabletID) ||
                     (HMD.tabletScreenID && objectID === HMD.tabletScreenID) ||
                     (HMD.homeButtonID && objectID === HMD.homeButtonID)) {
-                    return true;
+                    return intersectionType["HifiTablet"];
                 } else {
                     var overlayType = Overlays.getOverlayType(objectID);
-                    return overlayType === "web3d" || triggerPressed;
+                    var type = intersectionType["None"];
+                    if (Keyboard.containsID(objectID) && !Keyboard.preferMalletsOverLasers) {
+                        type = intersectionType["HifiKeyboard"];
+                    } else if (overlayType === "web3d") {
+                        type = intersectionType["WebOverlay"];
+                    } else if (triggerPressed) {
+                        type = intersectionType["Overlay"];
+                    }
+
+                    return type;
                 }
             } else if (intersection.type === Picks.INTERSECTED_ENTITY) {
                 var entityProperties = Entities.getEntityProperties(objectID, DISPATCHER_PROPERTIES);
                 var entityType = entityProperties.type;
                 var isLocked = entityProperties.locked;
-                return entityType === "Web" && (!isLocked || triggerPressed);
+                if (entityType === "Web" && (!isLocked || triggerPressed)) {
+                    return intersectionType["WebEntity"];
+                }
             }
-            return false;
+            return intersectionType["None"];
         };
 
         this.deleteContextOverlay = function() {
@@ -152,9 +173,9 @@ Script.include("/~/system/libraries/controllers.js");
             }
         };
 
-        this.updateAllwaysOn = function() {
+        this.updateAlwaysOn = function(type) {
             var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser";
-            this.parameters.handLaser.allwaysOn = !Settings.getValue(PREFER_STYLUS_OVER_LASER, false);
+            this.parameters.handLaser.allwaysOn = (!Settings.getValue(PREFER_STYLUS_OVER_LASER, false) || type === intersectionType["HifiKeyboard"]);
         };
 
         this.getDominantHand = function() {
@@ -164,18 +185,27 @@ Script.include("/~/system/libraries/controllers.js");
         this.dominantHandOverride = false;
 
         this.isReady = function(controllerData) {
-            var otherModuleRunning = this.getOtherModule().running;
-            otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand.
             var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE &&
-                                   controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE;
-            var allowThisModule = !otherModuleRunning || isTriggerPressed;
+                controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE;
+            var type = this.isPointingAtTriggerable(controllerData, isTriggerPressed, false);
 
-            if ((allowThisModule && this.isPointingAtTriggerable(controllerData, isTriggerPressed, false)) && !this.grabModuleWantsNearbyOverlay(controllerData)) {
-                this.updateAllwaysOn();
-                if (isTriggerPressed) {
-                    this.dominantHandOverride = true; // Override dominant hand.
-                    this.getOtherModule().dominantHandOverride = false;
+            if (type !== intersectionType["None"] && !this.grabModuleWantsNearbyOverlay(controllerData)) {
+                if (type === intersectionType["WebOverlay"] || type === intersectionType["WebEntity"] || type === intersectionType["HifiTablet"]) {
+                    var otherModuleRunning = this.getOtherModule().running;
+                    otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand.
+                    var allowThisModule = !otherModuleRunning || isTriggerPressed;
+
+                    if (!allowThisModule) {
+                        return makeRunningValues(true, [], []);
+                    }
+
+                    if (isTriggerPressed) {
+                        this.dominantHandOverride = true; // Override dominant hand.
+                        this.getOtherModule().dominantHandOverride = false;
+                    }
                 }
+
+                this.updateAlwaysOn(type);
                 if (this.parameters.handLaser.allwaysOn || isTriggerPressed) {
                     return makeRunningValues(true, [], []);
                 }
@@ -187,33 +217,42 @@ Script.include("/~/system/libraries/controllers.js");
             return makeRunningValues(false, [], []);
         };
 
-        this.run = function(controllerData, deltaTime) {
+        this.shouldThisModuleRun = function(controllerData) {
             var otherModuleRunning = this.getOtherModule().running;
             otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand.
             otherModuleRunning = otherModuleRunning || this.getOtherModule().dominantHandOverride; // Override dominant hand.
             var grabModuleNeedsToRun = this.grabModuleWantsNearbyOverlay(controllerData);
             // only allow for non-near grab
-            var allowThisModule = !otherModuleRunning && !grabModuleNeedsToRun;
+            return !otherModuleRunning && !grabModuleNeedsToRun;
+        };
+
+        this.run = function(controllerData, deltaTime) {
+            this.addObjectToIgnoreList(controllerData);
+            var type = this.isPointingAtTriggerable(controllerData, isTriggerPressed, false);
             var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE;
             var laserOn = isTriggerPressed || this.parameters.handLaser.allwaysOn;
             this.addObjectToIgnoreList(controllerData);
-            if (allowThisModule) {
-                if (isTriggerPressed && !this.isPointingAtTriggerable(controllerData, isTriggerPressed, true)) {
-                    // if trigger is down + not pointing at a web entity, keep running web surface laser
+
+            if (type === intersectionType["HifiTablet"] && laserOn) {
+                if (this.shouldThisModuleRun(controllerData)) {
                     this.running = true;
                     return makeRunningValues(true, [], []);
-                } else if (laserOn && this.isPointingAtTriggerable(controllerData, isTriggerPressed, false)) {
-                    // if trigger is down + pointing at a web entity/overlay, keep running web surface laser
-                    this.running = true;
-                    return makeRunningValues(true, [], []);
-                } else {
-                    this.deleteContextOverlay();
-                    this.running = false;
-                    this.dominantHandOverride = false;
-                    return makeRunningValues(false, [], []);
                 }
+            } else if ((type === intersectionType["WebOverlay"] || type === intersectionType["WebEntity"]) && laserOn) { // auto laser on WebEntities andWebOverlays
+                if (this.shouldThisModuleRun(controllerData)) {
+                    this.running = true;
+                    return makeRunningValues(true, [], []);
+                }
+            } else if ((type === intersectionType["HifiKeyboard"] && laserOn) || type === intersectionType["Overlay"]) {
+                this.running = true;
+                return makeRunningValues(true, [], []);
+            } else if (isTriggerPressed && !this.isPointingAtTriggerable(controllerData, isTriggerPressed, true)) {
+                // if trigger is down + not pointing at a web entity, keep running web surface laser
+                this.running = true;
+                return makeRunningValues(true, [], []);
+
             }
-            // if module needs to stop from near grabs or other modules are running, stop it.
+
             this.deleteContextOverlay();
             this.running = false;
             this.dominantHandOverride = false;