diff --git a/css/portfolio.css b/css/portfolio.css
index bc4b834..3069227 100644
--- a/css/portfolio.css
+++ b/css/portfolio.css
@@ -1,3 +1,54 @@
 @charset "utf-8";
 
 * { box-sizing: border-box; }
+
+/* Image viewer */
+.hidden { display: none; }
+#viewer {
+    background-color: #0008;
+    height: 100%;
+    left: 0;
+    padding-top: 5%;
+    position: fixed;
+    top: 0;
+    text-align: center;
+    width: 100%;
+}
+.viewer_button {
+    cursor: pointer;
+    height: 2em;
+    position: fixed;
+    width: 2em;
+}
+#viewer > img {
+    border: solid 5px #eee;
+    box-shadow: #000a 0 5px 16px 0;
+    max-height: 90%;
+    max-width: 90%;
+}
+.viewer_button::before {
+    display: block;
+    color: #ddd;
+    font-size: 2em;
+    font-weight: bold;
+    height: 1em;
+    position: "fixed";
+    width: 1em;
+}
+.viewer_button:hover::before { color: #fff; }
+#close {
+    right: 2em;
+    top: 2em;
+}
+#close::before {
+    content: "x";
+    right: 1em;
+    top: 1em;
+}
+#prev {
+    left: 2em;
+    top: calc(50% - 2em);
+}
+#prev::before {
+    content: "<<";
+    left: 1em;
diff --git a/js/image_viewer.js b/js/image_viewer.js
new file mode 100644
index 0000000..c5a4275
--- /dev/null
+++ b/js/image_viewer.js
@@ -0,0 +1,103 @@
+function pop(event) {
+    var target = event.target.parentNode;
+    if (target.nodeName != "FIGURE") {
+        target = event.target;
+    }
+    var href = target.parentNode.dataset.href;
+    var alt = target.firstElementChild.alt;
+    var viewer = document.getElementById('viewer');
+    var image = document.createElement('img');
+    image.src = href;
+    image.alt = alt;
+    image.id = 'img' + target.parentNode.parentNode.id;
+    viewer.appendChild(image);
+    viewer.classList.remove('hidden');
+}
+
+function unpop(event) {
+    var viewer = document.getElementById('viewer');
+    viewer.classList.add('hidden');
+    if (viewer.lastChild.nodeName == 'IMG') {
+        viewer.removeChild(viewer.lastChild);
+    }
+}
+
+function displayNeighbour(which) {
+    var viewer = document.getElementById('viewer');
+    var current = viewer.lastChild;
+    if (current.nodeName != 'IMG') {
+        return;
+    }
+    var ref_image_container = document.getElementById(current.id.slice(3))
+    var new_image_container = which == 'next' ?
+        ref_image_container.nextElementSibling :
+        ref_image_container.previousElementSibling;
+    if (new_image_container == null) {
+        return;
+    }
+    var new_image = new_image_container.firstElementChild;
+    current.src = new_image.dataset.href;
+    current.alt = new_image_container.firstElementChild.firstElementChild.firstElementChild.alt;
+    current.id = 'img' + new_image_container.id;
+}
+
+function displayPrev(event) {
+    displayNeighbour('prev');
+}
+
+function displayNext(event) {
+    displayNeighbour('next');
+}
+
+/* Adds new HTML elements for interactive viewing */
+var viewer = document.createElement('div');
+viewer.id = 'viewer';
+viewer.classList.add('hidden');
+
+var close = document.createElement('div');
+close.id = 'close';
+close.classList.add('viewer_button');
+close.addEventListener('click', unpop);
+viewer.appendChild(close);
+
+/* not implemented yet */
+var prev = document.createElement('div');
+prev.id = 'prev';
+prev.classList.add('viewer_button');
+prev.addEventListener('click', displayPrev);
+viewer.appendChild(prev);
+
+var next = document.createElement('div');
+next.id = 'next';
+next.classList.add('viewer_button');
+next.addEventListener('click', displayNext);
+viewer.appendChild(next);
+/* not implemented yet */
+
+document.body.appendChild(viewer);
+document.body.addEventListener(
+    'keydown',
+    (event) => {
+        if (event.defaultPrevented) {
+            return;
+        }
+        switch (event.key) {
+            case "Escape":
+                unpop();
+                break;
+            case "ArrowLeft":
+                displayPrev();
+                break;
+            case "ArrowRight":
+                displayNext();
+                break;
+        }
+    }
+);
+
+/* Makes thumbnails interactive */
+for (var thumb of document.getElementsByClassName('thumbnail')) {
+    thumb.dataset.href = thumb.href;
+    thumb.href = 'javascript: void(0);';
+    thumb.addEventListener('click', pop);
+}
diff --git a/lib/Image.php b/lib/Image.php
index bb16a80..913bedc 100644
--- a/lib/Image.php
+++ b/lib/Image.php
@@ -2,6 +2,7 @@
 
 final class Image
 {
+    public string $id;
     public string $full;
     public string $thumbnail;
     public string $title;
@@ -9,8 +10,9 @@ final class Image
     public int $width;
     public int $height;
 
-    public function __construct(string $full, string $thumbnail, string $title, string $alt, int $width, int $height)
+    public function __construct(string $id, string $full, string $thumbnail, string $title, string $alt, int $width, int $height)
     {
+        $this->id = $id;
         $this->full = $full;
         $this->thumbnail = $thumbnail;
         $this->title = $title;
diff --git a/views/index.phtml b/views/index.phtml
index 0d54ebe..a379855 100644
--- a/views/index.phtml
+++ b/views/index.phtml
@@ -19,7 +19,7 @@
         <h2><?=$gallery->label?></h2>
         <ul id="<?=$gallery->id?>" class="gallery">
 <?php foreach ($gallery->images as $image): ?>
-            <li>
+            <li id="<?=$image->id?>">
                 <a class="thumbnail" href="<?=$image->full?>">
                     <figure>
                         <img src="<?=$image->thumbnail?>" alt="<?=$image->alt?>" data-width="<?=$image->width?>" data-height="<?=$image->height?>">
@@ -46,5 +46,6 @@
         <p class="subfooter"><?=$subfooter?></p>
 <?php endif; ?>
     </footer>
+    <script src="js/image_viewer.js"></script>
 </body>
 </html>