diff --git a/frontends/web/index.ts b/frontends/web/index.ts
index 2bd2deabc2aa1a314df3ea0eddf83a9fad59bb04..bf4afbd1c5c7592b58c5b014907d69cabae2a583 100644
--- a/frontends/web/index.ts
+++ b/frontends/web/index.ts
@@ -50,9 +50,12 @@ const BACKGROUNDS = [
         background: background,
         backgrounds: BACKGROUNDS
     });
-    await emulator.main({ romUrl: romUrl });
 
     // sets the emulator in the global scope this is useful
     // to be able to access the emulator from global functions
     window.emulator = emulator;
+
+    // starts the emulator with the provided ROM URL, this is
+    // going to run the main emulator (infinite) loop
+    await emulator.main({ romUrl: romUrl });
 })();
diff --git a/frontends/web/react/components/index.ts b/frontends/web/react/components/index.ts
index be8be017e4b68ee2c5ce88645424dcdb785770fb..6ec9010a9a7d8099e4c715093e38cbf130dfcf2b 100644
--- a/frontends/web/react/components/index.ts
+++ b/frontends/web/react/components/index.ts
@@ -2,4 +2,5 @@ export * from "./audio-gb/audio-gb";
 export * from "./debug/debug";
 export * from "./help/help";
 export * from "./registers-gb/registers-gb";
+export * from "./serial-section/serial-section";
 export * from "./tiles-gb/tiles-gb";
diff --git a/frontends/web/react/components/serial-section/serial-section.css b/frontends/web/react/components/serial-section/serial-section.css
new file mode 100644
index 0000000000000000000000000000000000000000..ba1a95ad35c4da654c3324919935439d4c38e1ce
--- /dev/null
+++ b/frontends/web/react/components/serial-section/serial-section.css
@@ -0,0 +1,11 @@
+.serial-section .printer > .printer-lines {
+    font-size: 0px;
+}
+
+.serial-section .printer > .printer-lines > .printer-line {
+    display: block;
+}
+
+.serial-section .printer > .printer-lines > .placeholder {
+    font-size: initial;
+}
diff --git a/frontends/web/react/components/serial-section/serial-section.tsx b/frontends/web/react/components/serial-section/serial-section.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..be0018447965bccb8ffe44babb077381c060b98b
--- /dev/null
+++ b/frontends/web/react/components/serial-section/serial-section.tsx
@@ -0,0 +1,137 @@
+import React, { FC, useEffect, useRef, useState } from "react";
+import { ButtonSwitch, Info, Pair, PanelTab } from "emukit";
+import { GameboyEmulator, bufferToDataUrl } from "../../../ts";
+
+import "./serial-section.css";
+
+const DEVICE_ICON: { [key: string]: string } = {
+    Null: "🛑",
+    Logger: "📜",
+    Printer: "🖨️"
+};
+
+export type LoggerCallback = (data: Uint8Array) => void;
+export type PrinterCallback = (imageBuffer: Uint8Array) => void;
+
+type SerialSectionProps = {
+    emulator: GameboyEmulator;
+    style?: string[];
+    onLogger?: (onLoggerData: LoggerCallback) => void;
+    onPrinter?: (onPrinterData: PrinterCallback) => void;
+};
+
+export const SerialSection: FC<SerialSectionProps> = ({
+    emulator,
+    style = [],
+    onLogger,
+    onPrinter
+}) => {
+    const classes = () => ["serial-section", ...style].join(" ");
+    const [loggerData, setLoggerData] = useState<string>();
+    const [printerImageUrls, setPrinterImageUrls] = useState<string[]>();
+    const loggerDataRef = useRef<string[]>([]);
+    const printerDataRef = useRef<string[]>([]);
+    const loggerRef = useRef<HTMLDivElement>(null);
+    const imagesRef = useRef<HTMLDivElement>(null);
+
+    const onLoggerData = (data: Uint8Array) => {
+        const byte = data[0];
+        const charByte = String.fromCharCode(byte);
+        loggerDataRef.current.push(charByte);
+        setLoggerData(loggerDataRef.current.join(""));
+    };
+    const onPrinterData = (imageBuffer: Uint8Array) => {
+        const imageUrl = bufferToDataUrl(imageBuffer, 160);
+        printerDataRef.current.unshift(imageUrl);
+        setPrinterImageUrls([...printerDataRef.current]);
+    };
+
+    useEffect(() => {
+        if (loggerRef.current) {
+            onLogger && onLogger(onLoggerData);
+        }
+    }, [loggerRef, loggerRef.current]);
+
+    useEffect(() => {
+        if (imagesRef.current) {
+            onPrinter && onPrinter(onPrinterData);
+        }
+    }, [imagesRef, imagesRef.current]);
+
+    const onEngineChange = (option: string) => {
+        switch (option) {
+            case "Null":
+                emulator.loadNullDevice();
+                break;
+
+            case "Logger":
+                emulator.loadLoggerDevice();
+                break;
+
+            case "Printer":
+                emulator.loadPrinterDevice();
+                break;
+        }
+
+        const optionIcon = DEVICE_ICON[option] ?? "";
+        emulator.handlers.showToast?.(
+            `${optionIcon} ${option} attached to the serial port & active`
+        );
+    };
+
+    const getTabs = () => {
+        return [
+            <Info>
+                <Pair
+                    key="button-device"
+                    name={"Device"}
+                    valueNode={
+                        <ButtonSwitch
+                            options={["Null", "Logger", "Printer"]}
+                            size={"large"}
+                            style={["simple"]}
+                            onChange={onEngineChange}
+                        />
+                    }
+                />
+                <Pair key="baud-rate" name={"Baud Rate"} value={"1 KB/s"} />
+            </Info>,
+            <div className="logger" ref={loggerRef}>
+                <div className="logger-data">
+                    {loggerData || "Logger contents are empty."}
+                </div>
+            </div>,
+            <div className="printer" ref={imagesRef}>
+                <div className="printer-lines">
+                    {printerImageUrls ? (
+                        printerImageUrls.map((url, index) => (
+                            <img
+                                key={index}
+                                className="printer-line"
+                                src={url}
+                            />
+                        ))
+                    ) : (
+                        <span className="placeholder">
+                            Printer contents are empty.
+                        </span>
+                    )}
+                </div>
+            </div>
+        ];
+    };
+    const getTabNames = () => {
+        return ["Settings", "Logger", "Printer"];
+    };
+    return (
+        <div className={classes()}>
+            <PanelTab
+                tabs={getTabs()}
+                tabNames={getTabNames()}
+                selectors={true}
+            />
+        </div>
+    );
+};
+
+export default SerialSection;
diff --git a/frontends/web/res/serial.svg b/frontends/web/res/serial.svg
new file mode 100644
index 0000000000000000000000000000000000000000..578d6cb83ce6ee6cb657df805dfbb43e22df1f01
--- /dev/null
+++ b/frontends/web/res/serial.svg
@@ -0,0 +1 @@
+<svg width="48px" height="48px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-labelledby="swapHorizontalIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="swapHorizontalIconTitle">Swap items (horizontally)</title> <path d="M16 4L19 7L16 10"/> <path d="M4 7L18 7"/> <path d="M7 20L4 17L7 14"/> <path d="M19 17L5 17"/> </svg>
\ No newline at end of file
diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts
index 7023ecabbb95b34769d4f03824f50e6455d9a07b..c861d82554f6c9beb91332324ea5db5334eb766b 100644
--- a/frontends/web/ts/gb.ts
+++ b/frontends/web/ts/gb.ts
@@ -1,6 +1,7 @@
 import {
     AudioSpecs,
     BenchmarkResult,
+    SectionInfo,
     Compilation,
     Compiler,
     DebugPanel,
@@ -16,8 +17,16 @@ import {
     Size
 } from "emukit";
 import { PALETTES, PALETTES_MAP } from "./palettes";
-import { base64ToBuffer, bufferToBase64, bufferToDataUrl } from "./util";
-import { DebugAudio, DebugVideo, HelpFaqs, HelpKeyboard } from "../react";
+import { base64ToBuffer, bufferToBase64 } from "./util";
+import {
+    DebugAudio,
+    DebugVideo,
+    HelpFaqs,
+    HelpKeyboard,
+    LoggerCallback,
+    PrinterCallback,
+    SerialSection,
+} from "../react";
 
 import {
     Cartridge,
@@ -124,6 +133,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
      */
     private extraSettings: Record<string, string> = {};
 
+    private onLoggerData: ((data: Uint8Array) => void) | null = null;
+
+    private onPrinterData: ((imageBuffer: Uint8Array) => void) | null = null;
+
     constructor(extraSettings = {}) {
         super();
         this.extraSettings = extraSettings;
@@ -388,7 +401,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         // a valid state ready to be used
         this.gameBoy.reset();
         this.gameBoy.load_boot_default();
-        this.gameBoy.load_printer_ws();
+        this.gameBoy.load_null_ws();
         const cartridge = this.gameBoy.load_rom_ws(romData);
 
         // updates the name of the currently selected engine
@@ -465,6 +478,22 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         ];
     }
 
+    get sections(): SectionInfo[] {
+        return [
+            {
+                name: "Serial",
+                icon: require("../res/serial.svg"),
+                node: SerialSection({
+                    emulator: this,
+                    onLogger: (onLoggerData: LoggerCallback) =>
+                        (this.onLoggerData = onLoggerData),
+                    onPrinter: (onPrinterData: PrinterCallback) =>
+                        (this.onPrinterData = onPrinterData)
+                })
+            }
+        ];
+    }
+
     get help(): HelpPanel[] {
         return [
             {
@@ -740,6 +769,26 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         this.storeSettings();
     }
 
+    loadNullDevice() {
+        this.gameBoy?.load_null_ws();
+    }
+
+    loadLoggerDevice() {
+        this.gameBoy?.load_logger_ws();
+    }
+
+    loadPrinterDevice() {
+        this.gameBoy?.load_printer_ws();
+    }
+
+    onLoggerDevice(data: Uint8Array) {
+        this.onLoggerData?.(data);
+    }
+
+    onPrinterDevice(imageBuffer: Uint8Array) {
+        this.onPrinterData?.(imageBuffer);
+    }
+
     /**
      * Tries to load game RAM from the `localStorage` using the
      * current cartridge title as the name of the item and
@@ -810,6 +859,7 @@ declare global {
     interface Window {
         emulator: GameboyEmulator;
         panic: (message: string) => void;
+        loggerCallback: (data: Uint8Array) => void;
         printerCallback: (imageBuffer: Uint8Array) => void;
     }
 
@@ -822,9 +872,12 @@ window.panic = (message: string) => {
     console.error(message);
 };
 
+window.loggerCallback = (data: Uint8Array) => {
+    window.emulator.onLoggerDevice(data);
+};
+
 window.printerCallback = (imageBuffer: Uint8Array) => {
-    const imageUrl = bufferToDataUrl(imageBuffer, 160);
-    console.image(imageUrl, imageBuffer.length / 160 / 3);
+    window.emulator.onPrinterDevice(imageBuffer);
 };
 
 console.image = (url: string, size = 80) => {