diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 742b72856..f430525f8 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -98,11 +98,13 @@ add_executable(yuzu
     game_list_p.h
     game_list_worker.cpp
     game_list_worker.h
+    hotkeys.cpp
+    hotkeys.h
+    install_dialog.cpp
+    install_dialog.h
     loading_screen.cpp
     loading_screen.h
     loading_screen.ui
-    hotkeys.cpp
-    hotkeys.h
     main.cpp
     main.h
     main.ui
diff --git a/src/yuzu/install_dialog.cpp b/src/yuzu/install_dialog.cpp
new file mode 100644
index 000000000..fac158c25
--- /dev/null
+++ b/src/yuzu/install_dialog.cpp
@@ -0,0 +1,72 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QCheckBox>
+#include <QDialogButtonBox>
+#include <QFileInfo>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QListWidget>
+#include <QVBoxLayout>
+#include "yuzu/install_dialog.h"
+#include "yuzu/uisettings.h"
+
+InstallDialog::InstallDialog(QWidget* parent, const QStringList& files) : QDialog(parent) {
+    file_list = new QListWidget(this);
+
+    for (const QString& file : files) {
+        QListWidgetItem* item = new QListWidgetItem(QFileInfo(file).fileName(), file_list);
+        item->setData(Qt::UserRole, file);
+        item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+        item->setCheckState(Qt::Checked);
+    }
+
+    file_list->setMinimumWidth((file_list->sizeHintForColumn(0) * 6) / 5);
+
+    vbox_layout = new QVBoxLayout;
+
+    hbox_layout = new QHBoxLayout;
+
+    description = new QLabel(tr("Please confirm these are the files you wish to install."));
+
+    overwrite_files = new QCheckBox(tr("Overwrite Existing Files"));
+    overwrite_files->setCheckState(Qt::Unchecked);
+
+    buttons = new QDialogButtonBox;
+    buttons->addButton(QDialogButtonBox::Cancel);
+    buttons->addButton(tr("Install"), QDialogButtonBox::AcceptRole);
+
+    connect(buttons, &QDialogButtonBox::accepted, this, &InstallDialog::accept);
+    connect(buttons, &QDialogButtonBox::rejected, this, &InstallDialog::reject);
+
+    hbox_layout->addWidget(overwrite_files);
+    hbox_layout->addWidget(buttons);
+
+    vbox_layout->addWidget(description);
+    vbox_layout->addWidget(file_list);
+    vbox_layout->addLayout(hbox_layout);
+
+    setLayout(vbox_layout);
+    setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+    setWindowTitle(tr("Install Files to NAND"));
+}
+
+InstallDialog::~InstallDialog() = default;
+
+QStringList InstallDialog::GetFilenames() const {
+    QStringList filenames;
+
+    for (int i = 0; i < file_list->count(); ++i) {
+        const QListWidgetItem* item = file_list->item(i);
+        if (item->checkState() == Qt::Checked) {
+            filenames.append(item->data(Qt::UserRole).toString());
+        }
+    }
+
+    return filenames;
+}
+
+bool InstallDialog::ShouldOverwriteFiles() const {
+    return overwrite_files->isChecked();
+}
\ No newline at end of file
diff --git a/src/yuzu/install_dialog.h b/src/yuzu/install_dialog.h
new file mode 100644
index 000000000..3eaa9e60a
--- /dev/null
+++ b/src/yuzu/install_dialog.h
@@ -0,0 +1,35 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QDialog>
+
+class QCheckBox;
+class QDialogButtonBox;
+class QHBoxLayout;
+class QLabel;
+class QListWidget;
+class QVBoxLayout;
+
+class InstallDialog : public QDialog {
+    Q_OBJECT
+
+public:
+    explicit InstallDialog(QWidget* parent, const QStringList& files);
+    ~InstallDialog() override;
+
+    QStringList GetFilenames() const;
+    bool ShouldOverwriteFiles() const;
+
+private:
+    QListWidget* file_list;
+
+    QVBoxLayout* vbox_layout;
+    QHBoxLayout* hbox_layout;
+
+    QLabel* description;
+    QCheckBox* overwrite_files;
+    QDialogButtonBox* buttons;
+};
\ No newline at end of file
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 4d501a8f9..45ddc3baf 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -107,6 +107,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
 #include "yuzu/game_list.h"
 #include "yuzu/game_list_p.h"
 #include "yuzu/hotkeys.h"
+#include "yuzu/install_dialog.h"
 #include "yuzu/loading_screen.h"
 #include "yuzu/main.h"
 #include "yuzu/uisettings.h"
@@ -1596,38 +1597,67 @@ void GMainWindow::OnMenuLoadFolder() {
 void GMainWindow::OnMenuInstallToNAND() {
     const QString file_filter =
         tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
-           "(*.nca);;Nintendo Submissions Package (*.nsp);;NX Cartridge "
+           "(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
            "Image (*.xci)");
-    QString filename = QFileDialog::getOpenFileName(this, tr("Install File"),
-                                                    UISettings::values.roms_path, file_filter);
+    QStringList files = QFileDialog::getOpenFileNames(this, tr("Install Files"),
+                                                      UISettings::values.roms_path, file_filter);
 
-    if (filename.isEmpty()) {
+    if (files.isEmpty()) {
         return;
     }
 
-    const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
-                                    const FileSys::VirtualFile& dest, std::size_t block_size) {
-        if (src == nullptr || dest == nullptr)
+    InstallDialog installDialog(this, files);
+    if (installDialog.exec() == QDialog::Rejected) {
+        return;
+    }
+
+    const QStringList filenames = installDialog.GetFilenames();
+    const bool overwrite_files = installDialog.ShouldOverwriteFiles();
+
+    int count = 0;
+    int total_count = filenames.size();
+    bool is_progressdialog_created = false;
+
+    const auto qt_raw_copy = [this, &count, &total_count, &is_progressdialog_created](
+                                 const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
+                                 std::size_t block_size) {
+        if (src == nullptr || dest == nullptr) {
             return false;
-        if (!dest->Resize(src->GetSize()))
+        }
+        if (!dest->Resize(src->GetSize())) {
             return false;
+        }
 
         std::array<u8, 0x1000> buffer{};
         const int progress_maximum = static_cast<int>(src->GetSize() / buffer.size());
 
-        QProgressDialog progress(
-            tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())),
-            tr("Cancel"), 0, progress_maximum, this);
-        progress.setWindowModality(Qt::WindowModal);
+        if (!is_progressdialog_created) {
+            ui.action_Install_File_NAND->setEnabled(false);
+            install_progress = new QProgressDialog(
+                tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())),
+                tr("Cancel"), 0, progress_maximum, this);
+            install_progress->setWindowTitle(
+                tr("%n file(s) remaining", "", total_count - count - 1));
+            install_progress->setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint &
+                                             ~Qt::WindowMaximizeButtonHint);
+            install_progress->setAutoClose(false);
+            is_progressdialog_created = true;
+        } else {
+            install_progress->setWindowTitle(
+                tr("%n file(s) remaining", "", total_count - count - 1));
+            install_progress->setLabelText(
+                tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())));
+            install_progress->setMaximum(progress_maximum);
+        }
 
         for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
-            if (progress.wasCanceled()) {
+            if (install_progress->wasCanceled()) {
                 dest->Resize(0);
                 return false;
             }
 
             const int progress_value = static_cast<int>(i / buffer.size());
-            progress.setValue(progress_value);
+            install_progress->setValue(progress_value);
 
             const auto read = src->Read(buffer.data(), buffer.size(), i);
             dest->Write(buffer.data(), read, i);
@@ -1636,143 +1666,166 @@ void GMainWindow::OnMenuInstallToNAND() {
         return true;
     };
 
-    const auto success = [this]() {
+    const auto success = [this, &count, &is_progressdialog_created]() {
+        if (is_progressdialog_created) {
+            install_progress->close();
+        }
         QMessageBox::information(this, tr("Successfully Installed"),
-                                 tr("The file was successfully installed."));
+                                 tr("%n file(s) successfully installed", "", count));
         game_list->PopulateAsync(UISettings::values.game_dirs);
         FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
                                        DIR_SEP + "game_list");
+        ui.action_Install_File_NAND->setEnabled(true);
     };
 
-    const auto failed = [this]() {
+    const auto failed = [this, &is_progressdialog_created](const QString& file) {
+        if (is_progressdialog_created) {
+            install_progress->close();
+        }
         QMessageBox::warning(
-            this, tr("Failed to Install"),
+            this, tr("Failed to Install %1").arg(QFileInfo(file).fileName()),
             tr("There was an error while attempting to install the provided file. It "
                "could have an incorrect format or be missing metadata. Please "
                "double-check your file and try again."));
+        game_list->PopulateAsync(UISettings::values.game_dirs);
+        ui.action_Install_File_NAND->setEnabled(true);
     };
 
-    const auto overwrite = [this]() {
-        return QMessageBox::question(this, tr("Failed to Install"),
-                                     tr("The file you are attempting to install already exists "
-                                        "in the cache. Would you like to overwrite it?")) ==
-               QMessageBox::Yes;
+    const auto overwrite = [this](const QString& file) {
+        return QMessageBox::question(
+                   this, tr("Failed to Install %1").arg(QFileInfo(file).fileName()),
+                   tr("The file you are attempting to install already exists "
+                      "in the cache. Would you like to overwrite it?")) == QMessageBox::Yes;
     };
 
-    if (filename.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
-        filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
-        std::shared_ptr<FileSys::NSP> nsp;
-        if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
-            nsp = std::make_shared<FileSys::NSP>(
-                vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
-            if (nsp->IsExtractedType())
-                failed();
-        } else {
-            const auto xci = std::make_shared<FileSys::XCI>(
-                vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
-            nsp = xci->GetSecurePartitionNSP();
-        }
+    for (const QString& filename : filenames) {
+        if (filename.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
+            filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
+            std::shared_ptr<FileSys::NSP> nsp;
+            if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
+                nsp = std::make_shared<FileSys::NSP>(
+                    vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
+                if (nsp->IsExtractedType()) {
+                    failed(filename);
+                    break;
+                }
+            } else {
+                const auto xci = std::make_shared<FileSys::XCI>(
+                    vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
+                nsp = xci->GetSecurePartitionNSP();
+            }
 
-        if (nsp->GetStatus() != Loader::ResultStatus::Success) {
-            failed();
-            return;
-        }
-        const auto res = Core::System::GetInstance()
-                             .GetFileSystemController()
-                             .GetUserNANDContents()
-                             ->InstallEntry(*nsp, false, qt_raw_copy);
-        if (res == FileSys::InstallResult::Success) {
-            success();
-        } else {
-            if (res == FileSys::InstallResult::ErrorAlreadyExists) {
-                if (overwrite()) {
+            if (nsp->GetStatus() != Loader::ResultStatus::Success) {
+                failed(filename);
+                break;
+            }
+            const auto res = Core::System::GetInstance()
+                                 .GetFileSystemController()
+                                 .GetUserNANDContents()
+                                 ->InstallEntry(*nsp, false, qt_raw_copy);
+            if (res == FileSys::InstallResult::Success) {
+                ++count;
+            } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
+                if (overwrite_files && overwrite(filename)) {
                     const auto res2 = Core::System::GetInstance()
                                           .GetFileSystemController()
                                           .GetUserNANDContents()
                                           ->InstallEntry(*nsp, true, qt_raw_copy);
-                    if (res2 == FileSys::InstallResult::Success) {
-                        success();
-                    } else {
-                        failed();
+                    if (res2 != FileSys::InstallResult::Success) {
+                        failed(filename);
+                        break;
                     }
+                    ++count;
+                } else {
+                    --total_count;
                 }
             } else {
-                failed();
+                failed(filename);
+                break;
             }
-        }
-    } else {
-        const auto nca = std::make_shared<FileSys::NCA>(
-            vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
-        const auto id = nca->GetStatus();
-
-        // Game updates necessary are missing base RomFS
-        if (id != Loader::ResultStatus::Success &&
-            id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
-            failed();
-            return;
-        }
-
-        const QStringList tt_options{tr("System Application"),
-                                     tr("System Archive"),
-                                     tr("System Application Update"),
-                                     tr("Firmware Package (Type A)"),
-                                     tr("Firmware Package (Type B)"),
-                                     tr("Game"),
-                                     tr("Game Update"),
-                                     tr("Game DLC"),
-                                     tr("Delta Title")};
-        bool ok;
-        const auto item = QInputDialog::getItem(
-            this, tr("Select NCA Install Type..."),
-            tr("Please select the type of title you would like to install this NCA as:\n(In "
-               "most instances, the default 'Game' is fine.)"),
-            tt_options, 5, false, &ok);
-
-        auto index = tt_options.indexOf(item);
-        if (!ok || index == -1) {
-            QMessageBox::warning(this, tr("Failed to Install"),
-                                 tr("The title type you selected for the NCA is invalid."));
-            return;
-        }
-
-        // If index is equal to or past Game, add the jump in TitleType.
-        if (index >= 5) {
-            index += static_cast<size_t>(FileSys::TitleType::Application) -
-                     static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
-        }
-
-        FileSys::InstallResult res;
-        if (index >= static_cast<s32>(FileSys::TitleType::Application)) {
-            res = Core::System::GetInstance()
-                      .GetFileSystemController()
-                      .GetUserNANDContents()
-                      ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
-                                     qt_raw_copy);
         } else {
-            res = Core::System::GetInstance()
-                      .GetFileSystemController()
-                      .GetSystemNANDContents()
-                      ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
-                                     qt_raw_copy);
-        }
+            const auto nca = std::make_shared<FileSys::NCA>(
+                vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
+            const auto id = nca->GetStatus();
 
-        if (res == FileSys::InstallResult::Success) {
-            success();
-        } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
-            if (overwrite()) {
-                const auto res2 = Core::System::GetInstance()
-                                      .GetFileSystemController()
-                                      .GetUserNANDContents()
-                                      ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index),
-                                                     true, qt_raw_copy);
-                if (res2 == FileSys::InstallResult::Success) {
-                    success();
+            // Game updates necessary are missing base RomFS
+            if (id != Loader::ResultStatus::Success &&
+                id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
+                failed(filename);
+                break;
+            }
+
+            const QStringList tt_options{tr("System Application"),
+                                         tr("System Archive"),
+                                         tr("System Application Update"),
+                                         tr("Firmware Package (Type A)"),
+                                         tr("Firmware Package (Type B)"),
+                                         tr("Game"),
+                                         tr("Game Update"),
+                                         tr("Game DLC"),
+                                         tr("Delta Title")};
+            bool ok;
+            const auto item = QInputDialog::getItem(
+                this, tr("Select NCA Install Type..."),
+                tr("Please select the type of title you would like to install this NCA as:\n(In "
+                   "most instances, the default 'Game' is fine.)"),
+                tt_options, 5, false, &ok);
+
+            auto index = tt_options.indexOf(item);
+            if (!ok || index == -1) {
+                QMessageBox::warning(this, tr("Failed to Install"),
+                                     tr("The title type you selected for the NCA is invalid."));
+                break;
+            }
+
+            // If index is equal to or past Game, add the jump in TitleType.
+            if (index >= 5) {
+                index += static_cast<size_t>(FileSys::TitleType::Application) -
+                         static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
+            }
+
+            FileSys::InstallResult res;
+            if (index >= static_cast<s32>(FileSys::TitleType::Application)) {
+                res = Core::System::GetInstance()
+                          .GetFileSystemController()
+                          .GetUserNANDContents()
+                          ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
+                                         qt_raw_copy);
+            } else {
+                res = Core::System::GetInstance()
+                          .GetFileSystemController()
+                          .GetSystemNANDContents()
+                          ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
+                                         qt_raw_copy);
+            }
+
+            if (res == FileSys::InstallResult::Success) {
+                ++count;
+            } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
+                if (overwrite_files && overwrite(filename)) {
+                    const auto res2 =
+                        Core::System::GetInstance()
+                            .GetFileSystemController()
+                            .GetUserNANDContents()
+                            ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true,
+                                           qt_raw_copy);
+                    if (res2 != FileSys::InstallResult::Success) {
+                        failed(filename);
+                        break;
+                    }
+                    ++count;
                 } else {
-                    failed();
+                    --total_count;
                 }
+            } else {
+                failed(filename);
+                break;
             }
-        } else {
-            failed();
+        }
+
+        // Return success only on the last file
+        if (filename == filenames.last()) {
+            success();
         }
     }
 }
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 8e3d39c38..55d072e96 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -28,6 +28,7 @@ class MicroProfileDialog;
 class ProfilerWidget;
 class QLabel;
 class QPushButton;
+class QProgressDialog;
 class WaitTreeWidget;
 enum class GameListOpenTarget;
 class GameListPlaceholder;
@@ -272,6 +273,9 @@ private:
 
     HotkeyRegistry hotkey_registry;
 
+    // Install to NAND progress dialog
+    QProgressDialog* install_progress;
+
 protected:
     void dropEvent(QDropEvent* event) override;
     void dragEnterEvent(QDragEnterEvent* event) override;
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui
index bee6e107e..c3a1d715e 100644
--- a/src/yuzu/main.ui
+++ b/src/yuzu/main.ui
@@ -130,7 +130,7 @@
     <bool>true</bool>
    </property>
    <property name="text">
-    <string>Install File to NAND...</string>
+    <string>Install Files to NAND...</string>
    </property>
   </action>
   <action name="action_Load_File">