diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp
index 76038d77c..686fe0bad 100644
--- a/src/core/analog_controller.cpp
+++ b/src/core/analog_controller.cpp
@@ -466,10 +466,10 @@ std::optional<s32> AnalogController::StaticGetButtonCodeByName(std::string_view
 
 Controller::AxisList AnalogController::StaticGetAxisNames()
 {
-  return {{TRANSLATABLE("AnalogController", "LeftX"), static_cast<s32>(Axis::LeftX)},
-          {TRANSLATABLE("AnalogController", "LeftY"), static_cast<s32>(Axis::LeftY)},
-          {TRANSLATABLE("AnalogController", "RightX"), static_cast<s32>(Axis::RightX)},
-          {TRANSLATABLE("AnalogController", "RightY"), static_cast<s32>(Axis::RightY)}};
+  return {{TRANSLATABLE("AnalogController", "LeftX"), static_cast<s32>(Axis::LeftX), AxisType::Full},
+          {TRANSLATABLE("AnalogController", "LeftY"), static_cast<s32>(Axis::LeftY), AxisType::Full},
+          {TRANSLATABLE("AnalogController", "RightX"), static_cast<s32>(Axis::RightX), AxisType::Full},
+          {TRANSLATABLE("AnalogController", "RightY"), static_cast<s32>(Axis::RightY), AxisType::Full}};
 }
 
 Controller::ButtonList AnalogController::StaticGetButtonNames()
diff --git a/src/core/controller.h b/src/core/controller.h
index 3c63c3900..2f2a83eb4 100644
--- a/src/core/controller.h
+++ b/src/core/controller.h
@@ -14,8 +14,14 @@ class HostInterface;
 class Controller
 {
 public:
+  enum class AxisType : u8
+  {
+    Full,
+    Half
+  };
+
   using ButtonList = std::vector<std::pair<std::string, s32>>;
-  using AxisList = std::vector<std::pair<std::string, s32>>;
+  using AxisList = std::vector<std::tuple<std::string, s32, AxisType>>;
   using SettingList = std::vector<SettingInfo>;
 
   Controller();
diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp
index 85d235c41..d1c8e16c5 100644
--- a/src/core/negcon.cpp
+++ b/src/core/negcon.cpp
@@ -219,12 +219,12 @@ std::optional<s32> NeGcon::StaticGetButtonCodeByName(std::string_view button_nam
 
 Controller::AxisList NeGcon::StaticGetAxisNames()
 {
-#define A(n)                                                                                                           \
+#define A(n, t)                                                                                                        \
   {                                                                                                                    \
-    #n, static_cast <s32>(Axis::n)                                                                                     \
+    #n, static_cast <s32>(Axis::n), Controller::AxisType::t                                                            \
   }
 
-  return {A(Steering), A(I), A(II), A(L)};
+  return {A(Steering, Full), A(I, Half), A(II, Half), A(L, Half)};
 
 #undef A
 }
diff --git a/src/duckstation-qt/controllersettingswidget.cpp b/src/duckstation-qt/controllersettingswidget.cpp
index 76783c03f..a66014163 100644
--- a/src/duckstation-qt/controllersettingswidget.cpp
+++ b/src/duckstation-qt/controllersettingswidget.cpp
@@ -224,7 +224,7 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin
     const int num_rows = (static_cast<int>(axises.size()) + 1) / 2;
     int current_row = 0;
     int current_column = 0;
-    for (const auto& [axis_name, axis_code] : axises)
+    for (const auto& [axis_name, axis_code, axis_type] : axises)
     {
       if (current_row == num_rows)
       {
@@ -235,8 +235,8 @@ void ControllerSettingsWidget::createPortBindingSettingsUi(int index, PortSettin
       std::string section_name = StringUtil::StdStringFromFormat("Controller%d", index + 1);
       std::string key_name = StringUtil::StdStringFromFormat("Axis%s", axis_name.c_str());
       QLabel* label = new QLabel(qApp->translate(cname, axis_name.c_str()), ui->bindings_container);
-      InputAxisBindingWidget* button = new InputAxisBindingWidget(m_host_interface, std::move(section_name),
-                                                                  std::move(key_name), ui->bindings_container);
+      InputAxisBindingWidget* button = new InputAxisBindingWidget(
+        m_host_interface, std::move(section_name), std::move(key_name), axis_type, ui->bindings_container);
       layout->addWidget(label, start_row + current_row, current_column);
       layout->addWidget(button, start_row + current_row, current_column + 1);
 
diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp
index 56b261152..0edded305 100644
--- a/src/duckstation-qt/inputbindingdialog.cpp
+++ b/src/duckstation-qt/inputbindingdialog.cpp
@@ -36,8 +36,32 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event)
 {
   const QEvent::Type event_type = event->type();
 
-  if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonRelease ||
-      event_type == QEvent::MouseButtonDblClick)
+  // if the key is being released, set the input
+  if (event_type == QEvent::KeyRelease)
+  {
+    addNewBinding(std::move(m_new_binding_value));
+    stopListeningForInput();
+    return true;
+  }
+  else if (event_type == QEvent::KeyPress)
+  {
+    QString binding = QtUtils::KeyEventToString(static_cast<QKeyEvent*>(event));
+    if (!binding.isEmpty())
+      m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString();
+
+    return true;
+  }
+  else if (event_type == QEvent::MouseButtonRelease)
+  {
+    const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->button());
+    const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask);
+    m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1);
+    addNewBinding(std::move(m_new_binding_value));
+    stopListeningForInput();
+    return true;
+  }
+
+  if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick)
   {
     return true;
   }
@@ -103,6 +127,27 @@ void InputBindingDialog::addNewBinding(std::string new_binding)
   saveListToSettings();
 }
 
+void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive)
+{
+  const char* sign_char = "";
+  if (positive)
+  {
+    sign_char = *positive ? "+" : "-";
+  }
+
+  std::string binding =
+    StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index);
+  addNewBinding(std::move(binding));
+  stopListeningForInput();
+}
+
+void InputBindingDialog::bindToControllerButton(int controller_index, int button_index)
+{
+  std::string binding = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index);
+  addNewBinding(std::move(binding));
+  stopListeningForInput();
+}
+
 void InputBindingDialog::onAddBindingButtonClicked()
 {
   if (isListeningForInput())
@@ -159,38 +204,6 @@ InputButtonBindingDialog::~InputButtonBindingDialog()
     InputButtonBindingDialog::stopListeningForInput();
 }
 
-bool InputButtonBindingDialog::eventFilter(QObject* watched, QEvent* event)
-{
-  const QEvent::Type event_type = event->type();
-
-  // if the key is being released, set the input
-  if (event_type == QEvent::KeyRelease)
-  {
-    addNewBinding(std::move(m_new_binding_value));
-    stopListeningForInput();
-    return true;
-  }
-  else if (event_type == QEvent::KeyPress)
-  {
-    QString binding = QtUtils::KeyEventToString(static_cast<QKeyEvent*>(event));
-    if (!binding.isEmpty())
-      m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString();
-
-    return true;
-  }
-  else if (event_type == QEvent::MouseButtonRelease)
-  {
-    const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->button());
-    const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask);
-    m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1);
-    addNewBinding(std::move(m_new_binding_value));
-    stopListeningForInput();
-    return true;
-  }
-
-  return InputBindingDialog::eventFilter(watched, event);
-}
-
 void InputButtonBindingDialog::hookControllerInput()
 {
   ControllerInterface* controller_interface = m_host_interface->getControllerInterface();
@@ -206,7 +219,7 @@ void InputButtonBindingDialog::hookControllerInput()
 
       // TODO: this probably should consider the "last value"
       QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, ei.value > 0));
+                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, ei.value > 0));
       return ControllerInterface::Hook::CallbackResult::StopMonitoring;
     }
     else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
@@ -229,21 +242,6 @@ void InputButtonBindingDialog::unhookControllerInput()
   controller_interface->ClearHook();
 }
 
-void InputButtonBindingDialog::bindToControllerAxis(int controller_index, int axis_index, bool positive)
-{
-  std::string binding =
-    StringUtil::StdStringFromFormat("Controller%d/%cAxis%d", controller_index, positive ? '+' : '-', axis_index);
-  addNewBinding(std::move(binding));
-  stopListeningForInput();
-}
-
-void InputButtonBindingDialog::bindToControllerButton(int controller_index, int button_index)
-{
-  std::string binding = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index);
-  addNewBinding(std::move(binding));
-  stopListeningForInput();
-}
-
 void InputButtonBindingDialog::startListeningForInput(u32 timeout_in_seconds)
 {
   InputBindingDialog::startListeningForInput(timeout_in_seconds);
@@ -257,8 +255,10 @@ void InputButtonBindingDialog::stopListeningForInput()
 }
 
 InputAxisBindingDialog::InputAxisBindingDialog(QtHostInterface* host_interface, std::string section_name,
-                                               std::string key_name, std::vector<std::string> bindings, QWidget* parent)
-  : InputBindingDialog(host_interface, std::move(section_name), std::move(key_name), std::move(bindings), parent)
+                                               std::string key_name, std::vector<std::string> bindings,
+                                               Controller::AxisType axis_type, QWidget* parent)
+  : InputBindingDialog(host_interface, std::move(section_name), std::move(key_name), std::move(bindings), parent),
+    m_axis_type(axis_type)
 {
 }
 
@@ -282,6 +282,13 @@ void InputAxisBindingDialog::hookControllerInput()
         return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
 
       QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
+                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, std::nullopt));
+      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+    }
+    else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half &&
+             ei.value > 0.0f)
+    {
+      QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index),
                                 Q_ARG(int, ei.button_or_axis_number));
       return ControllerInterface::Hook::CallbackResult::StopMonitoring;
     }
@@ -299,11 +306,19 @@ void InputAxisBindingDialog::unhookControllerInput()
   controller_interface->ClearHook();
 }
 
-void InputAxisBindingDialog::bindToControllerAxis(int controller_index, int axis_index)
+bool InputAxisBindingDialog::eventFilter(QObject* watched, QEvent* event)
 {
-  std::string binding = StringUtil::StdStringFromFormat("Controller%d/Axis%d", controller_index, axis_index);
-  addNewBinding(std::move(binding));
-  stopListeningForInput();
+  if (m_axis_type != Controller::AxisType::Half)
+  {
+    const QEvent::Type event_type = event->type();
+
+    if (event_type == QEvent::KeyRelease || event_type == QEvent::KeyPress || event_type == QEvent::MouseButtonRelease)
+    {
+      return true;
+    }
+  }
+
+  return InputBindingDialog::eventFilter(watched, event);
 }
 
 void InputAxisBindingDialog::startListeningForInput(u32 timeout_in_seconds)
diff --git a/src/duckstation-qt/inputbindingdialog.h b/src/duckstation-qt/inputbindingdialog.h
index 84ee7efe0..ff26b3776 100644
--- a/src/duckstation-qt/inputbindingdialog.h
+++ b/src/duckstation-qt/inputbindingdialog.h
@@ -1,7 +1,9 @@
 #pragma once
 #include "common/types.h"
+#include "core/controller.h"
 #include "ui_inputbindingdialog.h"
 #include <QtWidgets/QDialog>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -17,6 +19,8 @@ public:
   ~InputBindingDialog();
 
 protected Q_SLOTS:
+  void bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive);
+  void bindToControllerButton(int controller_index, int button_index);
   void onAddBindingButtonClicked();
   void onRemoveBindingButtonClicked();
   void onClearBindingsButtonClicked();
@@ -52,7 +56,7 @@ protected:
   u32 m_input_listen_remaining_seconds = 0;
 };
 
-class InputButtonBindingDialog : public InputBindingDialog
+class InputButtonBindingDialog final : public InputBindingDialog
 {
   Q_OBJECT
 
@@ -61,13 +65,6 @@ public:
                            std::vector<std::string> bindings, QWidget* parent);
   ~InputButtonBindingDialog();
 
-protected:
-  bool eventFilter(QObject* watched, QEvent* event) override;
-
-private Q_SLOTS:
-  void bindToControllerAxis(int controller_index, int axis_index, bool positive);
-  void bindToControllerButton(int controller_index, int button_index);
-
 protected:
   void startListeningForInput(u32 timeout_in_seconds) override;
   void stopListeningForInput() override;
@@ -75,21 +72,22 @@ protected:
   void unhookControllerInput();
 };
 
-class InputAxisBindingDialog : public InputBindingDialog
+class InputAxisBindingDialog final : public InputBindingDialog
 {
   Q_OBJECT
 
 public:
   InputAxisBindingDialog(QtHostInterface* host_interface, std::string section_name, std::string key_name,
-                           std::vector<std::string> bindings, QWidget* parent);
+                         std::vector<std::string> bindings, Controller::AxisType axis_type, QWidget* parent);
   ~InputAxisBindingDialog();
 
-private Q_SLOTS:
-  void bindToControllerAxis(int controller_index, int axis_index);
-
 protected:
+  bool eventFilter(QObject* watched, QEvent* event) override;
   void startListeningForInput(u32 timeout_in_seconds) override;
   void stopListeningForInput() override;
   void hookControllerInput();
   void unhookControllerInput();
+
+private:
+  Controller::AxisType m_axis_type;
 };
diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp
index ec97936dc..9d55dc16e 100644
--- a/src/duckstation-qt/inputbindingwidgets.cpp
+++ b/src/duckstation-qt/inputbindingwidgets.cpp
@@ -40,6 +40,27 @@ void InputBindingWidget::updateText()
     setText(QString::fromStdString(m_bindings[0]));
 }
 
+void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive)
+{
+  const char* sign_char = "";
+  if (positive)
+  {
+    sign_char = *positive ? "+" : "-";
+  }
+
+  m_new_binding_value =
+    StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index);
+  setNewBinding();
+  stopListeningForInput();
+}
+
+void InputBindingWidget::bindToControllerButton(int controller_index, int button_index)
+{
+  m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index);
+  setNewBinding();
+  stopListeningForInput();
+}
+
 void InputBindingWidget::beginRebindAll()
 {
   m_is_binding_all = true;
@@ -53,8 +74,32 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event)
 {
   const QEvent::Type event_type = event->type();
 
-  if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonRelease ||
-      event_type == QEvent::MouseButtonDblClick)
+  // if the key is being released, set the input
+  if (event_type == QEvent::KeyRelease)
+  {
+    setNewBinding();
+    stopListeningForInput();
+    return true;
+  }
+  else if (event_type == QEvent::KeyPress)
+  {
+    QString binding = QtUtils::KeyEventToString(static_cast<QKeyEvent*>(event));
+    if (!binding.isEmpty())
+      m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString();
+
+    return true;
+  }
+  else if (event_type == QEvent::MouseButtonRelease)
+  {
+    const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->button());
+    const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask);
+    m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1);
+    setNewBinding();
+    stopListeningForInput();
+    return true;
+  }
+
+  if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick)
   {
     return true;
   }
@@ -185,38 +230,6 @@ InputButtonBindingWidget::~InputButtonBindingWidget()
     InputButtonBindingWidget::stopListeningForInput();
 }
 
-bool InputButtonBindingWidget::eventFilter(QObject* watched, QEvent* event)
-{
-  const QEvent::Type event_type = event->type();
-
-  // if the key is being released, set the input
-  if (event_type == QEvent::KeyRelease)
-  {
-    setNewBinding();
-    stopListeningForInput();
-    return true;
-  }
-  else if (event_type == QEvent::KeyPress)
-  {
-    QString binding = QtUtils::KeyEventToString(static_cast<QKeyEvent*>(event));
-    if (!binding.isEmpty())
-      m_new_binding_value = QStringLiteral("Keyboard/%1").arg(binding).toStdString();
-
-    return true;
-  }
-  else if (event_type == QEvent::MouseButtonRelease)
-  {
-    const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->button());
-    const u32 button_index = (button_mask == 0u) ? 0 : CountTrailingZeros(button_mask);
-    m_new_binding_value = StringUtil::StdStringFromFormat("Mouse/Button%d", button_index + 1);
-    setNewBinding();
-    stopListeningForInput();
-    return true;
-  }
-
-  return InputBindingWidget::eventFilter(watched, event);
-}
-
 void InputButtonBindingWidget::hookControllerInput()
 {
   ControllerInterface* controller_interface = m_host_interface->getControllerInterface();
@@ -232,7 +245,7 @@ void InputButtonBindingWidget::hookControllerInput()
 
       // TODO: this probably should consider the "last value"
       QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, ei.value > 0));
+                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, ei.value > 0));
       return ControllerInterface::Hook::CallbackResult::StopMonitoring;
     }
     else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
@@ -255,21 +268,6 @@ void InputButtonBindingWidget::unhookControllerInput()
   controller_interface->ClearHook();
 }
 
-void InputButtonBindingWidget::bindToControllerAxis(int controller_index, int axis_index, bool positive)
-{
-  m_new_binding_value =
-    StringUtil::StdStringFromFormat("Controller%d/%cAxis%d", controller_index, positive ? '+' : '-', axis_index);
-  setNewBinding();
-  stopListeningForInput();
-}
-
-void InputButtonBindingWidget::bindToControllerButton(int controller_index, int button_index)
-{
-  m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Button%d", controller_index, button_index);
-  setNewBinding();
-  stopListeningForInput();
-}
-
 void InputButtonBindingWidget::startListeningForInput(u32 timeout_in_seconds)
 {
   InputBindingWidget::startListeningForInput(timeout_in_seconds);
@@ -291,8 +289,8 @@ void InputButtonBindingWidget::openDialog()
 }
 
 InputAxisBindingWidget::InputAxisBindingWidget(QtHostInterface* host_interface, std::string section_name,
-                                               std::string key_name, QWidget* parent)
-  : InputBindingWidget(host_interface, std::move(section_name), std::move(key_name), parent)
+                                               std::string key_name, Controller::AxisType axis_type, QWidget* parent)
+  : InputBindingWidget(host_interface, std::move(section_name), std::move(key_name), parent), m_axis_type(axis_type)
 {
 }
 
@@ -316,6 +314,13 @@ void InputAxisBindingWidget::hookControllerInput()
         return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
 
       QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
+                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, std::nullopt));
+      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+    }
+    else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half &&
+             ei.value > 0.0f)
+    {
+      QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index),
                                 Q_ARG(int, ei.button_or_axis_number));
       return ControllerInterface::Hook::CallbackResult::StopMonitoring;
     }
@@ -333,11 +338,19 @@ void InputAxisBindingWidget::unhookControllerInput()
   controller_interface->ClearHook();
 }
 
-void InputAxisBindingWidget::bindToControllerAxis(int controller_index, int axis_index)
+bool InputAxisBindingWidget::eventFilter(QObject* watched, QEvent* event)
 {
-  m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Axis%d", controller_index, axis_index);
-  setNewBinding();
-  stopListeningForInput();
+  if (m_axis_type != Controller::AxisType::Half)
+  {
+    const QEvent::Type event_type = event->type();
+
+    if (event_type == QEvent::KeyRelease || event_type == QEvent::KeyPress || event_type == QEvent::MouseButtonRelease)
+    {
+      return true;
+    }
+  }
+
+  return InputBindingWidget::eventFilter(watched, event);
 }
 
 void InputAxisBindingWidget::startListeningForInput(u32 timeout_in_seconds)
@@ -354,7 +367,7 @@ void InputAxisBindingWidget::stopListeningForInput()
 
 void InputAxisBindingWidget::openDialog()
 {
-  InputAxisBindingDialog binding_dialog(m_host_interface, m_section_name, m_key_name, m_bindings,
+  InputAxisBindingDialog binding_dialog(m_host_interface, m_section_name, m_key_name, m_bindings, m_axis_type,
                                         QtUtils::GetRootWidget(this));
   binding_dialog.exec();
   reloadBinding();
diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h
index 9c3bc338d..e34f44d8d 100644
--- a/src/duckstation-qt/inputbindingwidgets.h
+++ b/src/duckstation-qt/inputbindingwidgets.h
@@ -1,6 +1,8 @@
 #pragma once
+#include "core/controller.h"
 #include "core/types.h"
 #include <QtWidgets/QPushButton>
+#include <optional>
 
 class QTimer;
 
@@ -18,6 +20,8 @@ public:
   ALWAYS_INLINE void setNextWidget(InputBindingWidget* widget) { m_next_widget = widget; }
 
 public Q_SLOTS:
+  void bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive);
+  void bindToControllerButton(int controller_index, int button_index);
   void beginRebindAll();
   void clearBinding();
   void reloadBinding();
@@ -66,13 +70,6 @@ public:
                            QWidget* parent);
   ~InputButtonBindingWidget();
 
-protected:
-  bool eventFilter(QObject* watched, QEvent* event) override;
-
-private Q_SLOTS:
-  void bindToControllerAxis(int controller_index, int axis_index, bool positive);
-  void bindToControllerButton(int controller_index, int button_index);
-
 protected:
   void startListeningForInput(u32 timeout_in_seconds) override;
   void stopListeningForInput() override;
@@ -87,18 +84,19 @@ class InputAxisBindingWidget : public InputBindingWidget
 
 public:
   InputAxisBindingWidget(QtHostInterface* host_interface, std::string section_name, std::string key_name,
-                         QWidget* parent);
+                         Controller::AxisType axis_type, QWidget* parent);
   ~InputAxisBindingWidget();
 
-private Q_SLOTS:
-  void bindToControllerAxis(int controller_index, int axis_index);
-
 protected:
+  bool eventFilter(QObject* watched, QEvent* event) override;
   void startListeningForInput(u32 timeout_in_seconds) override;
   void stopListeningForInput() override;
   void openDialog() override;
   void hookControllerInput();
   void unhookControllerInput();
+
+private:
+  Controller::AxisType m_axis_type;
 };
 
 class InputRumbleBindingWidget : public InputBindingWidget
diff --git a/src/duckstation-qt/main.cpp b/src/duckstation-qt/main.cpp
index 8eb740aaf..cfad70bea 100644
--- a/src/duckstation-qt/main.cpp
+++ b/src/duckstation-qt/main.cpp
@@ -1,6 +1,7 @@
 #include "common/log.h"
 #include "mainwindow.h"
 #include "qthostinterface.h"
+#include "qtutils.h"
 #include <QtWidgets/QApplication>
 #include <QtWidgets/QMessageBox>
 #include <cstdlib>
@@ -8,6 +9,9 @@
 
 int main(int argc, char* argv[])
 {
+  // Register any standard types we need elsewhere
+  qRegisterMetaType<std::optional<bool>>();
+
   QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
   QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h
index ef9975616..5eb42093a 100644
--- a/src/duckstation-qt/qtutils.h
+++ b/src/duckstation-qt/qtutils.h
@@ -1,9 +1,12 @@
 #pragma once
 #include <QtCore/QByteArray>
+#include <QtCore/QMetaType>
 #include <QtCore/QString>
 #include <initializer_list>
 #include <optional>
 
+Q_DECLARE_METATYPE(std::optional<bool>);
+
 class ByteStream;
 
 class QFrame;
diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp
index 26bbc4315..84f9a13fc 100644
--- a/src/frontend-common/common_host_interface.cpp
+++ b/src/frontend-common/common_host_interface.cpp
@@ -7,7 +7,6 @@
 #include "common/string_util.h"
 #include "controller_interface.h"
 #include "core/cdrom.h"
-#include "core/controller.h"
 #include "core/cpu_code_cache.h"
 #include "core/dma.h"
 #include "core/game_list.h"
@@ -1061,8 +1060,9 @@ void CommonHostInterface::UpdateControllerInputMap(SettingsInterface& si)
     const auto axis_names = Controller::GetAxisNames(ctype);
     for (const auto& it : axis_names)
     {
-      const std::string& axis_name = it.first;
-      const s32 axis_code = it.second;
+      const std::string& axis_name = std::get<std::string>(it);
+      const s32 axis_code = std::get<s32>(it);
+      const auto axis_type = std::get<Controller::AxisType>(it);
 
       const std::vector<std::string> bindings =
         si.GetStringList(category, TinyString::FromFormat("Axis%s", axis_name.c_str()));
@@ -1072,7 +1072,7 @@ void CommonHostInterface::UpdateControllerInputMap(SettingsInterface& si)
         if (!SplitBinding(binding, &device, &axis))
           continue;
 
-        AddAxisToInputMap(binding, device, axis, [this, controller_index, axis_code](float value) {
+        AddAxisToInputMap(binding, device, axis, axis_type, [this, controller_index, axis_code](float value) {
           if (System::IsShutdown())
             return;
 
@@ -1204,8 +1204,44 @@ bool CommonHostInterface::AddButtonToInputMap(const std::string& binding, const
 }
 
 bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const std::string_view& device,
-                                            const std::string_view& axis, InputAxisHandler handler)
+                                            const std::string_view& axis, Controller::AxisType axis_type,
+                                            InputAxisHandler handler)
 {
+  if (axis_type == Controller::AxisType::Half)
+  {
+    if (device == "Keyboard")
+    {
+      std::optional<int> key_id = GetHostKeyCode(axis);
+      if (!key_id.has_value())
+      {
+        Log_WarningPrintf("Unknown keyboard key in binding '%s'", binding.c_str());
+        return false;
+      }
+
+      m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler));
+      return true;
+    }
+
+    if (device == "Mouse")
+    {
+      if (StringUtil::StartsWith(axis, "Button"))
+      {
+        const std::optional<s32> button_index = StringUtil::FromChars<s32>(axis.substr(6));
+        if (!button_index.has_value())
+        {
+          Log_WarningPrintf("Invalid button in mouse binding '%s'", binding.c_str());
+          return false;
+        }
+
+        m_mouse_input_handlers.emplace(static_cast<HostMouseButton>(button_index.value()), std::move(handler));
+        return true;
+      }
+
+      Log_WarningPrintf("Malformed mouse binding '%s'", binding.c_str());
+      return false;
+    }
+  }
+
   if (StringUtil::StartsWith(device, "Controller"))
   {
     if (!m_controller_interface)
@@ -1233,6 +1269,18 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
 
       return true;
     }
+    else if (StringUtil::StartsWith(axis, "Button") && axis_type == Controller::AxisType::Half)
+    {
+      const std::optional<int> button_index = StringUtil::FromChars<int>(axis.substr(6));
+      if (!button_index ||
+          !m_controller_interface->BindControllerButtonToAxis(*controller_index, *button_index, std::move(handler)))
+      {
+        Log_WarningPrintf("Failed to bind controller button '%s' to axis", binding.c_str());
+        return false;
+      }
+
+      return true;
+    }
 
     Log_WarningPrintf("Malformed controller binding '%s' in button", binding.c_str());
     return false;
@@ -1539,7 +1587,7 @@ void CommonHostInterface::ClearAllControllerBindings(SettingsInterface& si)
       si.DeleteValue(section_name, button.first.c_str());
 
     for (const auto& axis : Controller::GetAxisNames(ctype))
-      si.DeleteValue(section_name, axis.first.c_str());
+      si.DeleteValue(section_name, std::get<std::string>(axis).c_str());
 
     if (Controller::GetVibrationMotorCount(ctype) > 0)
       si.DeleteValue(section_name, "Rumble");
@@ -1583,8 +1631,8 @@ void CommonHostInterface::ApplyInputProfile(const char* profile_path, SettingsIn
 
     for (const auto& axis : Controller::GetAxisNames(*ctype))
     {
-      const auto key_name = TinyString::FromFormat("Axis%s", axis.first.c_str());
-      si.DeleteValue(section_name, axis.first.c_str());
+      const auto key_name = TinyString::FromFormat("Axis%s", std::get<std::string>(axis).c_str());
+      si.DeleteValue(section_name, std::get<std::string>(axis).c_str());
       const std::vector<std::string> bindings = profile.GetStringList(section_name, key_name);
       for (const std::string& binding : bindings)
         si.AddToStringList(section_name, key_name, binding.c_str());
@@ -1642,7 +1690,7 @@ bool CommonHostInterface::SaveInputProfile(const char* profile_path, SettingsInt
 
     for (const auto& axis : Controller::GetAxisNames(ctype))
     {
-      const auto key_name = TinyString::FromFormat("Axis%s", axis.first.c_str());
+      const auto key_name = TinyString::FromFormat("Axis%s", std::get<std::string>(axis).c_str());
       const std::vector<std::string> bindings = si.GetStringList(section_name, key_name);
       for (const std::string& binding : bindings)
         profile.AddToStringList(section_name, key_name, binding.c_str());
diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h
index cc2218d65..9364df3b2 100644
--- a/src/frontend-common/common_host_interface.h
+++ b/src/frontend-common/common_host_interface.h
@@ -1,5 +1,6 @@
 #pragma once
 #include "common/string.h"
+#include "core/controller.h"
 #include "core/host_interface.h"
 #include <atomic>
 #include <functional>
@@ -196,7 +197,8 @@ protected:
   virtual bool AddButtonToInputMap(const std::string& binding, const std::string_view& device,
                                    const std::string_view& button, InputButtonHandler handler);
   virtual bool AddAxisToInputMap(const std::string& binding, const std::string_view& device,
-                                 const std::string_view& axis, InputAxisHandler handler);
+                                 const std::string_view& axis, Controller::AxisType axis_type,
+                                 InputAxisHandler handler);
   virtual bool AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors);
 
   /// Reloads the input map from config. Callable from controller interface.
diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h
index ab877a72a..60e287f11 100644
--- a/src/frontend-common/controller_interface.h
+++ b/src/frontend-common/controller_interface.h
@@ -3,9 +3,9 @@
 #include "core/types.h"
 #include <array>
 #include <functional>
-#include <optional>
 #include <map>
 #include <mutex>
+#include <optional>
 
 class HostInterface;
 class Controller;
@@ -54,6 +54,7 @@ public:
   virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0;
   virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
                                           ButtonCallback callback) = 0;
+  virtual bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) = 0;
 
   virtual void PollEvents() = 0;
 
diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp
index 919228d49..7d3818758 100644
--- a/src/frontend-common/sdl_controller_interface.cpp
+++ b/src/frontend-common/sdl_controller_interface.cpp
@@ -35,8 +35,7 @@ bool SDLControllerInterface::Initialize(CommonHostInterface* host_interface)
     Log_InfoPrintf("Loading game controller mappings from '%s'", gcdb_file_name.c_str());
     if (SDL_GameControllerAddMappingsFromFile(gcdb_file_name.c_str()) < 0)
     {
-      Log_ErrorPrintf("SDL_GameControllerAddMappingsFromFile(%s) failed: %s",
-                      gcdb_file_name.c_str(), SDL_GetError());
+      Log_ErrorPrintf("SDL_GameControllerAddMappingsFromFile(%s) failed: %s", gcdb_file_name.c_str(), SDL_GetError());
     }
   }
 
@@ -293,6 +292,19 @@ bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, in
   return true;
 }
 
+bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback)
+{
+  auto it = GetControllerDataForPlayerId(controller_index);
+  if (it == m_controllers.end())
+    return false;
+
+  if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
+    return false;
+
+  it->button_axis_mapping[button_number] = std::move(callback);
+  return true;
+}
+
 bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev)
 {
   const float value = static_cast<float>(ev->caxis.value) / (ev->caxis.value < 0 ? 32768.0f : 32767.0f);
@@ -350,11 +362,20 @@ bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev)
     return true;
 
   const ButtonCallback& cb = it->button_mapping[ev->cbutton.button];
-  if (!cb)
-    return false;
+  if (cb)
+  {
+    cb(pressed);
+    return true;
+  }
 
-  cb(pressed);
-  return true;
+  // Assume a half-axis, i.e. in 0..1 range
+  const AxisCallback& axis_cb = it->button_axis_mapping[ev->cbutton.button];
+  if (axis_cb)
+  {
+    axis_cb(pressed ? 1.0f : 0.0f);
+  }
+
+  return false;
 }
 
 u32 SDLControllerInterface::GetControllerRumbleMotorCount(int controller_index)
diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h
index 4fa43e556..d839b73a0 100644
--- a/src/frontend-common/sdl_controller_interface.h
+++ b/src/frontend-common/sdl_controller_interface.h
@@ -1,10 +1,10 @@
 #pragma once
-#include "core/types.h"
 #include "controller_interface.h"
+#include "core/types.h"
 #include <array>
 #include <functional>
-#include <vector>
 #include <mutex>
+#include <vector>
 
 union SDL_Event;
 
@@ -27,7 +27,9 @@ public:
   // Binding to events. If a binding for this axis/button already exists, returns false.
   bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override;
   bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
-  bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) override;
+  bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
+                                  ButtonCallback callback) override;
+  bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
 
   // Changing rumble strength.
   u32 GetControllerRumbleMotorCount(int controller_index) override;
@@ -59,6 +61,7 @@ private:
     std::array<AxisCallback, MAX_NUM_AXISES> axis_mapping;
     std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;
     std::array<std::array<ButtonCallback, 2>, MAX_NUM_AXISES> axis_button_mapping;
+    std::array<AxisCallback, MAX_NUM_BUTTONS> button_axis_mapping;
   };
 
   using ControllerDataVector = std::vector<ControllerData>;
diff --git a/src/frontend-common/xinput_controller_interface.cpp b/src/frontend-common/xinput_controller_interface.cpp
index 076746f3b..3ef93d90d 100644
--- a/src/frontend-common/xinput_controller_interface.cpp
+++ b/src/frontend-common/xinput_controller_interface.cpp
@@ -204,6 +204,19 @@ bool XInputControllerInterface::BindControllerAxisToButton(int controller_index,
   return true;
 }
 
+bool XInputControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
+                                                           AxisCallback callback)
+{
+  if (static_cast<u32>(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected)
+    return false;
+
+  if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
+    return false;
+
+  m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback);
+  return true;
+}
+
 bool XInputControllerInterface::HandleAxisEvent(u32 index, Axis axis, s32 value)
 {
   const float f_value = static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
@@ -255,10 +268,18 @@ bool XInputControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pr
     return true;
 
   const ButtonCallback& cb = m_controllers[index].button_mapping[button];
-  if (!cb)
-    return false;
+  if (cb)
+  {
+    cb(pressed);
+    return true;
+  }
 
-  cb(pressed);
+  // Assume a half-axis, i.e. in 0..1 range
+  const AxisCallback& axis_cb = m_controllers[index].button_axis_mapping[button];
+  if (axis_cb)
+  {
+    axis_cb(pressed ? 1.0f : 0.0f);
+  }
   return true;
 }
 
diff --git a/src/frontend-common/xinput_controller_interface.h b/src/frontend-common/xinput_controller_interface.h
index 5d02e1262..21802ff40 100644
--- a/src/frontend-common/xinput_controller_interface.h
+++ b/src/frontend-common/xinput_controller_interface.h
@@ -26,6 +26,7 @@ public:
   bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
   bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
                                   ButtonCallback callback) override;
+  bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
 
   // Changing rumble strength.
   u32 GetControllerRumbleMotorCount(int controller_index) override;
@@ -68,6 +69,7 @@ private:
     std::array<AxisCallback, MAX_NUM_AXISES> axis_mapping;
     std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;
     std::array<std::array<ButtonCallback, 2>, MAX_NUM_AXISES> axis_button_mapping;
+    std::array<AxisCallback, MAX_NUM_BUTTONS> button_axis_mapping;
   };
 
   using ControllerDataArray = std::array<ControllerData, XUSER_MAX_COUNT>;