diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp
index 128101e8c..89638cb85 100644
--- a/src/core/hid/emulated_controller.cpp
+++ b/src/core/hid/emulated_controller.cpp
@@ -93,6 +93,7 @@ void EmulatedController::ReloadFromSettings() {
         motion_params[index] = Common::ParamPackage(player.motions[index]);
     }
 
+    controller.color_values = {};
     controller.colors_state.fullkey = {
         .body = GetNpadColor(player.body_color_left),
         .button = GetNpadColor(player.button_color_left),
@@ -132,6 +133,11 @@ void EmulatedController::LoadDevices() {
     trigger_params[LeftIndex] = button_params[Settings::NativeButton::ZL];
     trigger_params[RightIndex] = button_params[Settings::NativeButton::ZR];
 
+    color_params[LeftIndex] = left_joycon;
+    color_params[RightIndex] = right_joycon;
+    color_params[LeftIndex].Set("color", true);
+    color_params[RightIndex].Set("color", true);
+
     battery_params[LeftIndex] = left_joycon;
     battery_params[RightIndex] = right_joycon;
     battery_params[LeftIndex].Set("battery", true);
@@ -160,6 +166,7 @@ void EmulatedController::LoadDevices() {
                            Common::Input::CreateInputDevice);
     std::ranges::transform(battery_params, battery_devices.begin(),
                            Common::Input::CreateInputDevice);
+    std::ranges::transform(color_params, color_devices.begin(), Common::Input::CreateInputDevice);
     camera_devices = Common::Input::CreateInputDevice(camera_params);
     ring_analog_device = Common::Input::CreateInputDevice(ring_params);
     nfc_devices = Common::Input::CreateInputDevice(nfc_params);
@@ -324,6 +331,19 @@ void EmulatedController::ReloadInput() {
         battery_devices[index]->ForceUpdate();
     }
 
+    for (std::size_t index = 0; index < color_devices.size(); ++index) {
+        if (!color_devices[index]) {
+            continue;
+        }
+        color_devices[index]->SetCallback({
+            .on_change =
+                [this, index](const Common::Input::CallbackStatus& callback) {
+                    SetColors(callback, index);
+                },
+        });
+        color_devices[index]->ForceUpdate();
+    }
+
     for (std::size_t index = 0; index < motion_devices.size(); ++index) {
         if (!motion_devices[index]) {
             continue;
@@ -429,6 +449,9 @@ void EmulatedController::UnloadInput() {
     for (auto& battery : battery_devices) {
         battery.reset();
     }
+    for (auto& color : color_devices) {
+        color.reset();
+    }
     for (auto& output : output_devices) {
         output.reset();
     }
@@ -458,6 +481,11 @@ void EmulatedController::EnableConfiguration() {
 void EmulatedController::DisableConfiguration() {
     is_configuring = false;
 
+    // Get Joycon colors before turning on the controller
+    for (const auto& color_device : color_devices) {
+        color_device->ForceUpdate();
+    }
+
     // Apply temporary npad type to the real controller
     if (tmp_npad_type != npad_type) {
         if (is_connected) {
@@ -926,6 +954,58 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback
     TriggerOnChange(ControllerTriggerType::Motion, true);
 }
 
+void EmulatedController::SetColors(const Common::Input::CallbackStatus& callback,
+                                   std::size_t index) {
+    if (index >= controller.color_values.size()) {
+        return;
+    }
+    std::unique_lock lock{mutex};
+    controller.color_values[index] = TransformToColor(callback);
+
+    if (is_configuring) {
+        lock.unlock();
+        TriggerOnChange(ControllerTriggerType::Color, false);
+        return;
+    }
+
+    if (controller.color_values[index].body == 0) {
+        return;
+    }
+
+    controller.colors_state.fullkey = {
+        .body = GetNpadColor(controller.color_values[index].body),
+        .button = GetNpadColor(controller.color_values[index].buttons),
+    };
+    if (npad_type == NpadStyleIndex::ProController) {
+        controller.colors_state.left = {
+            .body = GetNpadColor(controller.color_values[index].left_grip),
+            .button = GetNpadColor(controller.color_values[index].buttons),
+        };
+        controller.colors_state.right = {
+            .body = GetNpadColor(controller.color_values[index].right_grip),
+            .button = GetNpadColor(controller.color_values[index].buttons),
+        };
+    } else {
+        switch (index) {
+        case LeftIndex:
+            controller.colors_state.left = {
+                .body = GetNpadColor(controller.color_values[index].body),
+                .button = GetNpadColor(controller.color_values[index].buttons),
+            };
+            break;
+        case RightIndex:
+            controller.colors_state.right = {
+                .body = GetNpadColor(controller.color_values[index].body),
+                .button = GetNpadColor(controller.color_values[index].buttons),
+            };
+            break;
+        }
+    }
+
+    lock.unlock();
+    TriggerOnChange(ControllerTriggerType::Color, true);
+}
+
 void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callback,
                                     std::size_t index) {
     if (index >= controller.battery_values.size()) {
diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h
index aed331a1a..d044cc36b 100644
--- a/src/core/hid/emulated_controller.h
+++ b/src/core/hid/emulated_controller.h
@@ -35,6 +35,8 @@ using ControllerMotionDevices =
     std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeMotion::NumMotions>;
 using TriggerDevices =
     std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeTrigger::NumTriggers>;
+using ColorDevices =
+    std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
 using BatteryDevices =
     std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
 using CameraDevices = std::unique_ptr<Common::Input::InputDevice>;
@@ -46,6 +48,7 @@ using ButtonParams = std::array<Common::ParamPackage, Settings::NativeButton::Nu
 using StickParams = std::array<Common::ParamPackage, Settings::NativeAnalog::NumAnalogs>;
 using ControllerMotionParams = std::array<Common::ParamPackage, Settings::NativeMotion::NumMotions>;
 using TriggerParams = std::array<Common::ParamPackage, Settings::NativeTrigger::NumTriggers>;
+using ColorParams = std::array<Common::ParamPackage, max_emulated_controllers>;
 using BatteryParams = std::array<Common::ParamPackage, max_emulated_controllers>;
 using CameraParams = Common::ParamPackage;
 using RingAnalogParams = Common::ParamPackage;
@@ -457,6 +460,13 @@ private:
      */
     void SetMotion(const Common::Input::CallbackStatus& callback, std::size_t index);
 
+    /**
+     * Updates the color status of the controller
+     * @param callback A CallbackStatus containing the color status
+     * @param index color ID of the to be updated
+     */
+    void SetColors(const Common::Input::CallbackStatus& callback, std::size_t index);
+
     /**
      * Updates the battery status of the controller
      * @param callback A CallbackStatus containing the battery status
@@ -515,6 +525,7 @@ private:
     ControllerMotionParams motion_params;
     TriggerParams trigger_params;
     BatteryParams battery_params;
+    ColorParams color_params;
     CameraParams camera_params;
     RingAnalogParams ring_params;
     NfcParams nfc_params;
@@ -525,6 +536,7 @@ private:
     ControllerMotionDevices motion_devices;
     TriggerDevices trigger_devices;
     BatteryDevices battery_devices;
+    ColorDevices color_devices;
     CameraDevices camera_devices;
     RingAnalogDevice ring_analog_device;
     NfcDevices nfc_devices;
diff --git a/src/core/hid/input_converter.cpp b/src/core/hid/input_converter.cpp
index 502692875..d7e253044 100644
--- a/src/core/hid/input_converter.cpp
+++ b/src/core/hid/input_converter.cpp
@@ -304,6 +304,20 @@ Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& cal
     return nfc;
 }
 
+Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback) {
+    Common::Input::BodyColorStatus color{};
+    switch (callback.type) {
+    case Common::Input::InputType::Color:
+        color = callback.color_status;
+        break;
+    default:
+        LOG_ERROR(Input, "Conversion from type {} to color not implemented", callback.type);
+        break;
+    }
+
+    return color;
+}
+
 void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) {
     const auto& properties = analog.properties;
     float& raw_value = analog.raw_value;
diff --git a/src/core/hid/input_converter.h b/src/core/hid/input_converter.h
index b7eb6e660..c51c03e57 100644
--- a/src/core/hid/input_converter.h
+++ b/src/core/hid/input_converter.h
@@ -88,10 +88,18 @@ Common::Input::CameraStatus TransformToCamera(const Common::Input::CallbackStatu
  * Converts raw input data into a valid nfc status.
  *
  * @param callback Supported callbacks: Nfc.
- * @return A valid CameraObject object.
+ * @return A valid data tag vector.
  */
 Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& callback);
 
+/**
+ * Converts raw input data into a valid color status.
+ *
+ * @param callback Supported callbacks: Color.
+ * @return A valid Color object.
+ */
+Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback);
+
 /**
  * Converts raw analog data into a valid analog value
  * @param analog An analog object containing raw data and properties
diff --git a/src/input_common/drivers/joycon.cpp b/src/input_common/drivers/joycon.cpp
index 1fca11d34..c6f78c989 100644
--- a/src/input_common/drivers/joycon.cpp
+++ b/src/input_common/drivers/joycon.cpp
@@ -335,7 +335,16 @@ void Joycons::OnBatteryUpdate(std::size_t port, Joycon::ControllerType type,
 }
 
 void Joycons::OnColorUpdate(std::size_t port, Joycon::ControllerType type,
-                            const Joycon::Color& value) {}
+                            const Joycon::Color& value) {
+    const auto identifier = GetIdentifier(port, type);
+    Common::Input::BodyColorStatus color{
+        .body = value.body,
+        .buttons = value.buttons,
+        .left_grip = value.left_grip,
+        .right_grip = value.right_grip,
+    };
+    SetColor(identifier, color);
+}
 
 void Joycons::OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value) {
     const auto identifier = GetIdentifier(port, type);
diff --git a/src/input_common/input_engine.cpp b/src/input_common/input_engine.cpp
index 61cfd0911..91aa96aa7 100644
--- a/src/input_common/input_engine.cpp
+++ b/src/input_common/input_engine.cpp
@@ -79,6 +79,17 @@ void InputEngine::SetBattery(const PadIdentifier& identifier, Common::Input::Bat
     TriggerOnBatteryChange(identifier, value);
 }
 
+void InputEngine::SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value) {
+    {
+        std::scoped_lock lock{mutex};
+        ControllerData& controller = controller_list.at(identifier);
+        if (!configuring) {
+            controller.color = value;
+        }
+    }
+    TriggerOnColorChange(identifier, value);
+}
+
 void InputEngine::SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value) {
     {
         std::scoped_lock lock{mutex};
@@ -176,6 +187,18 @@ Common::Input::BatteryLevel InputEngine::GetBattery(const PadIdentifier& identif
     return controller.battery;
 }
 
+Common::Input::BodyColorStatus InputEngine::GetColor(const PadIdentifier& identifier) const {
+    std::scoped_lock lock{mutex};
+    const auto controller_iter = controller_list.find(identifier);
+    if (controller_iter == controller_list.cend()) {
+        LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
+                  identifier.pad, identifier.port);
+        return {};
+    }
+    const ControllerData& controller = controller_iter->second;
+    return controller.color;
+}
+
 BasicMotion InputEngine::GetMotion(const PadIdentifier& identifier, int motion) const {
     std::scoped_lock lock{mutex};
     const auto controller_iter = controller_list.find(identifier);
@@ -328,6 +351,20 @@ void InputEngine::TriggerOnBatteryChange(const PadIdentifier& identifier,
     }
 }
 
+void InputEngine::TriggerOnColorChange(const PadIdentifier& identifier,
+                                       [[maybe_unused]] Common::Input::BodyColorStatus value) {
+    std::scoped_lock lock{mutex_callback};
+    for (const auto& poller_pair : callback_list) {
+        const InputIdentifier& poller = poller_pair.second;
+        if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Color, 0)) {
+            continue;
+        }
+        if (poller.callback.on_change) {
+            poller.callback.on_change();
+        }
+    }
+}
+
 void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
                                         const BasicMotion& value) {
     std::scoped_lock lock{mutex_callback};
diff --git a/src/input_common/input_engine.h b/src/input_common/input_engine.h
index 6cbcf5207..6301c5719 100644
--- a/src/input_common/input_engine.h
+++ b/src/input_common/input_engine.h
@@ -40,6 +40,7 @@ enum class EngineInputType {
     Battery,
     Button,
     Camera,
+    Color,
     HatButton,
     Motion,
     Nfc,
@@ -199,6 +200,7 @@ public:
     bool GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const;
     f32 GetAxis(const PadIdentifier& identifier, int axis) const;
     Common::Input::BatteryLevel GetBattery(const PadIdentifier& identifier) const;
+    Common::Input::BodyColorStatus GetColor(const PadIdentifier& identifier) const;
     BasicMotion GetMotion(const PadIdentifier& identifier, int motion) const;
     Common::Input::CameraStatus GetCamera(const PadIdentifier& identifier) const;
     Common::Input::NfcStatus GetNfc(const PadIdentifier& identifier) const;
@@ -212,6 +214,7 @@ protected:
     void SetHatButton(const PadIdentifier& identifier, int button, u8 value);
     void SetAxis(const PadIdentifier& identifier, int axis, f32 value);
     void SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel value);
+    void SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value);
     void SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value);
     void SetCamera(const PadIdentifier& identifier, const Common::Input::CameraStatus& value);
     void SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value);
@@ -227,6 +230,7 @@ private:
         std::unordered_map<int, float> axes;
         std::unordered_map<int, BasicMotion> motions;
         Common::Input::BatteryLevel battery{};
+        Common::Input::BodyColorStatus color{};
         Common::Input::CameraStatus camera{};
         Common::Input::NfcStatus nfc{};
     };
@@ -235,6 +239,8 @@ private:
     void TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value);
     void TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value);
     void TriggerOnBatteryChange(const PadIdentifier& identifier, Common::Input::BatteryLevel value);
+    void TriggerOnColorChange(const PadIdentifier& identifier,
+                              Common::Input::BodyColorStatus value);
     void TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
                                const BasicMotion& value);
     void TriggerOnCameraChange(const PadIdentifier& identifier,
diff --git a/src/input_common/input_poller.cpp b/src/input_common/input_poller.cpp
index fb8be42e2..368ffbdd5 100644
--- a/src/input_common/input_poller.cpp
+++ b/src/input_common/input_poller.cpp
@@ -498,6 +498,58 @@ private:
     InputEngine* input_engine;
 };
 
+class InputFromColor final : public Common::Input::InputDevice {
+public:
+    explicit InputFromColor(PadIdentifier identifier_, InputEngine* input_engine_)
+        : identifier(identifier_), input_engine(input_engine_) {
+        UpdateCallback engine_callback{[this]() { OnChange(); }};
+        const InputIdentifier input_identifier{
+            .identifier = identifier,
+            .type = EngineInputType::Color,
+            .index = 0,
+            .callback = engine_callback,
+        };
+        last_color_value = {};
+        callback_key = input_engine->SetCallback(input_identifier);
+    }
+
+    ~InputFromColor() override {
+        input_engine->DeleteCallback(callback_key);
+    }
+
+    Common::Input::BodyColorStatus GetStatus() const {
+        return input_engine->GetColor(identifier);
+    }
+
+    void ForceUpdate() override {
+        const Common::Input::CallbackStatus status{
+            .type = Common::Input::InputType::Color,
+            .color_status = GetStatus(),
+        };
+
+        last_color_value = status.color_status;
+        TriggerOnChange(status);
+    }
+
+    void OnChange() {
+        const Common::Input::CallbackStatus status{
+            .type = Common::Input::InputType::Color,
+            .color_status = GetStatus(),
+        };
+
+        if (status.color_status.body != last_color_value.body) {
+            last_color_value = status.color_status;
+            TriggerOnChange(status);
+        }
+    }
+
+private:
+    const PadIdentifier identifier;
+    int callback_key;
+    Common::Input::BodyColorStatus last_color_value;
+    InputEngine* input_engine;
+};
+
 class InputFromMotion final : public Common::Input::InputDevice {
 public:
     explicit InputFromMotion(PadIdentifier identifier_, int motion_sensor_, float gyro_threshold_,
@@ -966,6 +1018,18 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateBatteryDevice(
     return std::make_unique<InputFromBattery>(identifier, input_engine.get());
 }
 
+std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateColorDevice(
+    const Common::ParamPackage& params) {
+    const PadIdentifier identifier = {
+        .guid = Common::UUID{params.Get("guid", "")},
+        .port = static_cast<std::size_t>(params.Get("port", 0)),
+        .pad = static_cast<std::size_t>(params.Get("pad", 0)),
+    };
+
+    input_engine->PreSetController(identifier);
+    return std::make_unique<InputFromColor>(identifier, input_engine.get());
+}
+
 std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateMotionDevice(
     Common::ParamPackage params) {
     const PadIdentifier identifier = {
@@ -1053,6 +1117,9 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::Create(
     if (params.Has("battery")) {
         return CreateBatteryDevice(params);
     }
+    if (params.Has("color")) {
+        return CreateColorDevice(params);
+    }
     if (params.Has("camera")) {
         return CreateCameraDevice(params);
     }
diff --git a/src/input_common/input_poller.h b/src/input_common/input_poller.h
index d7db13ce4..e097e254c 100644
--- a/src/input_common/input_poller.h
+++ b/src/input_common/input_poller.h
@@ -190,6 +190,17 @@ private:
     std::unique_ptr<Common::Input::InputDevice> CreateBatteryDevice(
         const Common::ParamPackage& params);
 
+    /**
+     * Creates a color device from the parameters given.
+     * @param params contains parameters for creating the device:
+     *               - "guid": text string for identifying controllers
+     *               - "port": port of the connected device
+     *               - "pad": slot of the connected controller
+     * @returns a unique input device with the parameters specified
+     */
+    std::unique_ptr<Common::Input::InputDevice> CreateColorDevice(
+        const Common::ParamPackage& params);
+
     /**
      * Creates a motion device from the parameters given.
      * @param params contains parameters for creating the device: