diff --git a/bricktracker/instructions.py b/bricktracker/instructions.py
index 0dd7f8e..5f91836 100644
--- a/bricktracker/instructions.py
+++ b/bricktracker/instructions.py
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
 import logging
 import os
 from shutil import copyfileobj
+import traceback
 from typing import Tuple, TYPE_CHECKING
 
 from bs4 import BeautifulSoup
@@ -12,14 +13,16 @@ from werkzeug.datastructures import FileStorage
 from werkzeug.utils import secure_filename
 
 from .exceptions import ErrorException, DownloadException
-from .parser import parse_set
 if TYPE_CHECKING:
     from .rebrickable_set import RebrickableSet
+    from .socket import BrickSocket
 
 logger = logging.getLogger(__name__)
 
 
 class BrickInstructions(object):
+    socket: 'BrickSocket'
+
     allowed: bool
     rebrickable: 'RebrickableSet | None'
     extension: str
@@ -29,9 +32,22 @@ class BrickInstructions(object):
     name: str
     size: int
 
-    def __init__(self, file: os.DirEntry | str, /):
+    def __init__(
+        self,
+        file: os.DirEntry | str,
+        /,
+        *,
+        socket: 'BrickSocket | None' = None,
+    ):
+        # Save the socket
+        if socket is not None:
+            self.socket = socket
+
         if isinstance(file, str):
             self.filename = file
+
+            if self.filename == '':
+                raise ErrorException('An instruction filename cannot be empty')
         else:
             self.filename = file.name
 
@@ -73,31 +89,84 @@ class BrickInstructions(object):
 
     # Download an instruction file
     def download(self, path: str, /) -> None:
-        target = self.path(filename=secure_filename(self.filename))
+        try:
+            # Just to make sure that the progress is initiated
+            self.socket.progress(
+                message='Downloading {file}'.format(
+                    file=self.filename,
+                )
+            )
 
-        if os.path.isfile(target):
-            raise ErrorException('Cannot download {target} as it already exists'.format(  # noqa: E501
-                target=self.filename
-            ))
+            target = self.path(filename=secure_filename(self.filename))
 
-        url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format(  # noqa: E501
-            path=path
-        )
+            # Skipping rather than failing here
+            if os.path.isfile(target):
+                self.socket.complete(
+                    message='File {file} already exists, skipped'.format(
+                        file=self.filename,
+                    )
+                )
 
-        response = requests.get(url, stream=True)
-        if response.ok:
-            with open(target, 'wb') as f:
-                copyfileobj(response.raw, f)
-        else:
-            raise DownloadException('Failed to download {file}. Status code: {code}'.format(  # noqa: E501
-                file=self.filename,
-                code=response.status_code
-            ))
+            else:
+                url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format(  # noqa: E501
+                    path=path
+                )
 
-        # Info
-        logger.info('The instruction file {file} has been downloaded'.format(
-            file=self.filename
-        ))
+                # Request the file
+                self.socket.progress(
+                    message='Requesting {url}'.format(
+                        url=url,
+                    )
+                )
+
+                response = requests.get(url, stream=True)
+                if response.ok:
+
+                    # Store the content header as size
+                    try:
+                        self.size = int(
+                            response.headers.get('Content-length', 0)
+                        )
+                    except Exception:
+                        self.size = 0
+
+                    # Downloading the file
+                    self.socket.progress(
+                        message='Downloading {url} ({size})'.format(
+                            url=url,
+                            size=self.human_size(),
+                        )
+                    )
+
+                    with open(target, 'wb') as f:
+                        copyfileobj(response.raw, f)
+                else:
+                    raise DownloadException('failed to download: {code}'.format(  # noqa: E501
+                        code=response.status_code
+                    ))
+
+                # Info
+                logger.info('The instruction file {file} has been downloaded'.format(  # noqa: E501
+                    file=self.filename
+                ))
+
+                # Complete
+                self.socket.complete(
+                    message='File {file} downloaded ({size})'.format(  # noqa: E501
+                        file=self.filename,
+                        size=self.human_size()
+                    )
+                )
+
+        except Exception as e:
+            self.socket.fail(
+                message='Error while downloading instruction {file}: {error}'.format(  # noqa: E501
+                    file=self.filename,
+                    error=e,
+                )
+            )
+
+            logger.debug(traceback.format_exc())
 
     # Display the size in a human format
     def human_size(self) -> str:
@@ -175,36 +244,9 @@ class BrickInstructions(object):
         else:
             return 'file-line'
 
-    # Download selected instructions for a set
-    @staticmethod
-    def download_instructions(form: dict[str, str], /) -> None:
-        selected_instructions: list[Tuple[str, str]] = []
-
-        # Get the list of instructions
-        for key in form:
-            if key.startswith('instruction-') and form.get(key) == 'on':
-                _, _, index = key.partition('-')
-                alt_text = form.get(f'instruction-alt-text-{index}', '')
-                href_text = form.get(f'instruction-href-text-{index}', '').removeprefix('/instructions/')  # Remove the /instructions/ part  # noqa: E501
-                selected_instructions.append((href_text, alt_text))
-
-        # Raise if nothing selected
-        if not len(selected_instructions):
-            raise ErrorException('No instruction was selected to download')
-
-        # Loop over selected instructions and download them
-        for href, filename in selected_instructions:
-            BrickInstructions(f"{filename}.pdf").download(href)
-
     # Find the instructions for a set
     @staticmethod
-    def find_instructions(form: dict[str, str], /) -> list[Tuple[str, str]]:
-        # Grab the set ID
-        set: str = form.get('add-set', '')
-
-        # Parse it
-        set = parse_set(set)
-
+    def find_instructions(set: str, /) -> list[Tuple[str, str]]:
         response = requests.get(
             current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format(
                 path=set,
diff --git a/bricktracker/socket.py b/bricktracker/socket.py
index 4351592..1db6947 100644
--- a/bricktracker/socket.py
+++ b/bricktracker/socket.py
@@ -5,6 +5,8 @@ from flask import copy_current_request_context, Flask, request
 from flask_socketio import SocketIO
 
 from .configuration_list import BrickConfigurationList
+from .instructions import BrickInstructions
+from .instructions_list import BrickInstructionsList
 from .login import LoginManager
 from .set import BrickSet
 from .sql import close as sql_close
@@ -17,6 +19,7 @@ MESSAGES: Final[dict[str, str]] = {
     'COMPLETE': 'complete',
     'CONNECT': 'connect',
     'DISCONNECT': 'disconnect',
+    'DOWNLOAD_INSTRUCTIONS': 'download_instructions',
     'FAIL': 'fail',
     'IMPORT_SET': 'import_set',
     'LOAD_SET': 'load_set',
@@ -84,6 +87,41 @@ class BrickSocket(object):
         def disconnect() -> None:
             self.disconnected()
 
+        @self.socket.on(MESSAGES['DOWNLOAD_INSTRUCTIONS'], namespace=self.namespace)  # noqa: E501
+        def download_instructions(data: dict[str, Any], /) -> None:
+            # Needs to be authenticated
+            if LoginManager.is_not_authenticated():
+                self.fail(message='You need to be authenticated')
+                return
+
+            instructions = BrickInstructions(
+                '{name}.pdf'.format(name=data.get('alt', '')),
+                socket=self
+            )
+
+            path = data.get('href', '').removeprefix('/instructions/')
+
+            # Update the progress
+            try:
+                self.progress_total = int(data.get('total', 0))
+                self.progress_count = int(data.get('current', 0))
+            except Exception:
+                pass
+
+            # Start it in a thread if requested
+            if self.threaded:
+                @copy_current_request_context
+                def do_download() -> None:
+                    instructions.download(path)
+
+                    BrickInstructionsList(force=True)
+
+                self.socket.start_background_task(do_download)
+            else:
+                instructions.download(path)
+
+                BrickInstructionsList(force=True)
+
         @self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace)
         def import_set(data: dict[str, Any], /) -> None:
             # Needs to be authenticated
diff --git a/bricktracker/views/instructions.py b/bricktracker/views/instructions.py
index d347b06..2c2138a 100644
--- a/bricktracker/views/instructions.py
+++ b/bricktracker/views/instructions.py
@@ -14,6 +14,7 @@ from .exceptions import exception_handler
 from ..instructions import BrickInstructions
 from ..instructions_list import BrickInstructionsList
 from ..parser import parse_set
+from ..socket import MESSAGES
 from .upload import upload_helper
 
 instructions_page = Blueprint(
@@ -149,24 +150,22 @@ def download() -> str:
 
 
 # Show search results
-@instructions_page.route('/download/select', methods=['POST'])
-@login_required
-@exception_handler(__file__, post_redirect='instructions.download')
-def select_download() -> str:
-    return render_template(
-        'instructions.html',
-        download=True,
-        instructions=BrickInstructions.find_instructions(request.form)
-    )
-
-
-# Download files
 @instructions_page.route('/download', methods=['POST'])
 @login_required
 @exception_handler(__file__, post_redirect='instructions.download')
-def do_download() -> Response:
-    BrickInstructions.download_instructions(request.form)
+def do_download() -> str:
+    # Grab the set number
+    try:
+        set = parse_set(request.form.get('download-set', ''))
+    except Exception:
+        set = ''
 
-    BrickInstructionsList(force=True)
-
-    return redirect(url_for('instructions.list'))
+    return render_template(
+        'instructions.html',
+        download=True,
+        instructions=BrickInstructions.find_instructions(set),
+        set=set,
+        path=current_app.config['SOCKET_PATH'],
+        namespace=current_app.config['SOCKET_NAMESPACE'],
+        messages=MESSAGES
+    )
diff --git a/static/scripts/socket/instructions.js b/static/scripts/socket/instructions.js
new file mode 100644
index 0000000..6a1fdc2
--- /dev/null
+++ b/static/scripts/socket/instructions.js
@@ -0,0 +1,108 @@
+// Instructions Socket class
+class BrickInstructionsSocket extends BrickSocket {
+    constructor(id, path, namespace, messages) {
+        super(id, path, namespace, messages, true);
+
+        // Listeners
+        this.download_listener = undefined;
+
+        // Form elements (built based on the initial id)
+        this.html_button = document.getElementById(id);
+        this.html_files = document.getElementById(`${id}-files`);
+
+        if (this.html_button) {
+            this.download_listener = ((bricksocket) => (e) => {
+                if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) {
+                    bricksocket.toggle(false);
+
+                    bricksocket.download_instructions();
+                }
+            })(this);
+
+            this.html_button.addEventListener("click", this.download_listener);
+        }
+
+        if (this.html_card_dismiss && this.html_card) {
+            this.html_card_dismiss.addEventListener("click", ((card) => (e) => {
+                card.classList.add("d-none");
+            })(this.html_card));
+        }
+
+        // Setup the socket
+        this.setup();
+    }
+
+    // Upon receiving a complete message
+    complete(data) {
+        super.complete(data);
+
+        // Uncheck current file
+        this.file.checked = false;
+
+        // Download the next file
+        this.download_instructions(true);
+    }
+
+    // Get the list of checkboxes describing files
+    get_files(checked=false) {
+        let files = [];
+
+        if (this.html_files) {
+            files = [...this.html_files.querySelectorAll('input[type="checkbox"]')];
+
+            if (checked) {
+                files = files.filter(file => file.checked);
+            }
+        }
+
+        return files;
+    }
+
+    // Download an instructions file
+    download_instructions(from_complete=false) {
+        if (this.html_files) {
+            if (!from_complete) {
+                this.total = this.get_files(true).length;
+                this.current = 0;
+                this.clear_status();
+            }
+
+            // Find the next checkbox
+            this.file = this.get_files(true).shift();
+
+            // Abort if nothing left to process
+            if (this.file === undefined) {
+                // Settle the form
+                this.spinner(false);
+                this.toggle(true);
+
+                return;
+            }
+
+            this.spinner(true);
+
+            this.current++;
+            this.socket.emit(this.messages.DOWNLOAD_INSTRUCTIONS, {
+                alt: this.file.dataset.downloadAlt,
+                href: this.file.dataset.downloadHref,
+                total: this.total,
+                current: this.current,
+            });
+        } else {
+            this.fail("Could not find the list of files to download");
+        }
+    }
+
+    // Toggle clicking on the button, or sending events
+    toggle(enabled) {
+        super.toggle(enabled);
+
+        if (this.html_files) {
+            this.get_files().forEach(el => el.disabled != enabled);
+        }
+
+        if (this.html_button) {
+            this.html_button.disabled = !enabled;
+        }
+    }
+}
diff --git a/templates/base.html b/templates/base.html
index 12c4afa..6d89d3d 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -83,6 +83,7 @@
     <script src="{{ url_for('static', filename='scripts/grid.js') }}"></script>
     <script src="{{ url_for('static', filename='scripts/set.js') }}"></script>
     <script src="{{ url_for('static', filename='scripts/socket/socket.js') }}"></script>
+    <script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
     <script src="{{ url_for('static', filename='scripts/socket/set.js') }}"></script>
     <script src="{{ url_for('static', filename='scripts/table.js') }}"></script>
     <script type="text/javascript">
diff --git a/templates/instructions/download.html b/templates/instructions/download.html
index 9543702..c896769 100644
--- a/templates/instructions/download.html
+++ b/templates/instructions/download.html
@@ -1,50 +1,63 @@
 <div class="container">
-    {% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
-    <div class="row">
-        <div class="col-12">
-            <form method="POST" action="{{ url_for('instructions.select_download') }}">
-                <div class="card mb-3">
-                    <div class="card-header">
-                        <h5 class="mb-0"><i class="ri-add-circle-line"></i> Download instructions from Rebrickable</h5>
-                    </div>
-                    <div class="card-body">
-                        <div id="add-fail" class="alert alert-danger d-none" role="alert"></div>
-                        <div id="add-complete" class="alert alert-success d-none" role="alert"></div>
-                        <div class="mb-3">
-                            <label for="add-set" class="form-label">Set number (only one)</label>
-                            <input type="text" class="form-control" id="add-set" name="add-set" placeholder="107-1 or 1642-1 or ..." value="{{ set }}">
-                        </div>
-                        <button type="submit" class="btn btn-primary">Search</button>
-                    </div>
-                </div>
-            </form>
-            {% if instructions %}
-            <div class="card mb-3">
-                <div class="card-header">
-                    <h5 class="mb-0"><i class="ri-add-circle-line"></i> Select instructions to download</h5>
-                </div>
-                <div class="card-body">
-                    <form method="POST" action="{{ url_for('instructions.do_download') }}">
-                        <div class="mb-3">
-                            <label class="form-label">Available Instructions</label>
-                            <div class="form-check">
-                                {% for alt_text, href in instructions %}
-                                <div class="form-check">
-                                    <input class="form-check-input" type="checkbox" name="instruction-{{ loop.index }}" id="instruction-{{ loop.index }}">
-                                    <label class="form-check-label" for="instruction-{{ loop.index }}">
-                                        {{ alt_text }}
-                                    </label>
-                                    <input type="hidden" name="instruction-alt-text-{{ loop.index }}" value="{{ alt_text }}">
-                                    <input type="hidden" name="instruction-href-text-{{ loop.index }}" value="{{ href }}">
-                                </div>
-                                {% endfor %}
-                            </div>
-                        </div>
-                        <button type="submit" class="btn btn-primary">Download Selected</button>
-                    </form>
-                </div>
+  {% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+  <div class="row">
+    <div class="col-12">
+      <form method="POST" action="{{ url_for('instructions.do_download') }}">
+        <div class="card mb-3">
+          <div class="card-header">
+            <h5 class="mb-0"><i class="ri-download-line"></i> Download instructions from Rebrickable</h5>
+          </div>
+          <div class="card-body">
+            <div class="mb-3">
+              <label for="download-set" class="form-label">Set number (only one)</label>
+              <input type="text" class="form-control" id="download-set" name="download-set" placeholder="107-1 or 1642-1 or ..." value="{{ set }}">
             </div>
-            {% endif %}
+          </div>
+          <div class="card-footer text-end">
+            <button type="submit" class="btn btn-primary"><i class="ri-search-line"></i> Search</button>
+          </div>
         </div>
+      </form>
+      {% if instructions %}
+        <div class="card mb-3">
+          <div class="card-header">
+            <h5 class="mb-0"><i class="ri-checkbox-line"></i> Select instructions to download</h5>
+          </div>
+          <div class="card-body">
+            <div class="mb-3">
+              <div id="download-fail" class="alert alert-danger d-none" role="alert"></div>
+              <div id="download-complete"></div>
+              <h5 class="border-bottom">Available Instructions</h5>
+              <div id="download-files">
+                {% for alt_text, href in instructions %}
+                  <div class="form-check">
+                    <input class="form-check-input" type="checkbox" id="instruction-{{ loop.index }}" data-download-href="{{ href }}" data-download-alt="{{ alt_text }}" autocomplete="off">
+                    <label class="form-check-label" for="instruction-{{ loop.index }}">{{ alt_text }}</label>
+                  </div>
+                {% endfor %}
+              </div>
+            </div>
+            <hr>
+            <div class="mb-3">
+              <p>
+                Progress <span id="download-count"></span>
+                <span id="download-spinner" class="d-none">
+                  <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
+                  <span class="visually-hidden" role="status">Loading...</span>
+                </span>
+              </p>
+              <div id="download-progress" class="progress" role="progressbar" aria-label="Download an instructions file progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
+                <div id="download-progress-bar" class="progress-bar" style="width: 0%"></div>
+              </div>
+              <p id="download-progress-message" class="text-center d-none"></p>
+            </div>
+          </div>
+          <div class="card-footer text-end">
+            <span id="download-status-icon" class="me-1"></span><span id="download-status" class="me-1"></span><button id="download" type="button" class="btn btn-primary"><i class="ri-download-line"></i> Download selected files</button>
+          </div>
+        </div>
+        {% include 'instructions/socket.html' %}
+      {% endif %}
     </div>
-</div>
\ No newline at end of file
+  </div>
+</div>
diff --git a/templates/instructions/socket.html b/templates/instructions/socket.html
new file mode 100644
index 0000000..3bd73f4
--- /dev/null
+++ b/templates/instructions/socket.html
@@ -0,0 +1,10 @@
+<script type="text/javascript">
+  document.addEventListener("DOMContentLoaded", () => {
+    new BrickInstructionsSocket('download', '{{ path }}', '{{ namespace }}', {
+      COMPLETE: '{{ messages['COMPLETE'] }}',
+      DOWNLOAD_INSTRUCTIONS: '{{ messages['DOWNLOAD_INSTRUCTIONS'] }}',
+      FAIL: '{{ messages['FAIL'] }}',
+      PROGRESS: '{{ messages['PROGRESS'] }}',
+    });
+  });
+</script>