diff --git a/src/input_common/input_poller.cpp b/src/input_common/input_poller.cpp
index ffb9b945e5..a8eb1442b1 100644
--- a/src/input_common/input_poller.cpp
+++ b/src/input_common/input_poller.cpp
@@ -705,6 +705,47 @@ private:
     InputEngine* input_engine;
 };
 
+class InputFromNfc final : public Common::Input::InputDevice {
+public:
+    explicit InputFromNfc(PadIdentifier identifier_, InputEngine* input_engine_)
+        : identifier(identifier_), input_engine(input_engine_) {
+        UpdateCallback engine_callback{[this]() { OnChange(); }};
+        const InputIdentifier input_identifier{
+            .identifier = identifier,
+            .type = EngineInputType::Nfc,
+            .index = 0,
+            .callback = engine_callback,
+        };
+        callback_key = input_engine->SetCallback(input_identifier);
+    }
+
+    ~InputFromNfc() override {
+        input_engine->DeleteCallback(callback_key);
+    }
+
+    Common::Input::NfcStatus GetStatus() const {
+        return input_engine->GetNfc(identifier);
+    }
+
+    void ForceUpdate() override {
+        OnChange();
+    }
+
+    void OnChange() {
+        const Common::Input::CallbackStatus status{
+            .type = Common::Input::InputType::Nfc,
+            .nfc_status = GetStatus(),
+        };
+
+        TriggerOnChange(status);
+    }
+
+private:
+    const PadIdentifier identifier;
+    int callback_key;
+    InputEngine* input_engine;
+};
+
 class OutputFromIdentifier final : public Common::Input::OutputDevice {
 public:
     explicit OutputFromIdentifier(PadIdentifier identifier_, InputEngine* input_engine_)
@@ -727,6 +768,14 @@ public:
         return input_engine->SetCameraFormat(identifier, camera_format);
     }
 
+    Common::Input::NfcState SupportsNfc() override {
+        return input_engine->SupportsNfc(identifier);
+    }
+
+    Common::Input::NfcState WriteNfcData(const std::vector<u8>& data) override {
+        return input_engine->WriteNfcData(identifier, data);
+    }
+
 private:
     const PadIdentifier identifier;
     InputEngine* input_engine;
@@ -978,6 +1027,18 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateCameraDevice(
     return std::make_unique<InputFromCamera>(identifier, input_engine.get());
 }
 
+std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateNfcDevice(
+    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<InputFromNfc>(identifier, input_engine.get());
+}
+
 InputFactory::InputFactory(std::shared_ptr<InputEngine> input_engine_)
     : input_engine(std::move(input_engine_)) {}
 
@@ -989,6 +1050,9 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::Create(
     if (params.Has("camera")) {
         return CreateCameraDevice(params);
     }
+    if (params.Has("nfc")) {
+        return CreateNfcDevice(params);
+    }
     if (params.Has("button") && params.Has("axis")) {
         return CreateTriggerDevice(params);
     }
diff --git a/src/input_common/input_poller.h b/src/input_common/input_poller.h
index 4410a84154..d7db13ce42 100644
--- a/src/input_common/input_poller.h
+++ b/src/input_common/input_poller.h
@@ -222,6 +222,16 @@ private:
     std::unique_ptr<Common::Input::InputDevice> CreateCameraDevice(
         const Common::ParamPackage& params);
 
+    /**
+     * Creates a nfc 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> CreateNfcDevice(const Common::ParamPackage& params);
+
     std::shared_ptr<InputEngine> input_engine;
 };
 } // namespace InputCommon
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index 75a57b9fc6..b2064ef955 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -11,6 +11,7 @@
 #include "input_common/drivers/tas_input.h"
 #include "input_common/drivers/touch_screen.h"
 #include "input_common/drivers/udp_client.h"
+#include "input_common/drivers/virtual_amiibo.h"
 #include "input_common/helpers/stick_from_buttons.h"
 #include "input_common/helpers/touch_from_buttons.h"
 #include "input_common/input_engine.h"
@@ -87,6 +88,15 @@ struct InputSubsystem::Impl {
         Common::Input::RegisterFactory<Common::Input::OutputDevice>(camera->GetEngineName(),
                                                                     camera_output_factory);
 
+        virtual_amiibo = std::make_shared<VirtualAmiibo>("virtual_amiibo");
+        virtual_amiibo->SetMappingCallback(mapping_callback);
+        virtual_amiibo_input_factory = std::make_shared<InputFactory>(virtual_amiibo);
+        virtual_amiibo_output_factory = std::make_shared<OutputFactory>(virtual_amiibo);
+        Common::Input::RegisterFactory<Common::Input::InputDevice>(virtual_amiibo->GetEngineName(),
+                                                                   virtual_amiibo_input_factory);
+        Common::Input::RegisterFactory<Common::Input::OutputDevice>(virtual_amiibo->GetEngineName(),
+                                                                    virtual_amiibo_output_factory);
+
 #ifdef HAVE_SDL2
         sdl = std::make_shared<SDLDriver>("sdl");
         sdl->SetMappingCallback(mapping_callback);
@@ -327,6 +337,7 @@ struct InputSubsystem::Impl {
     std::shared_ptr<TasInput::Tas> tas_input;
     std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
     std::shared_ptr<Camera> camera;
+    std::shared_ptr<VirtualAmiibo> virtual_amiibo;
 
     std::shared_ptr<InputFactory> keyboard_factory;
     std::shared_ptr<InputFactory> mouse_factory;
@@ -335,6 +346,7 @@ struct InputSubsystem::Impl {
     std::shared_ptr<InputFactory> udp_client_input_factory;
     std::shared_ptr<InputFactory> tas_input_factory;
     std::shared_ptr<InputFactory> camera_input_factory;
+    std::shared_ptr<InputFactory> virtual_amiibo_input_factory;
 
     std::shared_ptr<OutputFactory> keyboard_output_factory;
     std::shared_ptr<OutputFactory> mouse_output_factory;
@@ -342,6 +354,7 @@ struct InputSubsystem::Impl {
     std::shared_ptr<OutputFactory> udp_client_output_factory;
     std::shared_ptr<OutputFactory> tas_output_factory;
     std::shared_ptr<OutputFactory> camera_output_factory;
+    std::shared_ptr<OutputFactory> virtual_amiibo_output_factory;
 
 #ifdef HAVE_SDL2
     std::shared_ptr<SDLDriver> sdl;
@@ -402,6 +415,14 @@ const Camera* InputSubsystem::GetCamera() const {
     return impl->camera.get();
 }
 
+VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() {
+    return impl->virtual_amiibo.get();
+}
+
+const VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() const {
+    return impl->virtual_amiibo.get();
+}
+
 std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
     return impl->GetInputDevices();
 }
diff --git a/src/input_common/main.h b/src/input_common/main.h
index 9a969e747b..ced2523839 100644
--- a/src/input_common/main.h
+++ b/src/input_common/main.h
@@ -33,6 +33,7 @@ class Camera;
 class Keyboard;
 class Mouse;
 class TouchScreen;
+class VirtualAmiibo;
 struct MappingData;
 } // namespace InputCommon
 
@@ -101,6 +102,12 @@ public:
     /// Retrieves the underlying camera input device.
     [[nodiscard]] const Camera* GetCamera() const;
 
+    /// Retrieves the underlying virtual amiibo input device.
+    [[nodiscard]] VirtualAmiibo* GetVirtualAmiibo();
+
+    /// Retrieves the underlying virtual amiibo input device.
+    [[nodiscard]] const VirtualAmiibo* GetVirtualAmiibo() const;
+
     /**
      * Returns all available input devices that this Factory can create a new device with.
      * Each returned ParamPackage should have a `display` field used for display, a `engine` field