diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 92bf46068bd64c858ea717c0cb9d6d719e321b88..28d50c7570bf528b34be105ebecbb858c36767a4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -55,6 +55,7 @@ deploy-netlify-preview:
   stage: deploy
   script:
     - cd examples/web/dist
+    - cp -rp ../static/* .
     - npm_config_yes=true npx --package=netlify-cli netlify deploy --dir=.
   dependencies:
     - build-wasm
@@ -65,6 +66,7 @@ deploy-netlify-prod:
   stage: deploy
   script:
     - cd examples/web/dist
+    - cp -rp ../static/* .
     - npm_config_yes=true npx --package=netlify-cli netlify deploy --dir=. --prod
   dependencies:
     - build-wasm
@@ -75,6 +77,7 @@ deploy-cloudfare-master:
   stage: deploy
   script:
     - cd examples/web/dist
+    - cp -rp ../static/* .
     - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch master
   dependencies:
     - build-wasm
@@ -85,6 +88,7 @@ deploy-cloudfare-stable:
   stage: deploy
   script:
     - cd examples/web/dist
+    - cp -rp ../static/* .
     - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch stable
   dependencies:
     - build-wasm
@@ -95,6 +99,7 @@ deploy-cloudfare-prod:
   stage: deploy
   script:
     - cd examples/web/dist
+    - cp -rp ../static/* .
     - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch prod
   dependencies:
     - build-wasm
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e38f0beb70ff1904f7810c51ddcd4ca37eeb464a..4c3e07b9e26049c73f5bf89adbf83f93f334083e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+*
+
+### Changed
+
+*
+
+### Fixed
+
+*
+
+## [0.4.0] - 2022-11-01
+
+### Added
+
+* A whole new layout implemented using React.JS 🔥
 * Instant boot support using the `GameBoy.boot()` method
 * Support for pending cycles in web version
 
diff --git a/Cargo.toml b/Cargo.toml
index 9523dbb9f5319699a67b859632cb1041bc3114e5..0d90a81bb287fdf92180c5d9a707f86bf3850f5d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "boytacean"
 description = "A Game Boy emulator that is written in Rust."
-version = "0.3.0"
+version = "0.4.0"
 authors = ["João Magalhães <joamag@gmail.com>"]
 license = "Apache-2.0"
 repository = "https://gitlab.stage.hive.pt/joamag/boytacean"
diff --git a/examples/sdl/Cargo.toml b/examples/sdl/Cargo.toml
index 97b7e3ea960e3bbc3f7a91d0d7323516286e673b..0c50371b7608b756ffeefdf6b45191188bd41829 100644
--- a/examples/sdl/Cargo.toml
+++ b/examples/sdl/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "boytacean-sdl"
-version = "0.3.0"
+version = "0.4.0"
 authors = ["João Magalhães <joamag@gmail.com>"]
 description = "Game Boy Emulator SDL (Desktop) Application"
 license = "Apache-2.0"
diff --git a/examples/sdl/src/main.rs b/examples/sdl/src/main.rs
index 264a1cda21e30cabf8528e0c6a6c6854570b3850..e72b7a05e7dcd6b1c1d098e72dec6ccada22262a 100644
--- a/examples/sdl/src/main.rs
+++ b/examples/sdl/src/main.rs
@@ -142,6 +142,11 @@ impl Emulator {
                     if self.system.ppu_mode() == PpuMode::VBlank
                         && self.system.ppu_frame() != last_frame
                     {
+                        // clears the graphics canvas, making sure that no garbage
+                        // pixel data remaining in the pixel buffer, not doing this would
+                        // create visual glitches in OSs like Mac OS X
+                        self.graphics.canvas.clear();
+
                         // obtains the frame buffer of the Game Boy PPU and uses it
                         // to update the stream texture, copying it then to the canvas
                         let frame_buffer = self.system.frame_buffer().as_ref();
@@ -154,6 +159,8 @@ impl Emulator {
                         // information presented to the user
                         self.graphics.canvas.present();
 
+                        // obtains the index of the current PPU frame, this value
+                        // is going to be used to detect for new frame presence
                         last_frame = self.system.ppu_frame();
                     }
                 }
diff --git a/examples/web/index.css b/examples/web/index.css
index 25a1c6283991884492ae2f74949af2719f93b5a7..d8dd1b1daf19c6ae8772aadec8ad2ac13a9123ab 100644
--- a/examples/web/index.css
+++ b/examples/web/index.css
@@ -56,78 +56,6 @@ p {
     text-align: center;
 }
 
-.main > .side-left .canvas-container {
-    max-width: 100%;
-}
-
-.main > .side-left .canvas-container.fullscreen {
-    align-items: center;
-    background-color: #2d2d2d;
-    display: flex;
-    height: 100%;
-    justify-content: center;
-    left: 0px;
-    position: fixed;
-    top: 0px;
-    width: 100%;
-    z-index: 6;
-}
-
-.main > .side-left .canvas-container > .canvas-close {
-    bottom: 22px;
-    display: none;
-    position: absolute;
-    right: 22px;
-    user-select: none;
-    -o-user-select: none;
-    -ms-user-select: none;
-    -moz-user-select: none;
-    -khtml-user-select: none;
-    -webkit-user-select: none;
-}
-
-.main > .side-left .canvas-container > .canvas-close > img {
-    height: 32px;
-    width: 32px;
-}
-
-.main > .side-left .canvas-container.fullscreen > .canvas-close {
-    display: block;
-}
-
-.main > .side-left .canvas-container > .canvas-frame {
-    background-color: #1b1a17;
-    border: 2px solid #50cb93;
-    font-size: 0px;
-    margin-top: 78px;
-    max-width: 320px;
-    padding: 8px 8px 8px 8px;
-}
-
-@media only screen and (max-width: 1120px) {
-    .main > .side-left .canvas-container > .canvas-frame {
-        margin-top: 12px;
-    }
-}
-
-.main > .side-left .canvas-container.fullscreen > .canvas-frame {
-    background-color: transparent;
-    border: none;
-    box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
-    -o-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
-    -ms-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
-    -moz-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
-    -khtml-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
-    -webkit-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
-    margin: 0px 0px 0px 0px;
-    max-width: unset;
-    padding: 0px 0px 0px 0px;
-}
-
-.main > .side-left .canvas-container > .canvas-frame > .canvas {
-    width: 100%;
-}
-
 .main > .side-right {
     flex: 0 1;
     max-width: 100%;
@@ -182,343 +110,3 @@ p {
     content: '';
     display: block;
 }
-
-.toast-container {
-    background-color: black;
-    height: 0px;
-    left: 0px;
-    padding: 0px 24px 0px 24px;
-    pointer-events: none;
-    position: fixed;
-    text-align: center;
-    top: 0px;
-    width: 100%;
-    z-index: 8;
-}
-
-.toast-container > .toast {
-    background-color: #2a9d8f;
-    border-radius: 4px 4px 4px 4px;
-    -o-border-radius: 4px 4px 4px 4px;
-    -ms-border-radius: 4px 4px 4px 4px;
-    -moz-border-radius: 4px 4px 4px 4px;
-    -khtml-border-radius: 4px 4px 4px 4px;
-    -webkit-border-radius: 4px 4px 4px 4px;
-    cursor: pointer;
-    display: inline-block;
-    font-size: 20px;
-    line-height: 22px;
-    opacity: 0.0;
-    -o-opacity: 0.0;
-    -ms-opacity: 0.0;
-    -moz-opacity: 0.0;
-    -khtml-opacity: 0.0;
-    -webkit-opacity: 0.0;
-    padding: 12px 18px 12px 18px;
-    position: relative;
-    top: -46px;
-    transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    width: fit-content;
-}
-
-.toast-container > .toast.error {
-    background-color: #e63946;
-}
-
-.toast-container > .toast.visible {
-    opacity: 1.0;
-    -o-opacity: 1.0;
-    -ms-opacity: 1.0;
-    -moz-opacity: 1.0;
-    -khtml-opacity: 1.0;
-    -webkit-opacity: 1.0;
-    pointer-events: all;
-    top: 24px;
-}
-
-.button-area {
-    user-select: none;
-    -o-user-select: none;
-    -ms-user-select: none;
-    -moz-user-select: none;
-    -khtml-user-select: none;
-    -webkit-user-select: none;
-}
-
-.button-area > * {
-    margin-bottom: 12px;
-}
-
-.magnify-button {
-    cursor: pointer;
-    display: inline-block;
-    transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
-}
-
-.magnify-button:hover {
-    transform: scale(1.3, 1.3);
-    -o-transform: scale(1.3, 1.3);
-    -ms-transform: scale(1.3, 1.3);
-    -moz-transform: scale(1.3, 1.3);
-    -khtml-transform: scale(1.3, 1.3);
-    -webkit-transform: scale(1.3, 1.3);
-}
-
-.magnify-button:active {
-    transform: scale(1.0, 1.0);
-    -o-transform: scale(1.0, 1.0);
-    -ms-transform: scale(1.0, 1.0);
-    -moz-transform: scale(1.0, 1.0);
-    -khtml-transform: scale(1.0, 1.0);
-    -webkit-transform: scale(1.0, 1.0);
-}
-
-.overlay {
-    align-items: center;
-    background-color: rgba(80, 203, 147, 0.95);
-    display: flex;
-    font-size: 48px;
-    height: 100%;
-    justify-content: center;
-    left: 0px;
-    opacity: 0.0;
-    -o-opacity: 0.0;
-    -ms-opacity: 0.0;
-    -moz-opacity: 0.0;
-    -khtml-opacity: 0.0;
-    -webkit-opacity: 0.0;
-    pointer-events: none;
-    position: fixed;
-    text-align: center;
-    top: 0px;
-    transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    width: 100%;
-    z-index: 10;
-}
-
-.overlay.visible {
-    opacity: 1.0;
-    -o-opacity: 1.0;
-    -ms-opacity: 1.0;
-    -moz-opacity: 1.0;
-    -khtml-opacity: 1.0;
-    -webkit-opacity: 1.0;
-}
-
-.overlay .overlay-image {
-    margin-top: 16px;
-}
-
-.overlay .overlay-image > img {
-    width: 64px;
-}
-
-.modal-container {
-    align-items: center;
-    background-color: rgba(20, 20, 20, 0.95);
-    display: flex;
-    height: 100%;
-    justify-content: center;
-    left: 0px;
-    opacity: 0;
-    -o-opacity: 0;
-    -ms-opacity: 0;
-    -moz-opacity: 0;
-    -khtml-opacity: 0;
-    -webkit-opacity: 0;
-    padding: 0px 12px 0px 12px;
-    pointer-events: none;
-    position: fixed;
-    text-align: center;
-    top: 0px;
-    transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
-    width: 100%;
-    z-index: 10;
-}
-
-.modal-container.visible {
-    opacity: 1.0;
-    -o-opacity: 1.0;
-    -ms-opacity: 1.0;
-    -moz-opacity: 1.0;
-    -khtml-opacity: 1.0;
-    -webkit-opacity: 1.0;
-    transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-}
-
-.modal-container > .modal {
-    background-color: #264653;
-    border-radius: 6px 6px 6px 6px;
-    -o-border-radius: 6px 6px 6px 6px;
-    -ms-border-radius: 6px 6px 6px 6px;
-    -moz-border-radius: 6px 6px 6px 6px;
-    -khtml-border-radius: 6px 6px 6px 6px;
-    -webkit-border-radius: 6px 6px 6px 6px;
-    box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
-    -o-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
-    -ms-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
-    -moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
-    -khtml-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
-    -webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
-    margin-top: 30px;
-    max-width: 100%;
-    padding: 24px 24px 24px 24px;
-    text-align: left;
-    transform: scale(0.96);
-    -o-transform: scale(0.96);
-    -ms-transform: scale(0.96);
-    -moz-transform: scale(0.96);
-    -khtml-transform: scale(0.96);
-    -webkit-transform: scale(0.96);
-    transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
-    width: 480px;
-}
-
-.modal-container.visible > .modal {
-    margin-top: 0px;
-    pointer-events: all;
-    transform: scale(1);
-    -o-transform: scale(1);
-    -ms-transform: scale(1);
-    -moz-transform: scale(1);
-    -khtml-transform: scale(1);
-    -webkit-transform: scale(1);
-    transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -o-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -ms-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -moz-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -khtml-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-    -webkit-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
-}
-
-.modal-container > .modal .modal-top-buttons {
-    float: right;
-    margin-right: -10px;
-    margin-top: -10px;
-}
-
-.modal-container > .modal .modal-title {
-    font-size: 32px;
-    margin-top: 0px;
-    text-align: left;
-}
-
-.modal-container > .modal .modal-text {
-    font-size: 20px;
-    line-height: 22px;
-}
-
-.modal-container > .modal .modal-buttons {
-    font-size: 22px;
-    margin-top: 24px;
-    text-align: center;
-    user-select: none;
-    -o-user-select: none;
-    -ms-user-select: none;
-    -moz-user-select: none;
-    -khtml-user-select: none;
-    -webkit-user-select: none;
-}
-
-.modal-container > .modal .modal-buttons > .button.simple {
-    display: inline-block;
-    margin-right: 12px;
-    min-width: 120px;
-}
-
-.modal-container > .modal .modal-buttons > .button.simple:last-child {
-    margin-right: 0px;
-}
-
-.keyboard {
-    font-size: 0px;
-    text-align: center;
-    touch-callout: none;
-    -o-touch-callout: none;
-    -ms-touch-callout: none;
-    -moz-touch-callout: none;
-    -khtml-touch-callout: none;
-    -webkit-touch-callout: none;
-    user-select: none;
-    -o-user-select: none;
-    -ms-user-select: none;
-    -moz-user-select: none;
-    -khtml-user-select: none;
-    -webkit-user-select: none;
-}
-
-.keyboard > .keyboard-line {
-    margin-bottom: 12px;
-}
-
-.keyboard > .keyboard-line:last-child {
-    margin-bottom: 0px;
-}
-
-.keyboard .key {
-    border: 2px solid #ffffff;
-    border-radius: 5px 5px 5px 5px;
-    -o-border-radius: 5px 5px 5px 5px;
-    -ms-border-radius: 5px 5px 5px 5px;
-    -moz-border-radius: 5px 5px 5px 5px;
-    -khtml-border-radius: 5px 5px 5px 5px;
-    -webkit-border-radius: 5px 5px 5px 5px;
-    cursor: pointer;
-    display: inline-block;
-    font-size: 38px;
-    height: 48px;
-    line-height: 46px;
-    margin-right: 14px;
-    text-align: center;
-    width: 48px;
-}
-
-.keyboard .key:last-child {
-    margin-right: 0px;
-}
-
-.keyboard .key:hover {
-    background-color: #50cb93;
-}
-
-.keyboard .key:active {
-    background-color: #2a9d8f;
-}
-
-#section-diag {
-    display: none;
-}
-
-#separator-diag {
-    display: none;
-}
\ No newline at end of file
diff --git a/examples/web/index.html b/examples/web/index.html
index 2ff402b47b20289271dc61d32e582a5032541034..6cc648c63fe4151b108528a7c421bc1e5a2afd1d 100644
--- a/examples/web/index.html
+++ b/examples/web/index.html
@@ -14,147 +14,7 @@
 
 <body>
     <div id="app"></div>
-    <div class="main">
-        <div class="side-left">
-            <div id="canvas-container" class="canvas-container">
-                <span id="canvas-close" class="magnify-button canvas-close">
-                    <img class="large" src="res/minimise.svg" alt="minimise" />
-                </span>
-                <div class="canvas-frame">
-                    <canvas id="engine-canvas" class="canvas" width="320" height="288"></canvas>
-                </div>
-            </div>
-        </div>
-        <div class="side-right">
-            <h1>Boytacean <a id="version" href="https://gitlab.stage.hive.pt/joamag/boytacean/-/blob/master/CHANGELOG.md" target="_blank"></a> <img class="logo-image" src="res/thunder.png" alt="thunder" />
-            </h1>
-            <div class="separator"></div>
-            <div id="section-narrative" class="section">
-                <p>This is a <a href="https://en.wikipedia.org/wiki/Game_Boy" target="_blank">Game Boy</a> emulator built using
-                    the <a href="https://www.rust-lang.org" target="_blank">Rust Programming Language</a> and is running
-                    inside this browser with the help of <a href="https://webassembly.org/" target="_blank">WebAssembly</a>.
-                </p>
-                <p>You can check the source code of it at <a href="https://gitlab.stage.hive.pt/joamag/boytacean"
-                        target="_blank">GitLab</a>.</p>
-                <p>TIP: Drag and Drop ROM files to the Browser to load the ROM.</p>
-            </div>
-            <div id="separator-narrative"  class="separator"></div>
-            <div id="section-keyboard" class="section" style="display: none;">
-                <div id="keyboard" class="keyboard">
-                    <div class="keyboard-line">
-                        <span class="key">1</span>
-                        <span class="key">2</span>
-                        <span class="key">3</span>
-                        <span class="key">4</span>
-                    </div>
-                    <div class="keyboard-line">
-                        <span class="key">Q</span>
-                        <span class="key">W</span>
-                        <span class="key">E</span>
-                        <span class="key">R</span>
-                    </div>
-                    <div class="keyboard-line">
-                        <span class="key">A</span>
-                        <span class="key">S</span>
-                        <span class="key">D</span>
-                        <span class="key">F</span>
-                    </div>
-                    <div class="keyboard-line">
-                        <span class="key">Z</span>
-                        <span class="key">X</span>
-                        <span class="key">C</span>
-                        <span class="key">V</span>
-                    </div>
-                </div>
-            </div>
-            <div id="separator-keyboard" class="separator" style="display: none;"></div>
-            <div id="section-debug" class="section" style="display: none;">
-                <div id="debug" class="debug">
-                    <canvas id="canvas-tiles" class="canvas-tiles" width="128" height="192"></canvas>
-                </div>
-            </div>
-            <div id="separator-debug" class="separator" style="display: none;"></div>
-            <div id="section-diag" class="section">
-                <dl class="diag">
-                    <dt>Engine</dt>
-                    <dd id="engine" class="button simple">-</dd>
-                    <dt>ROM</dt>
-                    <dd id="rom-name">-</dd>
-                    <dt>ROM Size</dt>
-                    <dd><span id="rom-size">-</span> bytes</dd>
-                    <dt>CPU Frequency</dt>
-                    <dd>
-                        <span id="logic-frequency-minus" class="button simple">-</span>
-                        <span id="logic-frequency">-</span> Hz
-                        <span id="logic-frequency-plus" class="button simple">+</span></dd>
-                    <dt>Framerate</dt>
-                    <dd><span id="fps-count">-</span> fps</dd>
-                </dl>
-            </div>
-            <div id="separator-diag" class="separator"></div>
-            <div class="section">
-                <div class="button-area">
-                    <span id="button-pause" class="button simple border padded">
-                        <img src="res/pause.svg" alt="pause" /><span>Pause</span>
-                    </span>
-                    <span id="button-reset" class="button simple border padded">
-                        <img src="res/reset.svg" alt="reset" /><span>Reset</span>
-                    </span>
-                    <span id="button-benchmark" class="button simple border padded">
-                        <img src="res/bolt.svg" alt="bolt" /><span>Benchmark</span>
-                    </span>
-                    <span id="button-fullscreen" class="button simple border padded">
-                        <img src="res/maximise.svg" alt="maximise" /><span>Fullscreen</span>
-                    </span>
-                    <span id="button-keyboard" class="button simple border padded">
-                        <img src="res/dialpad.svg" alt="info" /><span>Keyboard</span>
-                    </span>
-                    <span id="button-information" class="button simple border padded">
-                        <img src="res/info.svg" alt="info" /><span>Information</span>
-                    </span>
-                    <span id="button-debug" class="button simple border padded">
-                        <img src="res/bug.svg" alt="bug" /><span>Debug</span>
-                    </span>
-                    <span id="button-theme" class="button simple border padded">
-                        <img src="res/marker.svg" alt="marker" /><span>Theme</span>
-                    </span>
-                    <span id="button-upload" class="button simple border padded file">
-                        <img src="res/upload.svg" alt="upload" /><span>Load ROM</span>
-                        <input type="file" id="button-upload-file" name="button-upload-file" accept=".gb">
-                    </span>
-                </div>
-            </div>
-        </div>
-    </div>
-    <div class="toast-container">
-        <div id="toast" class="toast"></div>
-    </div>
 </body>
-<div id="modal-container" class="modal-container">
-    <div id="modal" class="modal">
-        <div class="modal-top-buttons">
-            <span id="modal-close" class="button simple rounded no-text">
-                <img class="medium" src="res/close.svg" alt="close" />
-            </span>
-        </div>
-        <h2 id="modal-title" class="modal-title"></h2>
-        <p id="modal-text" class="modal-text"></p>
-        <div class="modal-buttons">
-            <span id="modal-cancel" class="button simple red border padded-large">Cancel</span>
-            <span id="modal-confirm" class="button simple border padded-large">Confirm</span>
-        </div>
-    </div>
-</div>
-<div id="overlay" class="overlay">
-    <div class="overlay-container">
-        <div class="overlay-text">
-            Drag to load ROM <span id="rom-name"></span>
-        </div>
-        <div class="overlay-image">
-            <img src="res/sunglasses.png" alt="sunglasses" />
-        </div>
-    </div>
-</div>
 <script type="module" src="index.ts"></script>
 </body>
 
diff --git a/examples/web/index.ts b/examples/web/index.ts
index 8631149265d051043e9a6d8b08ffaf58c327d143..cd8afa06d3e5d20999bd0bc491096e640098a657 100644
--- a/examples/web/index.ts
+++ b/examples/web/index.ts
@@ -1,23 +1,28 @@
-import { Emulator, startApp } from "./react/app";
-
-import { default as _wasm, GameBoy, PadKey, PpuMode } from "./lib/boytacean.js";
+import {
+    Emulator,
+    Observable,
+    PixelFormat,
+    RomInfo,
+    startApp
+} from "./react/app";
+
+import {
+    Cartridge,
+    default as _wasm,
+    GameBoy,
+    PadKey,
+    PpuMode
+} from "./lib/boytacean.js";
 import info from "./package.json";
 
 declare const require: any;
 
-const PIXEL_UNSET_COLOR = 0x1b1a17ff;
-
 const LOGIC_HZ = 600;
 const VISUAL_HZ = 60;
-const TIMER_HZ = 60;
 const IDLE_HZ = 10;
 
 const FREQUENCY_DELTA = 60;
 
-const DISPLAY_WIDTH = 160;
-const DISPLAY_HEIGHT = 144;
-const DISPLAY_RATIO = DISPLAY_WIDTH / DISPLAY_HEIGHT;
-
 const SAMPLE_RATE = 2;
 
 const BACKGROUNDS = [
@@ -41,21 +46,21 @@ const KEYS: Record<string, number> = {
     s: PadKey.B
 };
 
-const ROM_PATH = require("../../res/roms/20y.gb");
+const ARROW_KEYS: Record<string, boolean> = {
+    ArrowUp: true,
+    ArrowDown: true,
+    ArrowLeft: true,
+    ArrowRight: true
+};
 
-// Enumeration that describes the multiple pixel
-// formats and the associated size in bytes.
-enum PixelFormat {
-    RGB = 3,
-    RGBA = 4
-}
+const ROM_PATH = require("../../res/roms/20y.gb");
 
 /**
  * Top level class that controls the emulator behaviour
  * and "joins" all the elements together to bring input/output
  * of the associated machine.
  */
-class GameboyEmulator implements Emulator {
+class GameboyEmulator extends Observable implements Emulator {
     /**
      * The Game Boy engine (probably coming from WASM) that
      * is going to be used for the emulation.
@@ -70,16 +75,8 @@ class GameboyEmulator implements Emulator {
 
     private logicFrequency: number = LOGIC_HZ;
     private visualFrequency: number = VISUAL_HZ;
-    private timerFrequency: number = TIMER_HZ;
     private idleFrequency: number = IDLE_HZ;
 
-    private canvas: HTMLCanvasElement | null = null;
-    private canvasScaled: HTMLCanvasElement | null = null;
-    private canvasCtx: CanvasRenderingContext2D | null = null;
-    private canvasScaledCtx: CanvasRenderingContext2D | null = null;
-    private image: ImageData | null = null;
-    private videoBuff: DataView | null = null;
-    private toastTimeout: number | null = null;
     private paused: boolean = false;
     private nextTickTime: number = 0;
     private fps: number = 0;
@@ -89,6 +86,7 @@ class GameboyEmulator implements Emulator {
     private romName: string | null = null;
     private romData: Uint8Array | null = null;
     private romSize: number = 0;
+    private cartridge: Cartridge | null = null;
 
     async main() {
         // initializes the WASM module, this is required
@@ -97,13 +95,11 @@ class GameboyEmulator implements Emulator {
 
         // initializes the complete set of sub-systems
         // and registers the event handlers
-        await this.buildVisuals();
-        await this.init();
         await this.register();
 
-        // start the emulator subsystem with the initial
+        // boots the emulator subsystem with the initial
         // ROM retrieved from a remote data source
-        await this.start({ loadRom: true });
+        await this.boot({ loadRom: true });
 
         // the counter that controls the overflowing cycles
         // from tick to tick operation
@@ -145,7 +141,11 @@ class GameboyEmulator implements Emulator {
 
                 // displays the error information to both the end-user
                 // and the developer (for diagnostics)
-                this.showToast(message, true, 5000);
+                this.trigger("message", {
+                    text: message,
+                    error: true,
+                    timeout: 5000
+                });
                 console.error(err);
 
                 // pauses the machine, allowing the end-user to act
@@ -157,13 +157,10 @@ class GameboyEmulator implements Emulator {
                 // system and the machine state (to be able to recover)
                 // also sets the default color on screen to indicate the issue
                 if (isPanic) {
-                    await this.clearCanvas(undefined, {
-                        image: require("./res/storm.png"),
-                        imageScale: 0.2
-                    });
-
                     await wasm();
-                    await this.start({ restore: false });
+                    await this.boot({ restore: false });
+
+                    this.trigger("error");
                 }
             }
 
@@ -216,16 +213,14 @@ class GameboyEmulator implements Emulator {
             // frame is different from the previously rendered
             // one then it's time to update the canvas
             if (
-                this.gameBoy!.ppu_mode() == PpuMode.VBlank &&
-                this.gameBoy!.ppu_frame() != lastFrame
+                this.gameBoy!.ppu_mode() === PpuMode.VBlank &&
+                this.gameBoy!.ppu_frame() !== lastFrame
             ) {
-                // updates the canvas object with the new
-                // visual information coming in
-                this.updateCanvas(
-                    this.gameBoy!.frame_buffer_eager(),
-                    PixelFormat.RGB
-                );
                 lastFrame = this.gameBoy!.ppu_frame();
+
+                // triggers the frame event indicating that
+                // a new frame is now available for drawing
+                this.trigger("frame");
             }
         }
 
@@ -240,7 +235,7 @@ class GameboyEmulator implements Emulator {
             const currentTime = new Date().getTime();
             const deltaTime = (currentTime - this.frameStart) / 1000;
             const fps = Math.round(this.frameCount / deltaTime);
-            this.setFps(fps);
+            this.fps = fps;
             this.frameCount = 0;
             this.frameStart = currentTime;
         }
@@ -258,10 +253,13 @@ class GameboyEmulator implements Emulator {
      * Starts the current machine, setting the internal structure in
      * a proper state to start drawing and receiving input.
      *
+     * This method can also be used to load a new ROM into the machine.
+     *
      * @param options The options that are going to be used in the
-     * starting of the machine.
+     * starting of the machine, includes information on the ROM and
+     * the emulator engine to use.
      */
-    async start({
+    async boot({
         engine = "neo",
         restore = true,
         loadRom = false,
@@ -314,111 +312,27 @@ class GameboyEmulator implements Emulator {
 
         // updates the complete set of global information that
         // is going to be displayed
-        this.setEngine(this.engine!);
-        this.setRom(romName!, romData!);
-        this.setLogicFrequency(this.logicFrequency);
-        this.setFps(this.fps);
+        this.setRom(romName!, romData!, cartridge);
 
         // in case the restore (state) flag is set
         // then resumes the machine execution
         if (restore) this.resume();
-    }
-
-    async buildVisuals() {
-        /*     KeyValue.create("ROM", "-", { id: "diag:rom-name" }).mount(".diag");
-        KeyValue.create("ROM Size", "-", { id: "diag:rom-size" }).mount(
-            ".diag"
-        );
-        KeyValue.create("Framerate", "-", { id: "diag:framerate" }).mount(
-            ".diag"
-        );
-        KeyValue.create("ROM Type", "-", { id: "diag:rom-type" }).mount(
-            ".diag"
-        );
-        KeySwitch.create("Tobias", ["1", "2", "3"], {
-            id: "diag:tobias"
-        }).mount(".diag");
 
-        Button.create("Tobias", require("./res/close.svg"))
-            .bind("click", () => alert("Hello World"))
-            .mount(".button-area");*/
+        // triggers the booted event indicating that the
+        // emulator has finished the loading process
+        this.trigger("booted");
     }
 
     // @todo remove this method, or at least most of it
     async register() {
-        await Promise.all([
-            this.registerDrop(),
-            this.registerKeys(),
-            this.registerButtons(),
-            this.registerKeyboard(),
-            this.registerCanvas(),
-            this.registerToast(),
-            this.registerModal()
-        ]);
-    }
-
-    async init() {
-        await Promise.all([this.initBase(), this.initCanvas()]);
-    }
-
-    registerDrop() {
-        document.addEventListener("drop", async (event) => {
-            if (
-                !event.dataTransfer!.files ||
-                event.dataTransfer!.files.length === 0
-            ) {
-                return;
-            }
-
-            event.preventDefault();
-            event.stopPropagation();
-
-            const overlay = document.getElementById("overlay")!;
-            overlay.classList.remove("visible");
-
-            const file = event.dataTransfer!.files[0];
-
-            if (!file.name.endsWith(".gb")) {
-                this.showToast(
-                    "This is probably not a Game Boy ROM file!",
-                    true
-                );
-                return;
-            }
-
-            const arrayBuffer = await file.arrayBuffer();
-            const romData = new Uint8Array(arrayBuffer);
-
-            this.start({ engine: null, romName: file.name, romData: romData });
-
-            this.showToast(`Loaded ${file.name} ROM successfully!`);
-        });
-        document.addEventListener("dragover", async (event) => {
-            if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
-                return;
-
-            event.preventDefault();
-
-            const overlay = document.getElementById("overlay")!;
-            overlay.classList.add("visible");
-        });
-        document.addEventListener("dragenter", async (event) => {
-            if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
-                return;
-            const overlay = document.getElementById("overlay")!;
-            overlay.classList.add("visible");
-        });
-        document.addEventListener("dragleave", async (event) => {
-            if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
-                return;
-            const overlay = document.getElementById("overlay")!;
-            overlay.classList.remove("visible");
-        });
+        await Promise.all([this.registerKeys()]);
     }
 
     registerKeys() {
         document.addEventListener("keydown", (event) => {
             const keyCode = KEYS[event.key];
+            const isArrow = ARROW_KEYS[event.key] ?? false;
+            if (isArrow) event.preventDefault();
             if (keyCode !== undefined) {
                 this.gameBoy!.key_press(keyCode);
                 return;
@@ -426,25 +340,19 @@ class GameboyEmulator implements Emulator {
 
             switch (event.key) {
                 case "+":
-                    this.setLogicFrequency(
-                        this.logicFrequency + FREQUENCY_DELTA
-                    );
+                    this.logicFrequency += FREQUENCY_DELTA;
                     break;
 
                 case "-":
-                    this.setLogicFrequency(
-                        this.logicFrequency - FREQUENCY_DELTA
-                    );
-                    break;
-
-                case "Escape":
-                    this.minimize();
+                    this.logicFrequency -= FREQUENCY_DELTA;
                     break;
             }
         });
 
         document.addEventListener("keyup", (event) => {
             const keyCode = KEYS[event.key];
+            const isArrow = ARROW_KEYS[event.key] ?? false;
+            if (isArrow) event.preventDefault();
             if (keyCode !== undefined) {
                 this.gameBoy!.key_lift(keyCode);
                 return;
@@ -452,510 +360,66 @@ class GameboyEmulator implements Emulator {
         });
     }
 
-    registerButtons() {
-        const engine = document.getElementById("engine")!;
-        engine.addEventListener("click", () => {
-            const name = this.engine == "neo" ? "classic" : "neo";
-            this.start({ engine: name });
-            this.showToast(
-                `Game Boy running in engine "${name.toUpperCase()}" from now on!`
-            );
-        });
-
-        const logicFrequencyPlus = document.getElementById(
-            "logic-frequency-plus"
-        )!;
-        logicFrequencyPlus.addEventListener("click", () => {
-            this.setLogicFrequency(this.logicFrequency + FREQUENCY_DELTA);
-        });
-
-        const logicFrequencyMinus = document.getElementById(
-            "logic-frequency-minus"
-        )!;
-        logicFrequencyMinus.addEventListener("click", () => {
-            this.setLogicFrequency(this.logicFrequency - FREQUENCY_DELTA);
-        });
-
-        const buttonPause = document.getElementById("button-pause")!;
-        buttonPause.addEventListener("click", () => {
-            this.toggleRunning();
-        });
-
-        const buttonReset = document.getElementById("button-reset")!;
-        buttonReset.addEventListener("click", () => {
-            this.reset();
-        });
-
-        const buttonBenchmark = document.getElementById("button-benchmark")!;
-        buttonBenchmark.addEventListener("click", async () => {
-            const result = await this.showModal(
-                "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
-                "Confirm"
-            );
-            if (!result) return;
-            buttonBenchmark.classList.add("enabled");
-            this.pause();
-            try {
-                const initial = Date.now();
-                const count = 500000000;
-                for (let i = 0; i < count; i++) {
-                    this.gameBoy!.clock();
-                }
-                const delta = (Date.now() - initial) / 1000;
-                const frequency_mhz = count / delta / 1000 / 1000;
-                this.showToast(
-                    `Took ${delta.toFixed(
-                        2
-                    )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
-                        2
-                    )} Mhz)!`,
-                    undefined,
-                    7500
-                );
-            } finally {
-                this.resume();
-                buttonBenchmark.classList.remove("enabled");
-            }
-        });
-
-        const buttonFullscreen = document.getElementById("button-fullscreen")!;
-        buttonFullscreen.addEventListener("click", () => {
-            this.maximize();
-        });
-
-        const buttonKeyboard = document.getElementById("button-keyboard")!;
-        buttonKeyboard.addEventListener("click", () => {
-            const sectionKeyboard =
-                document.getElementById("section-keyboard")!;
-            const separatorKeyboard =
-                document.getElementById("separator-keyboard")!;
-            const sectionNarrative =
-                document.getElementById("section-narrative")!;
-            const separatorNarrative = document.getElementById(
-                "separator-narrative"
-            )!;
-            if (buttonKeyboard.classList.contains("enabled")) {
-                sectionKeyboard.style.display = "none";
-                separatorKeyboard.style.display = "none";
-                sectionNarrative.style.display = "block";
-                separatorNarrative.style.display = "block";
-                buttonKeyboard.classList.remove("enabled");
-            } else {
-                sectionKeyboard.style.display = "block";
-                separatorKeyboard.style.display = "block";
-                sectionNarrative.style.display = "none";
-                separatorNarrative.style.display = "none";
-                buttonKeyboard.classList.add("enabled");
-            }
-        });
-
-        const buttonDebug = document.getElementById("button-debug")!;
-        buttonDebug.addEventListener("click", () => {
-            const sectionDebug = document.getElementById("section-debug")!;
-            const separatorDebug = document.getElementById("separator-debug")!;
-            const sectionNarrative =
-                document.getElementById("section-narrative")!;
-            const separatorNarrative = document.getElementById(
-                "separator-narrative"
-            )!;
-            if (buttonDebug.classList.contains("enabled")) {
-                sectionDebug.style.display = "none";
-                separatorDebug.style.display = "none";
-                sectionNarrative.style.display = "block";
-                separatorNarrative.style.display = "block";
-                buttonDebug.classList.remove("enabled");
-            } else {
-                sectionDebug.style.display = "block";
-                separatorDebug.style.display = "block";
-                sectionNarrative.style.display = "none";
-                separatorNarrative.style.display = "none";
-                buttonDebug.classList.add("enabled");
-
-                const canvasTiles = document.getElementById(
-                    "canvas-tiles"
-                ) as HTMLCanvasElement;
-                const canvasTilesCtx = canvasTiles.getContext("2d")!;
-                canvasTilesCtx.imageSmoothingEnabled = false;
-
-                const canvasImage = canvasTilesCtx.createImageData(
-                    canvasTiles.width,
-                    canvasTiles.height
-                );
-                const videoBuff = new DataView(canvasImage.data.buffer);
-
-                /**
-                 * Draws the tile at the given index to the proper
-                 * vertical offset in the given context and buffer.
-                 *
-                 * @param index The index of the sprite to be drawn.
-                 * @param format The pixel format of the sprite.
-                 */
-                const drawTile = (
-                    index: number,
-                    context: CanvasRenderingContext2D,
-                    buffer: DataView,
-                    format: PixelFormat = PixelFormat.RGB
-                ) => {
-                    const pixels = this.gameBoy!.get_tile_buffer(index);
-                    const line = Math.floor(index / 16);
-                    const column = index % 16;
-                    let offset =
-                        (line * canvasTiles.width * 8 + column * 8) *
-                        PixelFormat.RGBA;
-                    let counter = 0;
-                    for (
-                        let index = 0;
-                        index < pixels.length;
-                        index += format
-                    ) {
-                        const color =
-                            (pixels[index] << 24) |
-                            (pixels[index + 1] << 16) |
-                            (pixels[index + 2] << 8) |
-                            (format == PixelFormat.RGBA
-                                ? pixels[index + 3]
-                                : 0xff);
-                        buffer.setUint32(offset, color);
-
-                        counter++;
-                        if (counter == 8) {
-                            counter = 0;
-                            offset +=
-                                (canvasTiles.width - 7) * PixelFormat.RGBA;
-                        } else {
-                            offset += PixelFormat.RGBA;
-                        }
-                    }
-                    context.putImageData(canvasImage, 0, 0);
-                };
-
-                for (let index = 0; index < 384; index++) {
-                    drawTile(index, canvasTilesCtx, videoBuff);
-                }
-
-                const vram = this.gameBoy!.vram_eager();
-                const step = 16;
-                for (let index = 0; index < vram.length; index += step) {
-                    let line = `${(index + 0x8000)
-                        .toString(16)
-                        .padStart(4, "0")}`;
-                    for (let j = 0; j < step; j++) {
-                        line += ` ${vram[index + j]
-                            .toString(16)
-                            .padStart(2, "0")}`;
-                    }
-                    console.info(line);
-                }
-            }
-        });
-
-        const buttonInformation =
-            document.getElementById("button-information")!;
-        buttonInformation.addEventListener("click", () => {
-            const sectionDiag = document.getElementById("section-diag")!;
-            const separatorDiag = document.getElementById("separator-diag")!;
-            if (buttonInformation.classList.contains("enabled")) {
-                sectionDiag.style.display = "none";
-                separatorDiag.style.display = "none";
-                buttonInformation.classList.remove("enabled");
-            } else {
-                sectionDiag.style.display = "block";
-                separatorDiag.style.display = "block";
-                buttonInformation.classList.add("enabled");
-            }
-        });
-
-        const buttonUploadFile = document.getElementById(
-            "button-upload-file"
-        ) as HTMLInputElement;
-        buttonUploadFile.addEventListener("change", async () => {
-            if (
-                !buttonUploadFile.files ||
-                buttonUploadFile.files.length === 0
-            ) {
-                return;
-            }
-
-            const file = buttonUploadFile.files[0];
-
-            const arrayBuffer = await file.arrayBuffer();
-            const romData = new Uint8Array(arrayBuffer);
-
-            buttonUploadFile.value = "";
-
-            this.start({ engine: null, romName: file.name, romData: romData });
-
-            this.showToast(`Loaded ${file.name} ROM successfully!`);
-        });
-    }
-
-    // @todo this should be converted into a component
-    registerKeyboard() {
-        const keyboard = document.getElementById("keyboard")!;
-        const keys = keyboard.getElementsByClassName("key");
-
-        keyboard.addEventListener("touchstart", function (event) {
-            event.preventDefault();
-            event.stopPropagation();
-        });
-
-        keyboard.addEventListener("touchend", function (event) {
-            event.preventDefault();
-            event.stopPropagation();
-        });
-
-        Array.prototype.forEach.call(keys, (k: Element) => {
-            k.addEventListener(
-                "mousedown",
-                function (this: HTMLElement, event) {
-                    const keyCode = KEYS[this.textContent!.toLowerCase()];
-                    //this.gameBoy.key_press_ws(keyCode); @todo
-                    event.preventDefault();
-                    event.stopPropagation();
-                }
-            );
-
-            k.addEventListener(
-                "touchstart",
-                function (this: HTMLElement, event) {
-                    const keyCode = KEYS[this.textContent!.toLowerCase()];
-                    //this.gameBoy.key_press_ws(keyCode); @todo
-                    event.preventDefault();
-                    event.stopPropagation();
-                }
-            );
-
-            k.addEventListener("mouseup", function (this: HTMLElement, event) {
-                const keyCode = KEYS[this.textContent!.toLowerCase()];
-                //this.gameBoy.key_lift_ws(keyCode); @todo
-                event.preventDefault();
-                event.stopPropagation();
-            });
-
-            k.addEventListener("touchend", function (this: HTMLElement, event) {
-                const keyCode = KEYS[this.textContent!.toLowerCase()];
-                //this.gameBoy.key_lift_ws(keyCode); @todo
-                event.preventDefault();
-                event.stopPropagation();
-            });
-        });
+    setRom(name: string, data: Uint8Array, cartridge: Cartridge) {
+        this.romName = name;
+        this.romData = data;
+        this.romSize = data.length;
+        this.cartridge = cartridge;
     }
 
-    registerCanvas() {
-        const canvasClose = document.getElementById("canvas-close")!;
-        canvasClose.addEventListener("click", () => {
-            this.minimize();
-        });
+    getName() {
+        return "Boytacean";
     }
 
-    registerToast() {
-        const toast = document.getElementById("toast")!;
-        toast.addEventListener("click", () => {
-            toast.classList.remove("visible");
-        });
+    getDevice(): string {
+        return "Game Boy";
     }
 
-    registerModal() {
-        const modalClose = document.getElementById("modal-close")!;
-        modalClose.addEventListener("click", () => {
-            this.hideModal(false);
-        });
-
-        const modalCancel = document.getElementById("modal-cancel")!;
-        modalCancel.addEventListener("click", () => {
-            this.hideModal(false);
-        });
-
-        const modalConfirm = document.getElementById("modal-confirm")!;
-        modalConfirm.addEventListener("click", () => {
-            this.hideModal(true);
-        });
-
-        document.addEventListener("keydown", (event) => {
-            if (event.key === "Escape") {
-                this.hideModal(false);
-            }
-        });
+    getDeviceUrl(): string {
+        return "https://en.wikipedia.org/wiki/Game_Boy";
     }
 
-    async initBase() {
-        this.setVersion(info.version);
+    getVersion() {
+        return info.version;
     }
 
-    async initCanvas() {
-        // initializes the off-screen canvas that is going to be
-        // used in the drawing process
-        this.canvas = document.createElement("canvas");
-        this.canvas.width = DISPLAY_WIDTH;
-        this.canvas.height = DISPLAY_HEIGHT;
-        this.canvasCtx = this.canvas.getContext("2d")!;
-
-        this.canvasScaled = document.getElementById(
-            "engine-canvas"
-        ) as HTMLCanvasElement;
-        this.canvasScaled.width =
-            this.canvasScaled.width * window.devicePixelRatio;
-        this.canvasScaled.height =
-            this.canvasScaled.height * window.devicePixelRatio;
-        this.canvasScaledCtx = this.canvasScaled.getContext("2d")!;
-
-        this.canvasScaledCtx.scale(
-            this.canvasScaled.width / this.canvas.width,
-            this.canvasScaled.height / this.canvas.height
-        );
-        this.canvasScaledCtx.imageSmoothingEnabled = false;
-
-        this.image = this.canvasCtx.createImageData(
-            this.canvas.width,
-            this.canvas.height
-        );
-        this.videoBuff = new DataView(this.image.data.buffer);
+    getVersionUrl() {
+        return "https://gitlab.stage.hive.pt/joamag/boytacean/-/blob/master/CHANGELOG.md";
     }
 
-    updateCanvas(pixels: Uint8Array, format: PixelFormat = PixelFormat.RGB) {
-        let offset = 0;
-        for (let index = 0; index < pixels.length; index += format) {
-            const color =
-                (pixels[index] << 24) |
-                (pixels[index + 1] << 16) |
-                (pixels[index + 2] << 8) |
-                (format == PixelFormat.RGBA ? pixels[index + 3] : 0xff);
-            this.videoBuff!.setUint32(offset, color);
-            offset += PixelFormat.RGBA;
-        }
-        this.canvasCtx!.putImageData(this.image!, 0, 0);
-        this.canvasScaledCtx!.drawImage(this.canvas!, 0, 0);
+    getPixelFormat(): PixelFormat {
+        return PixelFormat.RGB;
     }
 
-    async clearCanvas(
-        color = PIXEL_UNSET_COLOR,
-        {
-            image = null,
-            imageScale = 1
-        }: { image?: string | null; imageScale?: number } = {}
-    ) {
-        this.canvasScaledCtx!.fillStyle = `#${color
-            .toString(16)
-            .toUpperCase()}`;
-        this.canvasScaledCtx!.fillRect(
-            0,
-            0,
-            this.canvasScaled!.width,
-            this.canvasScaled!.height
-        );
-
-        // in case an image was requested then uses that to load
-        // an image at the center of the screen properly scaled
-        if (image) {
-            const img = await new Promise<HTMLImageElement>((resolve) => {
-                const img = new Image();
-                img.onload = () => {
-                    resolve(img);
-                };
-                img.src = image;
-            });
-            const [imgWidth, imgHeight] = [
-                img.width * imageScale * window.devicePixelRatio,
-                img.height * imageScale * window.devicePixelRatio
-            ];
-            const [x0, y0] = [
-                this.canvasScaled!.width / 2 - imgWidth / 2,
-                this.canvasScaled!.height / 2 - imgHeight / 2
-            ];
-            this.canvasScaledCtx!.setTransform(1, 0, 0, 1, 0, 0);
-            try {
-                this.canvasScaledCtx!.drawImage(
-                    img,
-                    x0,
-                    y0,
-                    imgWidth,
-                    imgHeight
-                );
-            } finally {
-                this.canvasScaledCtx!.scale(
-                    this.canvasScaled!.width / this.canvas!.width,
-                    this.canvasScaled!.height / this.canvas!.height
-                );
+    /**
+     * Returns the array buffer that contains the complete set of
+     * pixel data that is going to be drawn.
+     *
+     * @returns The current pixel data for the emulator display.
+     */
+    getImageBuffer(): Uint8Array {
+        return this.gameBoy!.frame_buffer_eager();
+    }
+
+    getRomInfo(): RomInfo {
+        return {
+            name: this.romName || undefined,
+            data: this.romData || undefined,
+            size: this.romSize,
+            extra: {
+                romType: this.cartridge?.rom_type_s(),
+                romSize: this.cartridge?.rom_size_s(),
+                ramSize: this.cartridge?.ram_size_s()
             }
-        }
-    }
-
-    async showToast(message: string, error = false, timeout = 3500) {
-        const toast = document.getElementById("toast")!;
-        toast.classList.remove("error");
-        if (error) toast.classList.add("error");
-        toast.classList.add("visible");
-        toast.textContent = message;
-        if (this.toastTimeout) clearTimeout(this.toastTimeout);
-        this.toastTimeout = setTimeout(() => {
-            toast.classList.remove("visible");
-            this.toastTimeout = null;
-        }, timeout);
-    }
-
-    async showModal(message: string, title = "Alert"): Promise<boolean> {
-        const modalContainer = document.getElementById("modal-container")!;
-        const modalTitle = document.getElementById("modal-title")!;
-        const modalText = document.getElementById("modal-text")!;
-        modalContainer.classList.add("visible");
-        modalTitle.textContent = title;
-        modalText.innerHTML = message.replace(/\n/g, "<br/>");
-        const result = (await new Promise((resolve) => {
-            global.modalCallback = resolve;
-        })) as boolean;
-        return result;
+        };
     }
 
-    async hideModal(result = true) {
-        const modalContainer = document.getElementById("modal-container")!;
-        modalContainer.classList.remove("visible");
-        if (global.modalCallback) global.modalCallback(result);
-        global.modalCallback = null;
+    getFramerate(): number {
+        return this.fps;
     }
 
-    setVersion(value: string) {
-        document.getElementById("version")!.textContent = value;
-    }
-
-    setEngine(name: string, upper = true) {
-        name = upper ? name.toUpperCase() : name;
-        document.getElementById("engine")!.textContent = name;
-    }
-
-    setRom(name: string, data: Uint8Array) {
-        this.romName = name;
-        this.romData = data;
-        this.romSize = data.length;
-        //@todo update this one
-        //Component.get<KeyValue>("diag:rom-name").value = name;
-        //Component.get<KeyValue>("diag:rom-size").value = `${data.length} bytes`;
-    }
-
-    setLogicFrequency(value: number) {
-        if (value < 0) this.showToast("Invalid frequency value!", true);
-        value = Math.max(value, 0);
-        this.logicFrequency = value;
-        document.getElementById("logic-frequency")!.textContent = String(value);
-    }
-
-    setFps(value: number) {
-        if (value < 0) this.showToast("Invalid FPS value!", true);
-        value = Math.max(value, 0);
-        this.fps = value;
-        //@todo
-        //Component.get<KeyValue>("diag:framerate").value = `${value} FPS`;
-    }
-
-    getName() {
-        return "Boytacean";
-    }
-
-    getVersion() {
-        return info.version;
-    }
-
-    getVersionUrl() {
-        return "https://gitlab.stage.hive.pt/joamag/boytacean/-/blob/master/CHANGELOG.md";
+    getTile(index: number): Uint8Array {
+        return this.gameBoy!.get_tile_buffer(index);
     }
 
     toggleRunning() {
@@ -968,88 +432,39 @@ class GameboyEmulator implements Emulator {
 
     pause() {
         this.paused = true;
-        const buttonPause = document.getElementById("button-pause")!;
-        const img = buttonPause.getElementsByTagName("img")[0];
-        const span = buttonPause.getElementsByTagName("span")[0];
-        buttonPause.classList.add("enabled");
-        img.src = require("./res/play.svg");
-        span.textContent = "Resume";
     }
 
     resume() {
         this.paused = false;
         this.nextTickTime = new Date().getTime();
-        const buttonPause = document.getElementById("button-pause")!;
-        const img = buttonPause.getElementsByTagName("img")[0];
-        const span = buttonPause.getElementsByTagName("span")[0];
-        buttonPause.classList.remove("enabled");
-        img.src = require("./res/pause.svg");
-        span.textContent = "Pause";
     }
 
-    /**
-     * Resets the emulator machine to the start state and loads
-     * the ROM that is currently set in the emulator.
-     */
     reset() {
-        this.start({ engine: null });
-    }
-
-    toggleWindow() {
-        this.maximize();
-    }
-
-    /**
-     * Maximizes the emulator's viewport taking up all the available
-     * window space. This method is responsible for keeping the aspect
-     * ratio of the emulator canvas according to the width/height ratio.
-     */
-    maximize() {
-        const canvasContainer = document.getElementById("canvas-container")!;
-        canvasContainer.classList.add("fullscreen");
-
-        window.addEventListener("resize", this.crop);
-
-        this.crop();
+        this.boot({ engine: null });
     }
 
-    /**
-     * Restore the emulator's viewport to the minimal size, should make all
-     * the other emulator's meta-information (info, buttons, etc.) visible.
-     */
-    minimize() {
-        const canvasContainer = document.getElementById("canvas-container")!;
-        const engineCanvas = document.getElementById("engine-canvas")!;
-        canvasContainer.classList.remove("fullscreen");
-        engineCanvas.style.width = "";
-        engineCanvas.style.height = "";
-        window.removeEventListener("resize", this.crop);
-    }
-
-    crop() {
-        // @todo let's make this more flexible
-        const engineCanvas = document.getElementById("engine-canvas")!;
-
-        // calculates the window ratio as this is fundamental to
-        // determine the proper way to crop the fullscreen
-        const windowRatio = window.innerWidth / window.innerHeight;
-
-        // in case the window is wider (more horizontal than the base ratio)
-        // this means that we must crop horizontally
-        if (windowRatio > DISPLAY_RATIO) {
-            engineCanvas.style.width = `${
-                window.innerWidth * (DISPLAY_RATIO / windowRatio)
-            }px`;
-            engineCanvas.style.height = `${window.innerHeight}px`;
-        } else {
-            engineCanvas.style.width = `${window.innerWidth}px`;
-            engineCanvas.style.height = `${
-                window.innerHeight * (windowRatio / DISPLAY_RATIO)
-            }px`;
+    benchmark(count = 50000000) {
+        let cycles = 0;
+        this.pause();
+        try {
+            const initial = Date.now();
+            for (let i = 0; i < count; i++) {
+                cycles += this.gameBoy!.clock();
+            }
+            const delta = (Date.now() - initial) / 1000;
+            const frequency_mhz = cycles / delta / 1000 / 1000;
+            return {
+                delta: delta,
+                count: count,
+                cycles: cycles,
+                frequency_mhz: frequency_mhz
+            };
+        } finally {
+            this.resume();
         }
     }
 
-    async fetchRom(romPath: string): Promise<[string, Uint8Array]> {
+    private async fetchRom(romPath: string): Promise<[string, Uint8Array]> {
         // extracts the name of the ROM from the provided
         // path by splitting its structure
         const romPathS = romPath.split(/\//g);
@@ -1070,15 +485,6 @@ class GameboyEmulator implements Emulator {
     }
 }
 
-type Global = {
-    modalCallback: Function | null;
-};
-
-//@todo check if this is really required
-const global: Global = {
-    modalCallback: null
-};
-
 declare global {
     interface Window {
         panic: (message: string) => void;
diff --git a/examples/web/package.json b/examples/web/package.json
index a956b883f06a58671732d1cff3f19fff1b0274ed..3f3fb0ce3f4a4c3b674407b58fddcf502b685f06 100644
--- a/examples/web/package.json
+++ b/examples/web/package.json
@@ -1,6 +1,6 @@
 {
     "name": "boytacean-web",
-    "version": "0.3.0",
+    "version": "0.4.0",
     "description": "The web version of Boytacean",
     "repository": {
         "type": "git",
diff --git a/examples/web/react/app.css b/examples/web/react/app.css
index 849c49d05241bc10b7e35cf04c75a8eee7e99e67..4c1786700a5d9be248c7a518c125744177b3972e 100644
--- a/examples/web/react/app.css
+++ b/examples/web/react/app.css
@@ -2,5 +2,18 @@
     color: #ffffff;
     font-family: VT323, Roboto, Open Sans, Arial, Helvetica, sans-serif;
     margin: 0px 0px 0px 0px;
-    display: none;
+}
+
+.app h3 {
+    margin: 10px 0px 10px 0px;
+}
+
+.app .display-container {
+    margin-top: 78px;
+}
+
+@media only screen and (max-width: 1120px) {
+    .app .display-container {
+        margin-top: 0px;
+    }
 }
diff --git a/examples/web/react/app.tsx b/examples/web/react/app.tsx
index 3144dab2db6e410d8e628d5b2579f13519ac8c81..e8b7269c0320e20342cdce9b3617d380f92f7acd 100644
--- a/examples/web/react/app.tsx
+++ b/examples/web/react/app.tsx
@@ -1,31 +1,211 @@
-import React, { FC, useEffect, useState } from "react";
+import React, { FC, useEffect, useRef, useState } from "react";
 import ReactDOM from "react-dom/client";
 
+declare const require: any;
+
 import {
     Button,
     ButtonContainer,
     ButtonIncrement,
     ButtonSwitch,
+    ClearHandler,
+    Display,
+    DrawHandler,
     Footer,
     Info,
+    KeyboardGB,
     Link,
+    Modal,
+    Overlay,
     Pair,
     PanelSplit,
     Paragraph,
     Section,
-    Title
+    Tiles,
+    Title,
+    Toast
 } from "./components";
 
 import "./app.css";
 
-export interface Emulator {
+export type Callback<T> = (owner: T, params?: Record<string, any>) => void;
+
+/**
+ * Abstract class that implements the basic functionality
+ * part of the definition of the Observer pattern.
+ *
+ * @see {@link https://en.wikipedia.org/wiki/Observer_pattern}
+ */
+export class Observable {
+    private events: Record<string, [Callback<this>]> = {};
+
+    bind(event: string, callback: Callback<this>) {
+        const callbacks = this.events[event] ?? [];
+        if (callbacks.includes(callback)) return;
+        callbacks.push(callback);
+        this.events[event] = callbacks;
+    }
+
+    unbind(event: string, callback: Callback<this>) {
+        const callbacks = this.events[event] ?? [];
+        if (!callbacks.includes(callback)) return;
+        const index = callbacks.indexOf(callback);
+        callbacks.splice(index, 1);
+        this.events[event] = callbacks;
+    }
+
+    trigger(event: string, params?: Record<string, any>) {
+        const callbacks = this.events[event] ?? [];
+        callbacks.forEach((c) => c(this, params));
+    }
+}
+
+export type RomInfo = {
+    name?: string;
+    data?: Uint8Array;
+    size?: number;
+    extra?: Record<string, string | undefined>;
+};
+
+export type BenchmarkResult = {
+    delta: number;
+    count: number;
+    cycles: number;
+    frequency_mhz: number;
+};
+
+export interface ObservableI {
+    bind(event: string, callback: Callback<this>): void;
+    unbind(event: string, callback: Callback<this>): void;
+    trigger(event: string): void;
+}
+
+/**
+ * Top level interface that declares the main abstract
+ * interface of an emulator structured entity.
+ * Should allow typical hardware operations to be performed.
+ */
+export interface Emulator extends ObservableI {
+    /**
+     * Obtains the descriptive name of the emulator.
+     *
+     * @returns The descriptive name of the emulator.
+     */
     getName(): string;
+
+    /**
+     * Obtains the name of the name of the hardware that
+     * is being emulated by the emulator (eg: Super Nintendo).
+     *
+     * @returns The name of the hardware that is being
+     * emulated.
+     */
+    getDevice(): string;
+
+    getDeviceUrl?(): string;
+
+    /**
+     * Obtains a semantic version string for the current
+     * version of the emulator.
+     *
+     * @returns The semantic version string.
+     * @see {@link https://semver.org}
+     */
     getVersion(): string;
-    getVersionUrl(): string;
+
+    /**
+     * Obtains a URL to the page describing the current version
+     * of the emulator.
+     *
+     * @returns A URL to the page describing the current version
+     * of the emulator.
+     */
+    getVersionUrl?(): string;
+
+    /**
+     * Obtains the pixel format of the emulator's display
+     * image buffer (eg: RGB).
+     *
+     * @returns The pixel format used for the emulator's
+     * image buffer.
+     */
+    getPixelFormat(): PixelFormat;
+
+    /**
+     * Obtains the complete image buffer as a sequence of
+     * bytes that respects the current pixel format from
+     * `getPixelFormat()`. This method returns an in memory
+     * pointer to the heap and not a copy.
+     *
+     * @returns The byte based image buffer that respects
+     * the emulator's pixel format.
+     */
+    getImageBuffer(): Uint8Array;
+
+    /**
+     * Obtains information about the ROM that is currently
+     * loaded in the emulator.
+     *
+     * @returns Structure containing the information about
+     * the ROM that is currently loaded in the emulator.
+     */
+    getRomInfo(): RomInfo;
+
+    /**
+     * Returns the current logic framerate of the running
+     * emulator.
+     *
+     * @returns The current logic framerate of the running
+     * emulator.
+     */
+    getFramerate(): number;
+
+    getTile(index: number): Uint8Array;
+
+    /**
+     * Boot (or reboots) the emulator according to the provided
+     * set of options.
+     *
+     * @param options The options that are going to be used for
+     * the booting operation of the emulator.
+     */
+    boot(options: any): void;
+
+    /**
+     * Toggle the running state of the emulator between paused
+     * and running, prevents consumers from the need to access
+     * the current running state of the emulator to implement
+     * a logic toggle.
+     */
     toggleRunning(): void;
     pause(): void;
     resume(): void;
+
+    /**
+     * Resets the emulator machine to the start state and
+     * re-loads the ROM that is currently set in the emulator.
+     */
     reset(): void;
+
+    /**
+     * Runs a benchmark operation in the emulator, effectively
+     * measuring the performance of it.
+     *
+     * @param count The number of benchmark iterations to be
+     * run, increasing this value will make the benchmark take
+     * more time to be executed.
+     * @returns The result metrics from the benchmark run.
+     */
+    benchmark(count?: number): BenchmarkResult;
+}
+
+/**
+ * Enumeration that describes the multiple pixel
+ * formats and the associated size in bytes.
+ */
+export enum PixelFormat {
+    RGB = 3,
+    RGBA = 4
 }
 
 type AppProps = {
@@ -35,11 +215,143 @@ type AppProps = {
 
 export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
     const [paused, setPaused] = useState(false);
+    const [fullscreen, setFullscreen] = useState(false);
     const [backgroundIndex, setBackgroundIndex] = useState(0);
+    const [romInfo, setRomInfo] = useState<RomInfo>({});
+    const [framerate, setFramerate] = useState(0);
+    const [keyaction, setKeyaction] = useState<string>();
+    const [modalTitle, setModalTitle] = useState<string>();
+    const [modalText, setModalText] = useState<string>();
+    const [modalVisible, setModalVisible] = useState(false);
+    const [toastText, setToastText] = useState<string>();
+    const [toastError, setToastError] = useState(false);
+    const [toastVisible, setToastVisible] = useState(false);
+    const [keyboardVisible, setKeyboardVisible] = useState(false);
+    const [infoVisible, setInfoVisible] = useState(true);
+    const [debugVisible, setDebugVisible] = useState(false);
+
+    const toastCounterRef = useRef(0);
+    const frameRef = useRef<boolean>(false);
+    const errorRef = useRef<boolean>(false);
+    const modalCallbackRef =
+        useRef<(value: boolean | PromiseLike<boolean>) => void>();
+
+    useEffect(() => {
+        document.body.style.backgroundColor = `#${getBackground()}`;
+    }, [backgroundIndex]);
+    useEffect(() => {
+        switch (keyaction) {
+            case "Escape":
+                setFullscreen(false);
+                setKeyaction(undefined);
+                break;
+            case "Fullscreen":
+                setFullscreen(!fullscreen);
+                setKeyaction(undefined);
+                break;
+        }
+    }, [keyaction]);
+    useEffect(() => {
+        const onKeyDown = (event: KeyboardEvent) => {
+            if (event.key === "Escape") {
+                setKeyaction("Escape");
+                event.stopPropagation();
+                event.preventDefault();
+            }
+            if (event.key === "f" && event.ctrlKey === true) {
+                setKeyaction("Fullscreen");
+                event.stopPropagation();
+                event.preventDefault();
+            }
+        };
+        const onBooted = () => {
+            const romInfo = emulator.getRomInfo();
+            setRomInfo(romInfo);
+            setPaused(false);
+        };
+        const onMessage = (
+            emulator: Emulator,
+            params: Record<string, any> = {}
+        ) => {
+            showToast(params.text, params.error, params.timeout);
+        };
+        document.addEventListener("keydown", onKeyDown);
+        emulator.bind("booted", onBooted);
+        emulator.bind("message", onMessage);
+        return () => {
+            document.removeEventListener("keydown", onKeyDown);
+            emulator.unbind("booted", onBooted);
+            emulator.unbind("message", onMessage);
+        };
+    }, []);
+
     const getPauseText = () => (paused ? "Resume" : "Pause");
     const getPauseIcon = () =>
         paused ? require("../res/play.svg") : require("../res/pause.svg");
     const getBackground = () => backgrounds[backgroundIndex];
+
+    const showModal = async (
+        text: string,
+        title = "Alert"
+    ): Promise<boolean> => {
+        setModalText(text);
+        setModalTitle(title);
+        setModalVisible(true);
+        const result = (await new Promise((resolve) => {
+            modalCallbackRef.current = resolve;
+        })) as boolean;
+        return result;
+    };
+    const showToast = async (text: string, error = false, timeout = 3500) => {
+        setToastText(text);
+        setToastError(error);
+        setToastVisible(true);
+        toastCounterRef.current++;
+        const counter = toastCounterRef.current;
+        await new Promise((resolve) => {
+            setTimeout(() => {
+                if (counter !== toastCounterRef.current) return;
+                setToastVisible(false);
+                resolve(true);
+            }, timeout);
+        });
+    };
+
+    const onFile = async (file: File) => {
+        // @todo must make this more flexible and not just
+        // Game Boy only (using the emulator interface)
+        if (!file.name.endsWith(".gb")) {
+            showToast(
+                `This is probably not a ${emulator.getDevice()} ROM file!`,
+                true
+            );
+            return;
+        }
+
+        const arrayBuffer = await file.arrayBuffer();
+        const romData = new Uint8Array(arrayBuffer);
+
+        emulator.boot({ engine: null, romName: file.name, romData: romData });
+
+        showToast(`Loaded ${file.name} ROM successfully!`);
+    };
+    const onModalConfirm = () => {
+        if (modalCallbackRef.current) {
+            modalCallbackRef.current(true);
+            modalCallbackRef.current = undefined;
+        }
+        setModalVisible(false);
+    };
+    const onModalCancel = () => {
+        if (modalCallbackRef.current) {
+            modalCallbackRef.current(false);
+            modalCallbackRef.current = undefined;
+        }
+        setModalVisible(false);
+    };
+    const onToastCancel = () => {
+        setToastVisible(false);
+    };
     const onPauseClick = () => {
         emulator.toggleRunning();
         setPaused(!paused);
@@ -47,36 +359,126 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
     const onResetClick = () => {
         emulator.reset();
     };
+    const onBenchmarkClick = async () => {
+        const result = await showModal(
+            "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
+            "Confirm"
+        );
+        if (!result) return;
+        const { delta, count, frequency_mhz } = emulator.benchmark();
+        await showToast(
+            `Took ${delta.toFixed(
+                2
+            )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
+                2
+            )} Mhz)!`,
+            undefined,
+            7500
+        );
+    };
+    const onFullscreenClick = () => {
+        setFullscreen(!fullscreen);
+    };
+    const onKeyboardClick = () => {
+        setKeyboardVisible(!keyboardVisible);
+    };
+    const onInformationClick = () => {
+        setInfoVisible(!infoVisible);
+    };
+    const onDebugClick = () => {
+        setDebugVisible(!debugVisible);
+    };
     const onThemeClick = () => {
         setBackgroundIndex((backgroundIndex + 1) % backgrounds.length);
     };
-    useEffect(() => {
-        document.body.style.backgroundColor = `#${getBackground()}`;
-    });
+    const onUploadFile = async (file: File) => {
+        const arrayBuffer = await file.arrayBuffer();
+        const romData = new Uint8Array(arrayBuffer);
+        emulator.boot({ engine: null, romName: file.name, romData: romData });
+        showToast(`Loaded ${file.name} ROM successfully!`);
+    };
+    const onEngineChange = (engine: string) => {
+        emulator.boot({ engine: engine.toLowerCase() });
+        showToast(
+            `${emulator.getDevice()} running in engine "${engine}" from now on!`
+        );
+    };
+    const onMinimize = () => {
+        setFullscreen(!fullscreen);
+    };
+    const onDrawHandler = (handler: DrawHandler) => {
+        if (frameRef.current) return;
+        frameRef.current = true;
+        emulator.bind("frame", () => {
+            handler(emulator.getImageBuffer(), PixelFormat.RGB);
+            setFramerate(emulator.getFramerate());
+        });
+    };
+    const onClearHandler = (handler: ClearHandler) => {
+        if (errorRef.current) return;
+        errorRef.current = true;
+        emulator.bind("error", async () => {
+            await handler(undefined, require("../res/storm.png"), 0.2);
+        });
+    };
+
     return (
         <div className="app">
+            <Overlay text={"Drag to load ROM"} onFile={onFile} />
+            <Modal
+                title={modalTitle}
+                text={modalText}
+                visible={modalVisible}
+                onConfirm={onModalConfirm}
+                onCancel={onModalCancel}
+            />
+            <Toast
+                text={toastText}
+                error={toastError}
+                visible={toastVisible}
+                onCancel={onToastCancel}
+            />
             <Footer color={getBackground()}>
                 Built with ❤️ by{" "}
                 <Link href="https://joao.me" target="_blank">
                     João Magalhães
                 </Link>
             </Footer>
-            <PanelSplit left={<div>This is the left panel</div>}>
+            <PanelSplit
+                left={
+                    <div className="display-container">
+                        <Display
+                            fullscreen={fullscreen}
+                            onDrawHandler={onDrawHandler}
+                            onClearHandler={onClearHandler}
+                            onMinimize={onMinimize}
+                        />
+                    </div>
+                }
+            >
                 <Title
                     text={emulator.getName()}
                     version={emulator.getVersion()}
-                    versionUrl={emulator.getVersionUrl()}
+                    versionUrl={
+                        emulator.getVersionUrl
+                            ? emulator.getVersionUrl()
+                            : undefined
+                    }
                     iconSrc={require("../res/thunder.png")}
                 ></Title>
                 <Section>
                     <Paragraph>
                         This is a{" "}
-                        <Link
-                            href="https://en.wikipedia.org/wiki/Game_Boy"
-                            target="_blank"
-                        >
-                            Game Boy
-                        </Link>{" "}
+                        {emulator.getDeviceUrl ? (
+                            <Link
+                                href={emulator.getDeviceUrl()}
+                                target="_blank"
+                            >
+                                {emulator.getDevice()}
+                            </Link>
+                        ) : (
+                            emulator.getDevice()
+                        )}{" "}
                         emulator built using the{" "}
                         <Link href="https://www.rust-lang.org" target="_blank">
                             Rust Programming Language
@@ -102,55 +504,149 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
                         ROM.
                     </Paragraph>
                 </Section>
+                {keyboardVisible && (
+                    <Section>
+                        <KeyboardGB />
+                    </Section>
+                )}
+                {debugVisible && (
+                    <Section>
+                        <h3>VRAM Tiles</h3>
+                        <Tiles
+                            getTile={(index) => emulator.getTile(index)}
+                            tileCount={384}
+                        />
+                    </Section>
+                )}
+                {infoVisible && (
+                    <Section>
+                        <Info>
+                            <Pair
+                                key="button-engine"
+                                name={"Engine"}
+                                valueNode={
+                                    <ButtonSwitch
+                                        options={["NEO", "CLASSIC"]}
+                                        size={"large"}
+                                        style={["simple"]}
+                                        onChange={onEngineChange}
+                                    />
+                                }
+                            />
+                            <Pair
+                                key="rom"
+                                name={"ROM"}
+                                value={romInfo.name ?? "-"}
+                            />
+                            <Pair
+                                key="rom-size"
+                                name={"ROM Size"}
+                                value={
+                                    romInfo.name ? `${romInfo.size} bytes` : "-"
+                                }
+                            />
+                            <Pair
+                                key="button-frequency"
+                                name={"CPU Frequency"}
+                                valueNode={
+                                    <ButtonIncrement
+                                        value={4.19}
+                                        delta={0.1}
+                                        min={0}
+                                        suffix={"MHz"}
+                                        decimalPlaces={2}
+                                    />
+                                }
+                            />
+                            <Pair
+                                key="rom-type"
+                                name={"ROM Type"}
+                                value={
+                                    romInfo.extra?.romType
+                                        ? `${romInfo.extra?.romType}`
+                                        : "-"
+                                }
+                            />
+                            <Pair
+                                key="framerate"
+                                name={"Framerate"}
+                                value={`${framerate} fps`}
+                            />
+                        </Info>
+                    </Section>
+                )}
                 <Section>
                     <ButtonContainer>
                         <Button
                             text={getPauseText()}
                             image={getPauseIcon()}
                             imageAlt="pause"
+                            enabled={paused}
+                            style={["simple", "border", "padded"]}
                             onClick={onPauseClick}
                         />
                         <Button
                             text={"Reset"}
                             image={require("../res/reset.svg")}
                             imageAlt="reset"
+                            style={["simple", "border", "padded"]}
                             onClick={onResetClick}
                         />
+                        <Button
+                            text={"Benchmark"}
+                            image={require("../res/bolt.svg")}
+                            imageAlt="benchmark"
+                            style={["simple", "border", "padded"]}
+                            onClick={onBenchmarkClick}
+                        />
+                        <Button
+                            text={"Fullscreen"}
+                            image={require("../res/maximise.svg")}
+                            imageAlt="maximise"
+                            style={["simple", "border", "padded"]}
+                            onClick={onFullscreenClick}
+                        />
+                        <Button
+                            text={"Keyboard"}
+                            image={require("../res/dialpad.svg")}
+                            imageAlt="keyboard"
+                            enabled={keyboardVisible}
+                            style={["simple", "border", "padded"]}
+                            onClick={onKeyboardClick}
+                        />
+                        <Button
+                            text={"Information"}
+                            image={require("../res/info.svg")}
+                            imageAlt="information"
+                            enabled={infoVisible}
+                            style={["simple", "border", "padded"]}
+                            onClick={onInformationClick}
+                        />
+                        <Button
+                            text={"Debug"}
+                            image={require("../res/bug.svg")}
+                            imageAlt="debug"
+                            enabled={debugVisible}
+                            style={["simple", "border", "padded"]}
+                            onClick={onDebugClick}
+                        />
                         <Button
                             text={"Theme"}
                             image={require("../res/marker.svg")}
-                            imageAlt="marker"
+                            imageAlt="theme"
+                            style={["simple", "border", "padded"]}
                             onClick={onThemeClick}
                         />
-                    </ButtonContainer>
-                    <Info>
-                        <Pair key="tobias" name={"Tobias"} value={"Matias"} />
-                        <Pair key="matias" name={"Matias"} value={"3"} />
-                        <Pair
-                            key="button-tobias"
-                            name={"Button Increment"}
-                            valueNode={
-                                <ButtonIncrement
-                                    value={200}
-                                    delta={100}
-                                    min={0}
-                                    suffix={"Hz"}
-                                />
-                            }
-                        />
-                        <Pair
-                            key="button-cpu"
-                            name={"Button Switch"}
-                            valueNode={
-                                <ButtonSwitch
-                                    options={["NEO", "CLASSIC"]}
-                                    size={"large"}
-                                    style={["simple"]}
-                                    onChange={(v) => alert(v)}
-                                />
-                            }
+                        <Button
+                            text={"Load ROM"}
+                            image={require("../res/upload.svg")}
+                            imageAlt="upload"
+                            file={true}
+                            accept={".gb"}
+                            style={["simple", "border", "padded"]}
+                            onFile={onUploadFile}
                         />
-                    </Info>
+                    </ButtonContainer>
                 </Section>
             </PanelSplit>
         </div>
diff --git a/examples/web/react/components/button-container/button-container.css b/examples/web/react/components/button-container/button-container.css
index eb4dc7f3a15f29db9fcb8a9d7ab5618353281f65..1d1df8f7b94bc330238cfcb7dbaa2ecea756599c 100644
--- a/examples/web/react/components/button-container/button-container.css
+++ b/examples/web/react/components/button-container/button-container.css
@@ -1,4 +1,9 @@
+.button-container {
+    margin-bottom: -12px;
+}
+
 .button-container > * {
+    margin-bottom: 12px;
     margin-right: 8px;
 }
 
diff --git a/examples/web/react/components/button-increment/button-increment.tsx b/examples/web/react/components/button-increment/button-increment.tsx
index a4395b771baa97b2319bc081616abf6307b7845d..62a9071ad02633bc663095c388215871d660fc24 100644
--- a/examples/web/react/components/button-increment/button-increment.tsx
+++ b/examples/web/react/components/button-increment/button-increment.tsx
@@ -10,6 +10,7 @@ type ButtonIncrementProps = {
     max?: number;
     prefix?: string;
     suffix?: string;
+    decimalPlaces?: number;
     size?: string;
     style?: string[];
     onClick?: () => void;
@@ -24,6 +25,7 @@ export const ButtonIncrement: FC<ButtonIncrementProps> = ({
     max,
     prefix,
     suffix,
+    decimalPlaces,
     size = "medium",
     style = ["simple", "border"],
     onClick,
@@ -32,9 +34,6 @@ export const ButtonIncrement: FC<ButtonIncrementProps> = ({
 }) => {
     const [valueState, setValue] = useState(value);
     const classes = () => ["button-increment", size, ...style].join(" ");
-    const _onClick = () => {
-        if (onClick) onClick();
-    };
     const _onMinusClick = () => {
         const valueNew = valueState - delta;
         if (onBeforeChange) {
@@ -54,7 +53,7 @@ export const ButtonIncrement: FC<ButtonIncrementProps> = ({
         if (onChange) onChange(valueNew);
     };
     return (
-        <span className={classes()} onClick={_onClick}>
+        <span className={classes()} onClick={onClick}>
             <Button
                 text={"-"}
                 size={size}
@@ -62,7 +61,9 @@ export const ButtonIncrement: FC<ButtonIncrementProps> = ({
                 onClick={_onMinusClick}
             />
             {prefix && <span className="prefix">{prefix}</span>}
-            <span className="value">{valueState}</span>
+            <span className="value">
+                {decimalPlaces ? valueState.toFixed(decimalPlaces) : valueState}
+            </span>
             {suffix && <span className="suffix">{suffix}</span>}
             <Button
                 text={"+"}
diff --git a/examples/web/react/components/button/button.css b/examples/web/react/components/button/button.css
index 67b45263526fd7fb20cc9ac9d4cf02ae6628441c..b83e604f23ecda9ca9251c03ce2da6ca55b1c1de 100644
--- a/examples/web/react/components/button/button.css
+++ b/examples/web/react/components/button/button.css
@@ -30,7 +30,7 @@
 }
 
 .button.simple.padded {
-    padding: 4px 10px 4px 10px;
+    padding: 0px 10px 0px 10px;
 }
 
 .button.simple.padded-large {
@@ -71,15 +71,15 @@
     width: 13px;
 }
 
-.button.simple > img.medium {
+.button.simple.medium > img {
     width: 20px;
 }
 
-.button.simple > img.large {
+.button.simple.large > img {
     width: 28px;
 }
 
-.button.simple > img.very-large {
+.button.simple.very-large > img {
     width: 38px;
 }
 
diff --git a/examples/web/react/components/button/button.tsx b/examples/web/react/components/button/button.tsx
index 7858e7ee1cb1a8d6a50c89201d6921cb7f38b947..8e26fb5d425d5113dfab94027feea8d205495d62 100644
--- a/examples/web/react/components/button/button.tsx
+++ b/examples/web/react/components/button/button.tsx
@@ -1,4 +1,4 @@
-import React, { FC } from "react";
+import React, { ChangeEvent, FC } from "react";
 
 import "./button.css";
 
@@ -6,33 +6,58 @@ type ButtonProps = {
     text: string;
     image?: string;
     imageAlt?: string;
+    enabled?: boolean;
+    file?: boolean;
+    accept?: string;
     size?: string;
     style?: string[];
     onClick?: () => void;
+    onFile?: (file: File) => void;
 };
 
 export const Button: FC<ButtonProps> = ({
     text,
     image,
     imageAlt,
+    enabled = false,
+    file = false,
+    accept = ".txt",
     size = "small",
     style = ["simple", "border"],
-    onClick
+    onClick,
+    onFile
 }) => {
-    const classes = () => ["button", size, ...style].join(" ");
-    const _onClick = () => (onClick ? onClick() : undefined);
-    const buttonSimple = () => (
-        <span className={classes()} onClick={_onClick}>
+    const classes = () =>
+        [
+            "button",
+            size,
+            enabled ? "enabled" : "",
+            file ? "file" : "",
+            ...style
+        ].join(" ");
+    const onFileChange = (event: ChangeEvent<HTMLInputElement>) => {
+        if (!event.target.files || event.target.files.length === 0) {
+            return;
+        }
+        const file = event.target.files[0];
+        onFile && onFile(file);
+        event.target.value = "";
+    };
+    const renderSimple = () => (
+        <span className={classes()} onClick={onClick}>
             {text}
         </span>
     );
-    const buttonImage = () => (
-        <span className={classes()} onClick={_onClick}>
-            <img src={image} alt={imageAlt} />
+    const renderComplex = () => (
+        <span className={classes()} onClick={onClick}>
+            {image && <img src={image} alt={imageAlt || text || "button"} />}
+            {file && (
+                <input type="file" accept={accept} onChange={onFileChange} />
+            )}
             <span>{text}</span>
         </span>
     );
-    return image ? buttonImage() : buttonSimple();
+    return image ? renderComplex() : renderSimple();
 };
 
 export default Button;
diff --git a/examples/web/react/components/canvas/canvas.css b/examples/web/react/components/canvas/canvas.css
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/examples/web/react/components/canvas/canvas.tsx b/examples/web/react/components/canvas/canvas.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..24f7544a1c8e7f5de294f02b474cda82f3dda36e
--- /dev/null
+++ b/examples/web/react/components/canvas/canvas.tsx
@@ -0,0 +1,72 @@
+import React, { FC, useEffect, useRef } from "react";
+
+import "./canvas.css";
+
+export type CanvasStructure = {
+    canvas: HTMLCanvasElement;
+    canvasContext: CanvasRenderingContext2D;
+    canvasImage: ImageData;
+    canvasBuffer: DataView;
+};
+
+type CanvasProps = {
+    width: number;
+    height: number;
+    scale?: number;
+    style?: string[];
+    onCanvas?: (structure: CanvasStructure) => void;
+};
+
+export const Canvas: FC<CanvasProps> = ({
+    width,
+    height,
+    scale = 1,
+    style = [],
+    onCanvas
+}) => {
+    const classes = () => ["canvas", ...style].join(" ");
+    const canvasRef = useRef<HTMLCanvasElement>(null);
+    useEffect(() => {
+        if (canvasRef.current) {
+            const structure = initCanvas(
+                width,
+                height,
+                scale,
+                canvasRef.current
+            );
+            onCanvas && onCanvas(structure);
+        }
+    }, [canvasRef]);
+    return (
+        <canvas
+            ref={canvasRef}
+            className={classes()}
+            style={{ width: width * scale }}
+            width={width}
+            height={height}
+        />
+    );
+};
+
+const initCanvas = (
+    width: number,
+    height: number,
+    scale: number,
+    canvas: HTMLCanvasElement,
+    smoothing = false
+): CanvasStructure => {
+    const canvasContext = canvas.getContext("2d")!;
+    canvasContext.imageSmoothingEnabled = smoothing;
+
+    const canvasImage = canvasContext.createImageData(width, height);
+    const canvasBuffer = new DataView(canvasImage.data.buffer);
+
+    return {
+        canvas: canvas,
+        canvasContext: canvasContext,
+        canvasImage: canvasImage,
+        canvasBuffer: canvasBuffer
+    };
+};
+
+export default Canvas;
diff --git a/examples/web/react/components/display/display.css b/examples/web/react/components/display/display.css
new file mode 100644
index 0000000000000000000000000000000000000000..0781b767f944e7c3e73c50e0533e63bd6cd9f49c
--- /dev/null
+++ b/examples/web/react/components/display/display.css
@@ -0,0 +1,104 @@
+.display {
+    max-width: 100%;
+}
+
+.display.fullscreen {
+    align-items: center;
+    background-color: #2d2d2d;
+    display: flex;
+    height: 100%;
+    justify-content: center;
+    left: 0px;
+    position: fixed;
+    top: 0px;
+    width: 100%;
+    z-index: 6;
+}
+
+.display > .magnify-button {
+    cursor: pointer;
+    display: inline-block;
+    transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+}
+
+.display > .magnify-button:hover {
+    transform: scale(1.3, 1.3);
+    -o-transform: scale(1.3, 1.3);
+    -ms-transform: scale(1.3, 1.3);
+    -moz-transform: scale(1.3, 1.3);
+    -khtml-transform: scale(1.3, 1.3);
+    -webkit-transform: scale(1.3, 1.3);
+}
+
+.display > .magnify-button:active {
+    transform: scale(1.0, 1.0);
+    -o-transform: scale(1.0, 1.0);
+    -ms-transform: scale(1.0, 1.0);
+    -moz-transform: scale(1.0, 1.0);
+    -khtml-transform: scale(1.0, 1.0);
+    -webkit-transform: scale(1.0, 1.0);
+}
+
+.display > .display-minimize {
+    bottom: 22px;
+    display: none;
+    position: absolute;
+    right: 22px;
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.display > .display-minimize > img {
+    height: 32px;
+    width: 32px;
+}
+
+.display.fullscreen > .display-minimize {
+    display: block;
+}
+
+.display > .display-frame {
+    background-color: #1b1a17;
+    border: 2px solid #50cb93;
+    box-sizing: content-box;
+    -o-box-sizing: content-box;
+    -ms-box-sizing: content-box;
+    -moz-box-sizing: content-box;
+    -khtml-box-sizing: content-box;
+    -webkit-box-sizing: content-box;
+    font-size: 0px;
+    padding: 8px 8px 8px 8px;
+    transition: width 0.35s cubic-bezier(0.075, 0.82, 0.165, 1), height 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: width 0.35s cubic-bezier(0.075, 0.82, 0.165, 1), height 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: width 0.35s cubic-bezier(0.075, 0.82, 0.165, 1), height 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: width 0.35s cubic-bezier(0.075, 0.82, 0.165, 1), height 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: width 0.35s cubic-bezier(0.075, 0.82, 0.165, 1), height 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: width 0.35s cubic-bezier(0.075, 0.82, 0.165, 1), height 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+}
+
+.display.fullscreen > .display-frame {
+    background-color: transparent;
+    border: none;
+    box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -o-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -ms-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -moz-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -khtml-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -webkit-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    margin: 0px 0px 0px 0px;
+    max-width: unset;
+    padding: 0px 0px 0px 0px;
+}
+
+.display > .display-frame > .display-canvas {
+    width: 100%;
+}
diff --git a/examples/web/react/components/display/display.tsx b/examples/web/react/components/display/display.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..821bb268bcf0c1cef87ebc21cc0d09f336d41e81
--- /dev/null
+++ b/examples/web/react/components/display/display.tsx
@@ -0,0 +1,280 @@
+import React, { FC, useState, useRef, useEffect } from "react";
+import { PixelFormat } from "../../app";
+
+import "./display.css";
+
+const PIXEL_UNSET_COLOR = 0x1b1a17ff;
+
+declare const require: any;
+
+/**
+ * Function that handles a draw operation into a
+ * certain drawing context.
+ */
+export type DrawHandler = (pixels: Uint8Array, format: PixelFormat) => void;
+
+export type ClearHandler = (
+    color?: number,
+    image?: string,
+    imageScale?: number
+) => Promise<void>;
+
+type DisplayOptions = {
+    width: number;
+    height: number;
+    logicWidth: number;
+    logicHeight: number;
+    scale?: number;
+};
+
+type DisplayProps = {
+    options?: DisplayOptions;
+    size?: string;
+    fullscreen?: boolean;
+    style?: string[];
+    onDrawHandler?: (caller: DrawHandler) => void;
+    onClearHandler?: (caller: ClearHandler) => void;
+    onMinimize?: () => void;
+};
+
+type CanvasContents = {
+    canvas: HTMLCanvasElement;
+    canvasCtx: CanvasRenderingContext2D;
+    canvasBuffer: HTMLCanvasElement;
+    canvasBufferCtx: CanvasRenderingContext2D;
+    imageData: ImageData;
+    videoBuffer: DataView;
+};
+
+export const Display: FC<DisplayProps> = ({
+    options = { width: 320, height: 288, logicWidth: 160, logicHeight: 144 },
+    size = "small",
+    fullscreen = false,
+    style = [],
+    onDrawHandler,
+    onClearHandler,
+    onMinimize
+}) => {
+    options = {
+        ...options,
+        ...{ width: 320, height: 288, logicWidth: 160, logicHeight: 144 }
+    };
+    if (!options.scale) {
+        options.scale = window.devicePixelRatio ? window.devicePixelRatio : 1;
+    }
+
+    const classes = () =>
+        ["display", fullscreen ? "fullscreen" : null, size, ...style].join(" ");
+
+    const [width, setWidth] = useState<number>();
+    const [height, setHeight] = useState<number>();
+    const canvasRef = useRef<HTMLCanvasElement>(null);
+    const canvasContentsRef = useRef<CanvasContents>();
+    const resizeRef = useRef(() => {
+        const [fullWidth, fullHeight] = crop(options.width / options.height);
+        setWidth(fullWidth);
+        setHeight(fullHeight);
+    });
+
+    useEffect(() => {
+        if (canvasRef.current) {
+            canvasContentsRef.current = initCanvas(
+                options.logicWidth,
+                options.logicHeight,
+                canvasRef.current
+            );
+        }
+    }, [canvasRef, options.scale]);
+
+    useEffect(() => {
+        if (fullscreen) {
+            resizeRef.current();
+            document.getElementsByTagName("body")[0].style.overflow = "hidden";
+            window.addEventListener("resize", resizeRef.current);
+        } else {
+            setWidth(undefined);
+            setHeight(undefined);
+            document
+                .getElementsByTagName("body")[0]
+                .style.removeProperty("overflow");
+            window.removeEventListener("resize", resizeRef.current);
+        }
+        return () => {
+            window.removeEventListener("resize", resizeRef.current);
+        };
+    }, [fullscreen]);
+
+    if (onDrawHandler) {
+        onDrawHandler((pixels, format) => {
+            if (!canvasContentsRef.current) return;
+            updateCanvas(canvasContentsRef.current, pixels, format);
+        });
+    }
+
+    if (onClearHandler) {
+        onClearHandler(async (color, image, imageScale) => {
+            if (!canvasContentsRef.current) return;
+            await clearCanvas(canvasContentsRef.current, color, {
+                image: image,
+                imageScale: imageScale
+            });
+        });
+    }
+
+    return (
+        <div className={classes()}>
+            <span
+                className="magnify-button display-minimize"
+                onClick={onMinimize}
+            >
+                <img
+                    className="large"
+                    src={require("./minimise.svg")}
+                    alt="minimise"
+                />
+            </span>
+            <div
+                className="display-frame"
+                style={{ width: width ?? options.width, height: height }}
+            >
+                <canvas
+                    ref={canvasRef}
+                    className="display-canvas"
+                    width={options.width * options.scale}
+                    height={options.height * options.scale}
+                ></canvas>
+            </div>
+        </div>
+    );
+};
+
+const initCanvas = (
+    width: number,
+    height: number,
+    canvas: HTMLCanvasElement
+): CanvasContents => {
+    // initializes the off-screen canvas that is going to be
+    // used in the drawing process, this is used essentially for
+    // performance reasons as it provides a way to draw pixels
+    // in the original size instead of the target one
+    const canvasBuffer = document.createElement("canvas");
+    canvasBuffer.width = width;
+    canvasBuffer.height = height;
+    const canvasBufferCtx = canvasBuffer.getContext("2d")!;
+    const imageData = canvasBufferCtx.createImageData(
+        canvasBuffer.width,
+        canvasBuffer.height
+    );
+    const videoBuffer = new DataView(imageData.data.buffer);
+
+    // initializes the visual canvas (where data is going to be written)
+    // with, resetting the transform vector to the identity and re-calculating
+    // the scale of the drawing properly
+    const canvasCtx = canvas.getContext("2d")!;
+    canvasCtx.setTransform(1, 0, 0, 1, 0, 0);
+    canvasCtx.scale(
+        canvas.width / canvasBuffer.width,
+        canvas.height / canvasBuffer.height
+    );
+    canvasCtx.imageSmoothingEnabled = false;
+
+    return {
+        canvas: canvas,
+        canvasCtx: canvasCtx,
+        canvasBuffer: canvasBuffer,
+        canvasBufferCtx: canvasBufferCtx,
+        imageData: imageData,
+        videoBuffer: videoBuffer
+    };
+};
+
+const updateCanvas = (
+    canvasContents: CanvasContents,
+    pixels: Uint8Array,
+    format: PixelFormat = PixelFormat.RGB
+) => {
+    let offset = 0;
+    for (let index = 0; index < pixels.length; index += format) {
+        const color =
+            (pixels[index] << 24) |
+            (pixels[index + 1] << 16) |
+            (pixels[index + 2] << 8) |
+            (format == PixelFormat.RGBA ? pixels[index + 3] : 0xff);
+        canvasContents.videoBuffer.setUint32(offset, color);
+        offset += PixelFormat.RGBA;
+    }
+    canvasContents.canvasBufferCtx.putImageData(canvasContents.imageData, 0, 0);
+    canvasContents.canvasCtx.drawImage(canvasContents.canvasBuffer, 0, 0);
+};
+
+const clearCanvas = async (
+    canvasContents: CanvasContents,
+    color = PIXEL_UNSET_COLOR,
+    {
+        image = null,
+        imageScale = 1
+    }: { image?: string | null; imageScale?: number } = {}
+) => {
+    // uses the "clear" color to fill a rectangle with the complete
+    // size of the canvas contents
+    canvasContents.canvasCtx.fillStyle = `#${color.toString(16).toUpperCase()}`;
+    canvasContents.canvasCtx.fillRect(
+        0,
+        0,
+        canvasContents.canvas.width,
+        canvasContents.canvas.height
+    );
+
+    // in case an image was requested then uses that to load
+    // an image at the center of the screen properly scaled
+    if (image) {
+        await drawImageCanvas(canvasContents, image, imageScale);
+    }
+};
+
+const drawImageCanvas = async (
+    canvasContents: CanvasContents,
+    image: string,
+    imageScale = 1.0
+) => {
+    const img = await new Promise<HTMLImageElement>((resolve) => {
+        const img = new Image();
+        img.onload = () => {
+            resolve(img);
+        };
+        img.src = image;
+    });
+    const [imgWidth, imgHeight] = [
+        img.width * imageScale * window.devicePixelRatio,
+        img.height * imageScale * window.devicePixelRatio
+    ];
+    const [x0, y0] = [
+        canvasContents.canvas.width / 2 - imgWidth / 2,
+        canvasContents.canvas.height / 2 - imgHeight / 2
+    ];
+    canvasContents.canvasCtx.setTransform(1, 0, 0, 1, 0, 0);
+    try {
+        canvasContents.canvasCtx.drawImage(img, x0, y0, imgWidth, imgHeight);
+    } finally {
+        canvasContents.canvasCtx.scale(
+            canvasContents.canvas.width / canvasContents.canvasBuffer.width,
+            canvasContents.canvas.height / canvasContents.canvasBuffer.height
+        );
+    }
+};
+
+const crop = (ratio: number): [number, number] => {
+    // calculates the window ratio as this is fundamental to
+    // determine the proper way to crop the fullscreen
+    const windowRatio = window.innerWidth / window.innerHeight;
+
+    // in case the window is wider (more horizontal than the base ratio)
+    // this means that we must crop horizontally
+    if (windowRatio > ratio) {
+        return [window.innerWidth * (ratio / windowRatio), window.innerHeight];
+    } else {
+        return [window.innerWidth, window.innerHeight * (windowRatio / ratio)];
+    }
+};
+
+export default Display;
diff --git a/examples/web/react/components/display/minimise.svg b/examples/web/react/components/display/minimise.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3d41ee133bf7a84a220f8649695c5fdc0a9fdb31
--- /dev/null
+++ b/examples/web/react/components/display/minimise.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="minimiseIconTitle" stroke="#ffffff" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="minimiseIconTitle">Minimise View</title> <polyline points="8 3 8 8 3 8"/> <polyline points="21 8 16 8 16 3"/> <polyline points="3 16 8 16 8 21"/> <polyline points="16 21 16 16 21 16"/> </svg>
\ No newline at end of file
diff --git a/examples/web/react/components/index.ts b/examples/web/react/components/index.ts
index 9f2a21f34b3fc6e70aabd5dcab19b492d09a0b7d..838636b94e5c09949d1234242e97bcdccf67ca09 100644
--- a/examples/web/react/components/index.ts
+++ b/examples/web/react/components/index.ts
@@ -2,11 +2,19 @@ export * from "./button/button";
 export * from "./button-container/button-container";
 export * from "./button-increment/button-increment";
 export * from "./button-switch/button-switch";
+export * from "./canvas/canvas";
+export * from "./display/display";
 export * from "./footer/footer";
 export * from "./info/info";
+export * from "./keyboard-chip8/keyboard-chip8";
+export * from "./keyboard-gb/keyboard-gb";
 export * from "./link/link";
+export * from "./modal/modal";
+export * from "./overlay/overlay";
 export * from "./pair/pair";
 export * from "./panel-split/panel-split";
 export * from "./paragraph/paragraph";
 export * from "./section/section";
+export * from "./tiles/tiles";
 export * from "./title/title";
+export * from "./toast/toast";
diff --git a/examples/web/react/components/keyboard-chip8/keyboard-chip8.css b/examples/web/react/components/keyboard-chip8/keyboard-chip8.css
new file mode 100644
index 0000000000000000000000000000000000000000..01f35fef038dc2cc39305784fdfde7dca61efb1d
--- /dev/null
+++ b/examples/web/react/components/keyboard-chip8/keyboard-chip8.css
@@ -0,0 +1,55 @@
+
+.keyboard-chip8 {
+    font-size: 0px;
+    text-align: center;
+    touch-callout: none;
+    -o-touch-callout: none;
+    -ms-touch-callout: none;
+    -moz-touch-callout: none;
+    -khtml-touch-callout: none;
+    -webkit-touch-callout: none;
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.keyboard-chip8 > .keyboard-line {
+    margin-bottom: 12px;
+}
+
+.keyboard-chip8 > .keyboard-line:last-child {
+    margin-bottom: 0px;
+}
+
+.keyboard-chip8 .key {
+    border: 2px solid #ffffff;
+    border-radius: 5px 5px 5px 5px;
+    -o-border-radius: 5px 5px 5px 5px;
+    -ms-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    -khtml-border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    cursor: pointer;
+    display: inline-block;
+    font-size: 38px;
+    height: 48px;
+    line-height: 46px;
+    margin-right: 14px;
+    text-align: center;
+    width: 48px;
+}
+
+.keyboard-chip8 .key:last-child {
+    margin-right: 0px;
+}
+
+.keyboard-chip8 .key:hover {
+    background-color: #50cb93;
+}
+
+.keyboard-chip8 .key:active {
+    background-color: #2a9d8f;
+}
diff --git a/examples/web/react/components/keyboard-chip8/keyboard-chip8.tsx b/examples/web/react/components/keyboard-chip8/keyboard-chip8.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..897cc353ee2d3b9bb20ce935a40c507a4ab68a84
--- /dev/null
+++ b/examples/web/react/components/keyboard-chip8/keyboard-chip8.tsx
@@ -0,0 +1,44 @@
+import React, { FC } from "react";
+
+import "./keyboard-chip8.css";
+
+type KeyboardChip8Props = {
+    style?: string[];
+    onKeyDown?: (key: string) => void;
+};
+
+export const KeyboardChip8: FC<KeyboardChip8Props> = ({
+    style = [],
+    onKeyDown
+}) => {
+    const classes = () => ["keyboard", "keyboard-chip8", ...style].join(" ");
+    const renderKey = (key: string) => {
+        return (
+            <span
+                className="key"
+                key={key}
+                onKeyDown={() => onKeyDown && onKeyDown(key)}
+            >
+                {key}
+            </span>
+        );
+    };
+    return (
+        <div className={classes()}>
+            <div className="keyboard-line">
+                {["1", "2", "3", "4"].map((k) => renderKey(k))}
+            </div>
+            <div className="keyboard-line">
+                {["Q", "W", "E", "R"].map((k) => renderKey(k))}
+            </div>
+            <div className="keyboard-line">
+                {["A", "S", "D", "F"].map((k) => renderKey(k))}
+            </div>
+            <div className="keyboard-line">
+                {["Z", "X", "C", "V"].map((k) => renderKey(k))}
+            </div>
+        </div>
+    );
+};
+
+export default KeyboardChip8;
diff --git a/examples/web/react/components/keyboard-gb/dpad.svg b/examples/web/react/components/keyboard-gb/dpad.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8115b91717cc9d018b30805c4836162b78d9670d
--- /dev/null
+++ b/examples/web/react/components/keyboard-gb/dpad.svg
@@ -0,0 +1 @@
+<svg width="700pt" height="700pt" version="1.1" viewBox="0 0 700 700" fill="#ffffff" stroke="#ffffff"  xmlns="http://www.w3.org/2000/svg"><path d="m338.45 240.62c3.1953 2.8047 7.3008 4.3516 11.551 4.3516s8.3555-1.5469 11.551-4.3516l78.75-70c3.7773-3.3164 5.9414-8.0977 5.9492-13.125v-105c0-4.6406-1.8438-9.0938-5.125-12.375s-7.7344-5.125-12.375-5.125h-157.5c-4.6406 0-9.0938 1.8438-12.375 5.125s-5.125 7.7344-5.125 12.375v105c0.007812 5.0273 2.1719 9.8086 5.9492 13.125zm-49.699-170.62h122.5v79.625l-61.25 54.426-61.25-54.426zm72.801 249.38c-3.1953-2.8047-7.3008-4.3516-11.551-4.3516s-8.3555 1.5469-11.551 4.3516l-78.75 70c-3.7773 3.3164-5.9414 8.0977-5.9492 13.125v105c0 4.6406 1.8438 9.0938 5.125 12.375s7.7344 5.125 12.375 5.125h157.5c4.6406 0 9.0938-1.8438 12.375-5.125s5.125-7.7344 5.125-12.375v-105c-0.007812-5.0273-2.1719-9.8086-5.9492-13.125zm49.699 170.62h-122.5v-79.625l61.25-54.426 61.25 54.426zm-100.62-221.55-70-78.75c-3.3164-3.7773-8.0977-5.9414-13.125-5.9492h-105c-4.6406 0-9.0938 1.8438-12.375 5.125s-5.125 7.7344-5.125 12.375v157.5c0 4.6406 1.8438 9.0938 5.125 12.375s7.7344 5.125 12.375 5.125h105c5.0273-0.007812 9.8086-2.1719 13.125-5.9492l70-78.75c2.8047-3.1953 4.3516-7.3008 4.3516-11.551s-1.5469-8.3555-4.3516-11.551zm-91 72.801h-79.625v-122.5h79.625l54.426 61.25zm357.88-157.5h-105c-5.0273 0.007812-9.8086 2.1719-13.125 5.9492l-70 78.75c-2.8047 3.1953-4.3516 7.3008-4.3516 11.551s1.5469 8.3555 4.3516 11.551l70 78.75c3.3164 3.7773 8.0977 5.9414 13.125 5.9492h105c4.6406 0 9.0938-1.8438 12.375-5.125s5.125-7.7344 5.125-12.375v-157.5c0-4.6406-1.8438-9.0938-5.125-12.375s-7.7344-5.125-12.375-5.125zm-17.5 157.5h-79.625l-54.426-61.25 54.426-61.25h79.625z"/></svg>
diff --git a/examples/web/react/components/keyboard-gb/keyboard-gb.css b/examples/web/react/components/keyboard-gb/keyboard-gb.css
new file mode 100644
index 0000000000000000000000000000000000000000..56f4b8dacbe21740773829853af511b26e7eb671
--- /dev/null
+++ b/examples/web/react/components/keyboard-gb/keyboard-gb.css
@@ -0,0 +1,59 @@
+
+.keyboard-gb {
+    font-size: 0px;
+    text-align: center;
+    touch-callout: none;
+    -o-touch-callout: none;
+    -ms-touch-callout: none;
+    -moz-touch-callout: none;
+    -khtml-touch-callout: none;
+    -webkit-touch-callout: none;
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.keyboard-gb > .keyboard-line {
+    margin-bottom: 12px;
+}
+
+.keyboard-gb > .keyboard-line:last-child {
+    margin-bottom: 0px;
+}
+
+.keyboard-gb .key {
+    border: 2px solid #ffffff;
+    border-radius: 5px 5px 5px 5px;
+    -o-border-radius: 5px 5px 5px 5px;
+    -ms-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    -khtml-border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    cursor: pointer;
+    display: inline-block;
+    font-size: 38px;
+    height: 48px;
+    line-height: 46px;
+    margin-right: 14px;
+    text-align: center;
+    width: 48px;
+}
+
+.keyboard-gb .key:last-child {
+    margin-right: 0px;
+}
+
+.keyboard-gb .key:hover {
+    background-color: #50cb93;
+}
+
+.keyboard-gb .key:active {
+    background-color: #2a9d8f;
+}
+
+.keyboard-gb .dpad {
+    width: 120px;
+}
diff --git a/examples/web/react/components/keyboard-gb/keyboard-gb.tsx b/examples/web/react/components/keyboard-gb/keyboard-gb.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6358673061284ede95093ec10915277fa9c0681b
--- /dev/null
+++ b/examples/web/react/components/keyboard-gb/keyboard-gb.tsx
@@ -0,0 +1,43 @@
+import React, { FC } from "react";
+
+import "./keyboard-gb.css";
+
+declare const require: any;
+
+type KeyboardGBProps = {
+    style?: string[];
+    onKeyDown?: (key: string) => void;
+};
+
+export const KeyboardGB: FC<KeyboardGBProps> = ({ style = [], onKeyDown }) => {
+    const classes = () => ["keyboard", "keyboard-gb", ...style].join(" ");
+    const renderKey = (key: string) => {
+        return (
+            <span
+                className="key"
+                key={key}
+                onKeyDown={() => onKeyDown && onKeyDown(key)}
+            >
+                {key}
+            </span>
+        );
+    };
+    return (
+        <div className={classes()}>
+            <div className="keyboard-line">
+                <img className="dpad" alt="dpad" src={require("./dpad.svg")} />
+            </div>
+            <div className="keyboard-line">
+                {["Q", "W", "E", "R"].map((k) => renderKey(k))}
+            </div>
+            <div className="keyboard-line">
+                {["A", "S", "D", "F"].map((k) => renderKey(k))}
+            </div>
+            <div className="keyboard-line">
+                {["Z", "X", "C", "V"].map((k) => renderKey(k))}
+            </div>
+        </div>
+    );
+};
+
+export default KeyboardGB;
diff --git a/examples/web/react/components/modal/close.svg b/examples/web/react/components/modal/close.svg
new file mode 100644
index 0000000000000000000000000000000000000000..aeac9982d889f0194b3b55905d166541704b5bac
--- /dev/null
+++ b/examples/web/react/components/modal/close.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="closeIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="closeIconTitle">Close</title> <path d="M6.34314575 6.34314575L17.6568542 17.6568542M6.34314575 17.6568542L17.6568542 6.34314575"/> </svg>
\ No newline at end of file
diff --git a/examples/web/react/components/modal/modal.css b/examples/web/react/components/modal/modal.css
new file mode 100644
index 0000000000000000000000000000000000000000..bcc2c27ca6885de37f72e0dcbeed5d466fae82ba
--- /dev/null
+++ b/examples/web/react/components/modal/modal.css
@@ -0,0 +1,132 @@
+.modal {
+    align-items: center;
+    background-color: rgba(20, 20, 20, 0.95);
+    display: flex;
+    height: 100%;
+    justify-content: center;
+    left: 0px;
+    opacity: 0;
+    -o-opacity: 0;
+    -ms-opacity: 0;
+    -moz-opacity: 0;
+    -khtml-opacity: 0;
+    -webkit-opacity: 0;
+    padding: 0px 12px 0px 12px;
+    pointer-events: none;
+    position: fixed;
+    text-align: center;
+    top: 0px;
+    transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: opacity 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: 100%;
+    z-index: 10;
+}
+
+.modal.visible {
+    opacity: 1.0;
+    -o-opacity: 1.0;
+    -ms-opacity: 1.0;
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    -webkit-opacity: 1.0;
+    pointer-events: initial;
+    transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+}
+
+.modal > .modal-window {
+    background-color: #264653;
+    border-radius: 6px 6px 6px 6px;
+    -o-border-radius: 6px 6px 6px 6px;
+    -ms-border-radius: 6px 6px 6px 6px;
+    -moz-border-radius: 6px 6px 6px 6px;
+    -khtml-border-radius: 6px 6px 6px 6px;
+    -webkit-border-radius: 6px 6px 6px 6px;
+    box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
+    -o-box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
+    -ms-box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
+    -moz-box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
+    -khtml-box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
+    -webkit-box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
+    margin-top: 30px;
+    max-width: 100%;
+    padding: 24px 24px 24px 24px;
+    text-align: left;
+    transform: scale(0.96);
+    -o-transform: scale(0.96);
+    -ms-transform: scale(0.96);
+    -moz-transform: scale(0.96);
+    -khtml-transform: scale(0.96);
+    -webkit-transform: scale(0.96);
+    transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: 480px;
+}
+
+.modal.visible > .modal-window {
+    margin-top: 0px;
+    pointer-events: all;
+    transform: scale(1);
+    -o-transform: scale(1);
+    -ms-transform: scale(1);
+    -moz-transform: scale(1);
+    -khtml-transform: scale(1);
+    -webkit-transform: scale(1);
+    transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), margin-top 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+}
+
+.modal > .modal-window > .modal-top-buttons {
+    float: right;
+    margin-right: -10px;
+    margin-top: -10px;
+}
+
+.modal > .modal-window > .modal-title {
+    font-size: 32px;
+    margin-top: 0px;
+    text-align: left;
+}
+
+.modal > .modal-window > .modal-text {
+    font-size: 20px;
+    line-height: 22px;
+}
+
+.modal > .modal-window > .modal-buttons {
+    font-size: 22px;
+    margin-top: 24px;
+    text-align: center;
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.modal > .modal-window > .modal-buttons > .button.simple {
+    display: inline-block;
+    margin-right: 12px;
+    min-width: 120px;
+}
+
+.modal > .modal-window > .modal-buttons > .button.simple:last-child {
+    margin-right: 0px;
+}
diff --git a/examples/web/react/components/modal/modal.tsx b/examples/web/react/components/modal/modal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c72f676188cba2db6d04e100e553d47e4b4fbbe5
--- /dev/null
+++ b/examples/web/react/components/modal/modal.tsx
@@ -0,0 +1,86 @@
+import React, { FC, useEffect } from "react";
+import Button from "../button/button";
+
+import "./modal.css";
+
+declare const require: any;
+
+type ModalProps = {
+    title?: string;
+    text?: string;
+    visible?: boolean;
+    overlayClose?: boolean;
+    style?: string[];
+    onConfirm?: () => void;
+    onCancel?: () => void;
+};
+
+export const Modal: FC<ModalProps> = ({
+    title = "Alert",
+    text = "Do you confirm the following operation?",
+    visible = false,
+    overlayClose = true,
+    style = [],
+    onConfirm,
+    onCancel
+}) => {
+    const classes = () =>
+        ["modal", visible ? "visible" : "", ...style].join(" ");
+    useEffect(() => {
+        const onKeyDown = (event: KeyboardEvent) => {
+            if (event.key === "Escape") {
+                onCancel && onCancel();
+            }
+        };
+        document.addEventListener("keydown", onKeyDown);
+        return () => {
+            document.removeEventListener("keydown", onKeyDown);
+        };
+    }, []);
+    const getTextHtml = (separator = /\n/g) => ({
+        __html: text.replace(separator, "<br/>")
+    });
+    const onWindowClick = (
+        event: React.MouseEvent<HTMLDivElement, MouseEvent>
+    ) => {
+        if (!overlayClose) return;
+        event.stopPropagation();
+    };
+    return (
+        <div className={classes()} onClick={onCancel}>
+            <div className="modal-window" onClick={onWindowClick}>
+                <div className="modal-top-buttons">
+                    <Button
+                        text={""}
+                        size={"medium"}
+                        style={["simple", "rounded", "no-text"]}
+                        image={require("./close.svg")}
+                        imageAlt="close"
+                        onClick={onCancel}
+                    />
+                </div>
+                <h2 className="modal-title">{title}</h2>
+                <p
+                    className="modal-text"
+                    dangerouslySetInnerHTML={getTextHtml()}
+                ></p>
+                <div className="modal-buttons">
+                    <Button
+                        text={"Cancel"}
+                        size={"medium"}
+                        style={["simple", "red", "border", "padded-large"]}
+                        onClick={onCancel}
+                    />
+                    <Button
+                        text={"Confirm"}
+                        size={"medium"}
+                        style={["simple", "border", "padded-large"]}
+                        onClick={onConfirm}
+                    />
+                </div>
+            </div>
+        </div>
+    );
+};
+
+export default Modal;
diff --git a/examples/web/react/components/overlay/overlay.css b/examples/web/react/components/overlay/overlay.css
new file mode 100644
index 0000000000000000000000000000000000000000..14ad1c9bda6f278b0b3980f4c6b7ed4ad95acf9f
--- /dev/null
+++ b/examples/web/react/components/overlay/overlay.css
@@ -0,0 +1,44 @@
+.overlay {
+    align-items: center;
+    background-color: rgba(80, 203, 147, 0.95);
+    display: flex;
+    font-size: 48px;
+    height: 100%;
+    justify-content: center;
+    left: 0px;
+    opacity: 0.0;
+    -o-opacity: 0.0;
+    -ms-opacity: 0.0;
+    -moz-opacity: 0.0;
+    -khtml-opacity: 0.0;
+    -webkit-opacity: 0.0;
+    pointer-events: none;
+    position: fixed;
+    text-align: center;
+    top: 0px;
+    transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: opacity 0.4s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: 100%;
+    z-index: 10;
+}
+
+.overlay.visible {
+    opacity: 1.0;
+    -o-opacity: 1.0;
+    -ms-opacity: 1.0;
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    -webkit-opacity: 1.0;
+}
+
+.overlay .overlay-image {
+    margin-top: 16px;
+}
+
+.overlay .overlay-image > img {
+    width: 64px;
+}
diff --git a/examples/web/react/components/overlay/overlay.tsx b/examples/web/react/components/overlay/overlay.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1762dcf2d15d33821e2f03cc0bb123eba41f52b0
--- /dev/null
+++ b/examples/web/react/components/overlay/overlay.tsx
@@ -0,0 +1,69 @@
+import React, { FC, useEffect, useState } from "react";
+
+import "./overlay.css";
+
+declare const require: any;
+
+type OverlayProps = {
+    text?: string;
+    style?: string[];
+    onFile?: (file: File) => void;
+};
+
+export const Overlay: FC<OverlayProps> = ({ text, style = [], onFile }) => {
+    const [visible, setVisible] = useState(false);
+    const classes = () =>
+        ["overlay", visible ? "visible" : "", ...style].join(" ");
+    useEffect(() => {
+        const onDrop = async (event: DragEvent) => {
+            if (!event.dataTransfer!.items) return;
+            if (event.dataTransfer!.items[0].type) return;
+
+            setVisible(false);
+
+            const file = event.dataTransfer!.files[0];
+            onFile && onFile(file);
+
+            event.preventDefault();
+            event.stopPropagation();
+        };
+        const onDragOver = async (event: DragEvent) => {
+            if (!event.dataTransfer!.items) return;
+            if (event.dataTransfer!.items[0].type) return;
+            setVisible(true);
+            event.preventDefault();
+        };
+        const onDragEnter = async (event: DragEvent) => {
+            if (!event.dataTransfer!.items) return;
+            if (event.dataTransfer!.items[0].type) return;
+            setVisible(true);
+        };
+        const onDragLeave = async (event: DragEvent) => {
+            if (!event.dataTransfer!.items) return;
+            if (event.dataTransfer!.items[0].type) return;
+            setVisible(false);
+        };
+        document.addEventListener("drop", onDrop);
+        document.addEventListener("dragover", onDragOver);
+        document.addEventListener("dragenter", onDragEnter);
+        document.addEventListener("dragleave", onDragLeave);
+        return () => {
+            document.removeEventListener("drop", onDrop);
+            document.removeEventListener("dragover", onDragOver);
+            document.removeEventListener("dragenter", onDragEnter);
+            document.removeEventListener("dragleave", onDragLeave);
+        };
+    }, []);
+    return (
+        <div className={classes()}>
+            <div className="overlay-container">
+                {text && <div className="overlay-text">{text}</div>}
+                <div className="overlay-image">
+                    <img alt="sunglasses" src={require("./sunglasses.png")} />
+                </div>
+            </div>
+        </div>
+    );
+};
+
+export default Overlay;
diff --git a/examples/web/react/components/overlay/sunglasses.png b/examples/web/react/components/overlay/sunglasses.png
new file mode 100644
index 0000000000000000000000000000000000000000..98b0bba649db0699a170016e18bc4b7a9ce3b70c
Binary files /dev/null and b/examples/web/react/components/overlay/sunglasses.png differ
diff --git a/examples/web/react/components/tiles/tiles.css b/examples/web/react/components/tiles/tiles.css
new file mode 100644
index 0000000000000000000000000000000000000000..59b7d18240ad1bc4ea361b700e70c75c911782de
--- /dev/null
+++ b/examples/web/react/components/tiles/tiles.css
@@ -0,0 +1,11 @@
+.tiles > .canvas {
+    background-color: #1b1a17;
+    border: 2px solid #50cb93;
+    box-sizing: content-box;
+    -o-box-sizing: content-box;
+    -ms-box-sizing: content-box;
+    -moz-box-sizing: content-box;
+    -khtml-box-sizing: content-box;
+    -webkit-box-sizing: content-box;
+    padding: 8px 8px 8px 8px;
+}
diff --git a/examples/web/react/components/tiles/tiles.tsx b/examples/web/react/components/tiles/tiles.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8ecc57d3b5442758914256df11f0074b0740aaee
--- /dev/null
+++ b/examples/web/react/components/tiles/tiles.tsx
@@ -0,0 +1,79 @@
+import React, { FC } from "react";
+import { PixelFormat } from "../../app";
+import Canvas, { CanvasStructure } from "../canvas/canvas";
+
+import "./tiles.css";
+
+type TilesProps = {
+    tileCount: number;
+    getTile: (index: number) => Uint8Array;
+    interval?: number;
+    style?: string[];
+};
+
+export const Tiles: FC<TilesProps> = ({
+    tileCount,
+    getTile,
+    interval = 500,
+    style = []
+}) => {
+    const classes = () => ["tiles", ...style].join(" ");
+    const onCanvas = (structure: CanvasStructure) => {
+        const drawTiles = () => {
+            for (let index = 0; index < 384; index++) {
+                const pixels = getTile(index);
+                drawTile(index, pixels, structure);
+            }
+        };
+        drawTiles();
+        setInterval(() => drawTiles(), interval);
+    };
+    return (
+        <div className={classes()}>
+            <Canvas width={128} height={192} scale={2} onCanvas={onCanvas} />
+        </div>
+    );
+};
+
+/**
+ * Draws the tile at the given index to the proper vertical
+ * offset in the given context and buffer.
+ *
+ * @param index The index of the sprite to be drawn.
+ * @param pixels Buffer of pixels that contains the RGB data
+ * that is going to be drawn.
+ * @param structure The canvas context to which the tile is
+ * growing to be drawn.
+ * @param format The pixel format of the sprite.
+ */
+const drawTile = (
+    index: number,
+    pixels: Uint8Array,
+    structure: CanvasStructure,
+    format: PixelFormat = PixelFormat.RGB
+) => {
+    const line = Math.floor(index / 16);
+    const column = index % 16;
+    let offset =
+        (line * structure.canvas.width * 8 + column * 8) * PixelFormat.RGBA;
+    let counter = 0;
+    for (let i = 0; i < pixels.length; i += format) {
+        const color =
+            (pixels[i] << 24) |
+            (pixels[i + 1] << 16) |
+            (pixels[i + 2] << 8) |
+            (format === PixelFormat.RGBA ? pixels[i + 3] : 0xff);
+        structure.canvasBuffer.setUint32(offset, color);
+
+        counter++;
+        if (counter === 8) {
+            counter = 0;
+            offset += (structure.canvas.width - 7) * PixelFormat.RGBA;
+        } else {
+            offset += PixelFormat.RGBA;
+        }
+    }
+    structure.canvasContext.putImageData(structure.canvasImage, 0, 0);
+};
+
+export default Tiles;
diff --git a/examples/web/react/components/title/title.css b/examples/web/react/components/title/title.css
index b10f9b68cd6195975c92f801645d7c0e937e8448..7989a40d87bde43cd73a7ae846e67e6a2743d4f6 100644
--- a/examples/web/react/components/title/title.css
+++ b/examples/web/react/components/title/title.css
@@ -1,4 +1,5 @@
-.title > .link {
+.title > .link,
+.title > .label {
     margin-left: 14px;
 }
 
diff --git a/examples/web/react/components/title/title.tsx b/examples/web/react/components/title/title.tsx
index c390617521d3a422055466254620d13ef61373bd..ac312ce77c2e03291d7602046d4b858d76d3067d 100644
--- a/examples/web/react/components/title/title.tsx
+++ b/examples/web/react/components/title/title.tsx
@@ -22,11 +22,14 @@ export const Title: FC<TitleProps> = ({
     return (
         <h1 className={classes()}>
             {text}
-            {version && (
-                <Link href={versionUrl} target="_blank">
-                    {version}
-                </Link>
-            )}
+            {version &&
+                (versionUrl ? (
+                    <Link href={versionUrl} target="_blank">
+                        {version}
+                    </Link>
+                ) : (
+                    <span className="label">{version}</span>
+                ))}
             {iconSrc && <img className="icon" src={iconSrc} alt="icon" />}
         </h1>
     );
diff --git a/examples/web/react/components/toast/toast.css b/examples/web/react/components/toast/toast.css
new file mode 100644
index 0000000000000000000000000000000000000000..8bf5233215e4af7f340b94e48b55107dada71e4d
--- /dev/null
+++ b/examples/web/react/components/toast/toast.css
@@ -0,0 +1,57 @@
+.toast {
+    background-color: black;
+    height: 0px;
+    left: 0px;
+    padding: 0px 24px 0px 24px;
+    pointer-events: none;
+    position: fixed;
+    text-align: center;
+    top: 0px;
+    width: 100%;
+    z-index: 8;
+}
+
+.toast > .toast-text {
+    background-color: #2a9d8f;
+    border-radius: 4px 4px 4px 4px;
+    -o-border-radius: 4px 4px 4px 4px;
+    -ms-border-radius: 4px 4px 4px 4px;
+    -moz-border-radius: 4px 4px 4px 4px;
+    -khtml-border-radius: 4px 4px 4px 4px;
+    -webkit-border-radius: 4px 4px 4px 4px;
+    cursor: pointer;
+    display: inline-block;
+    font-size: 20px;
+    line-height: 22px;
+    opacity: 0.0;
+    -o-opacity: 0.0;
+    -ms-opacity: 0.0;
+    -moz-opacity: 0.0;
+    -khtml-opacity: 0.0;
+    -webkit-opacity: 0.0;
+    padding: 12px 18px 12px 18px;
+    position: relative;
+    top: -46px;
+    transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: fit-content;
+}
+
+.toast.error > .toast-text {
+    background-color: #e63946;
+}
+
+.toast.visible > .toast-text {
+    opacity: 1.0;
+    -o-opacity: 1.0;
+    -ms-opacity: 1.0;
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    -webkit-opacity: 1.0;
+    pointer-events: all;
+    top: 24px;
+}
diff --git a/examples/web/react/components/toast/toast.tsx b/examples/web/react/components/toast/toast.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3c1f3831e86e5406420493c8d5a9afdc1b2f51a1
--- /dev/null
+++ b/examples/web/react/components/toast/toast.tsx
@@ -0,0 +1,36 @@
+import React, { FC } from "react";
+
+import "./toast.css";
+
+type ToastProps = {
+    text?: string;
+    error?: boolean;
+    visible?: boolean;
+    style?: string[];
+    onCancel?: () => void;
+};
+
+export const Toast: FC<ToastProps> = ({
+    text = "",
+    error = false,
+    visible = false,
+    style = [],
+    onCancel
+}) => {
+    const classes = () =>
+        [
+            "toast",
+            error ? "error" : "",
+            visible ? "visible" : "",
+            ...style
+        ].join(" ");
+    return (
+        <div className={classes()}>
+            <div className="toast-text" onClick={onCancel}>
+                {text}
+            </div>
+        </div>
+    );
+};
+
+export default Toast;
diff --git a/examples/web/static/_headers b/examples/web/static/_headers
new file mode 100644
index 0000000000000000000000000000000000000000..7d54b8fcaf282650627bbf184ad85c9c36633d01
--- /dev/null
+++ b/examples/web/static/_headers
@@ -0,0 +1,3 @@
+
+/*
+  X-Robots-Tag: all
diff --git a/examples/web/static/robots.txt b/examples/web/static/robots.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a78466b3d66c7386c0ccc2293a67bccf4b6c85ff
--- /dev/null
+++ b/examples/web/static/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Allow: /
diff --git a/examples/web/tsconfig.json b/examples/web/tsconfig.json
index 8f83dd8e31c5ab0b458c75bafaedad1b74ca9698..d38b2b510692a48dc85856135ce5d04fd93c0faa 100644
--- a/examples/web/tsconfig.json
+++ b/examples/web/tsconfig.json
@@ -7,6 +7,7 @@
         "target": "es6",
         "noImplicitAny": true,
         "noImplicitThis": true,
+        "noUnusedLocals": true,
         "alwaysStrict": true,
         "strictBindCallApply": true,
         "strictNullChecks": true,
diff --git a/src/cpu.rs b/src/cpu.rs
index b6b0a32fa314d83aa03b78afa852973366b13f52..e253dba68cb622d24f5743b7e8646bdd63589b4e 100644
--- a/src/cpu.rs
+++ b/src/cpu.rs
@@ -112,7 +112,7 @@ impl Cpu {
         }
 
         // @todo this is so bad, need to improve this by an order
-        // of magnitude
+        // of magnitude, to be able to have better performance
         if self.halted {
             if ((self.mmu.ie & 0x01 == 0x01) && self.mmu.ppu().int_vblank())
                 || ((self.mmu.ie & 0x02 == 0x02) && self.mmu.ppu().int_stat())
@@ -124,7 +124,8 @@ impl Cpu {
         }
 
         if self.ime {
-            // @todo aggregate all of this interrupts in the MMU
+            // @todo aggregate all of this interrupts in the MMU, as there's
+            // a lot of redundant code involved in here
             if (self.mmu.ie & 0x01 == 0x01) && self.mmu.ppu().int_vblank() {
                 debugln!("Going to run V-Blank interrupt handler (0x40)");
 
diff --git a/src/gb.rs b/src/gb.rs
index 2750414133440ad4878dbccefafdda3a04301b6b..3a72d3ff9efea0b3c76c963e58912eb729fffde7 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -113,10 +113,16 @@ impl GameBoy {
         self.frame_buffer().to_vec()
     }
 
+    /// Obtains the tile structure for the tile at the
+    /// given index, no conversion in the pixel buffer
+    /// is done so that the color reference is the GB one.
     pub fn get_tile(&mut self, index: usize) -> Tile {
         self.ppu().tiles()[index]
     }
 
+    /// Obtains the pixel buffer for the tile at the
+    /// provided index, converting the color buffer
+    /// using the currently loaded palette.
     pub fn get_tile_buffer(&mut self, index: usize) -> Vec<u8> {
         let tile = self.get_tile(index);
         tile.palette_buffer(self.ppu().palette())
diff --git a/src/inst.rs b/src/inst.rs
index a7692a4bb92b6068ca712cc9bd64f9779af1783d..497523a1efe4a4222dbad1af862a21d3cf02025a 100644
--- a/src/inst.rs
+++ b/src/inst.rs
@@ -3129,6 +3129,7 @@ fn set_7_mhl(cpu: &mut Cpu) {
 fn set_7_a(cpu: &mut Cpu) {
     cpu.a = set(cpu.a, 7);
 }
+
 /// Helper function to set one bit in a u8.
 fn set(value: u8, bit: u8) -> u8 {
     value | (1u8 << (bit as usize))