HTML, CSS and JS in a Desktop App... Qt WebEngine vs. Electron vs.?

Comparing approaches to building desktop apps with web technologies and Python
Heads up! You've already completed this tutorial.

I'm building a desktop app and finding that some of my UI requirements (custom text handling, drag-and-drop containers, user-resizable layouts) are difficult to achieve with Qt widgets alone. I know HTML/CSS/JS can handle these things more easily. Should I use Electron, or could I use Qt WebEngine inside my Python app instead? What are the trade-offs?

This is a common crossroads for developers building desktop applications. You've learned enough about Qt widgets to know what they're good at — and where they start to feel like a fight. Things like custom text rendering, flexible drag-and-drop layouts, and fine-grained styling are areas where web technologies genuinely shine, and it's perfectly reasonable to consider them.

Let's walk through the main options and see how they compare.

Option 1: Electron

Electron is the most well-known framework for building desktop apps with HTML, CSS, and JavaScript. Apps like VS Code, Slack, and Notion are built on it.

What you get:

  • A full Chromium browser bundled into your app.
  • Complete access to web technologies — everything you can do in a browser, you can do in your app.
  • A large ecosystem of JavaScript libraries and tools.
  • Cross-platform support out of the box.

What you give up:

  • Memory usage. Each Electron app ships its own copy of Chromium. Even a simple app can use 100–300 MB of RAM at idle.
  • Bundle size. A minimal Electron app is typically 150+ MB on disk.
  • Python integration. Electron is a JavaScript-first world. If you want to keep your application logic in Python, you'd need to run a separate Python process and communicate with it over IPC, sockets, or a local HTTP server. This adds complexity.
  • Startup time. Launching a full Chromium instance takes a moment.

Electron is a solid choice if you're comfortable working primarily in JavaScript and don't need tight Python integration. But if Python is central to your project, it introduces friction.

Option 2: Qt WebEngine

Qt WebEngine is also built on Chromium, but it's embedded as a widget inside a Qt application. This means you can mix traditional Qt widgets with web content in the same window, and everything runs in a single process with your Python code.

What you get:

  • Full web rendering (HTML, CSS, JS) inside a QWebEngineView widget.
  • Direct communication between Python and JavaScript using Qt's built-in bridge mechanisms (QWebChannel).
  • The ability to combine Qt widgets and web views in the same application — for example, a native menu bar with a web-based editor panel.
  • Your app logic stays in Python. No need for a separate process or HTTP server (though you can use one if you want).

What you give up:

  • Bundle size is still significant. Qt WebEngine includes Chromium, so your app will still be large (though potentially smaller than a full Electron app since you're not shipping Node.js as well).
  • Memory usage is still notable. Chromium is Chromium. You'll use less than Electron in many cases, but it's still a heavy dependency.
  • Complexity of the bridge. Communicating between Python and JavaScript via QWebChannel works well, but it's asynchronous and requires some setup.

For a Python-centric desktop app that needs web-based UI for specific parts, Qt WebEngine is a compelling middle ground.

A Quick Comparison

Electron Qt WebEngine Pure Qt Widgets
UI flexibility Excellent Excellent (in web view) Good, but limited for some use cases
Python integration Requires IPC Native (QWebChannel) Native
Memory usage High Moderate–High Low
Bundle size Large (~150 MB+) Large (~100 MB+) Small (~30 MB+)
Styling control Full CSS Full CSS (in web view) QSS (limited CSS subset)
Learning curve JS ecosystem Python + some JS Python + Qt API

Getting Started with Qt WebEngine in PyQt6

Let's look at what it takes to embed a web view in a PyQt6 application. First, install the required package:

sh
pip install PyQt6 PyQt6-WebEngine

Here's a minimal example that loads a local HTML page inside a Qt window:

python
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from PyQt6.QtWebEngineWidgets import QWebEngineView


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Qt WebEngine Example")
        self.resize(800, 600)

        self.browser = QWebEngineView()
        self.browser.setHtml("""
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body {
                    font-family: sans-serif;
                    background: #1e1e2e;
                    color: #cdd6f4;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    height: 100vh;
                    margin: 0;
                }
                .container {
                    text-align: center;
                }
                h1 {
                    font-size: 2em;
                }
                p {
                    font-size: 1.2em;
                    color: #a6adc8;
                }
            </style>
        </head>
        <body>
            <div class="container">
                <h1>Hello from Qt WebEngine</h1>
                <p>This is HTML/CSS rendered inside a PyQt6 app.</p>
            </div>
        </body>
        </html>
        """)

        central_widget = QWidget()
        layout = QVBoxLayout(central_widget)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.browser)
        self.setCentralWidget(central_widget)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

Run this, and you'll see a styled HTML page rendered inside a normal desktop window. You have full CSS at your disposal — flexbox, grid, animations, custom fonts, whatever you need.

Communicating Between Python and JavaScript

The real power of Qt WebEngine for Python developers is the ability to send data back and forth between your Python code and the JavaScript running in the web view. This is done through QWebChannel.

Here's a complete example where a button in the HTML page calls a Python method, and Python sends a response back:

python
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import QObject, pyqtSlot


class Backend(QObject):
    """Python object exposed to JavaScript."""

    @pyqtSlot(str, result=str)
    def process_text(self, text):
        # Do something with the text in Python
        word_count = len(text.split())
        return f"Python received your text! Word count: {word_count}"


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Python ↔ JavaScript Bridge")
        self.resize(800, 600)

        # Set up the web channel
        self.backend = Backend()
        self.channel = QWebChannel()
        self.channel.registerObject("backend", self.backend)

        # Set up the web view
        self.browser = QWebEngineView()
        self.browser.page().setWebChannel(self.channel)

        # Load HTML with JavaScript that talks to Python
        self.browser.setHtml("""
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body {
                    font-family: sans-serif;
                    background: #1e1e2e;
                    color: #cdd6f4;
                    padding: 40px;
                    margin: 0;
                }
                textarea {
                    width: 100%;
                    height: 150px;
                    background: #313244;
                    color: #cdd6f4;
                    border: 1px solid #45475a;
                    border-radius: 8px;
                    padding: 12px;
                    font-size: 1em;
                    resize: vertical;
                    box-sizing: border-box;
                }
                button {
                    margin-top: 12px;
                    padding: 10px 24px;
                    background: #89b4fa;
                    color: #1e1e2e;
                    border: none;
                    border-radius: 6px;
                    font-size: 1em;
                    cursor: pointer;
                }
                button:hover {
                    background: #74c7ec;
                }
                #result {
                    margin-top: 20px;
                    padding: 16px;
                    background: #313244;
                    border-radius: 8px;
                    min-height: 40px;
                }
            </style>
            <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
        </head>
        <body>
            <h1>Write something</h1>
            <textarea id="editor" placeholder="Type here..."></textarea>
            <button onclick="sendToPython()">Analyze in Python</button>
            <div id="result"></div>

            <script>
                var backend = null;

                new QWebChannel(qt.webChannelTransport, function(channel) {
                    backend = channel.objects.backend;
                });

                function sendToPython() {
                    var text = document.getElementById('editor').value;
                    if (backend) {
                        backend.process_text(text, function(response) {
                            document.getElementById('result').innerText = response;
                        });
                    }
                }
            </script>
        </body>
        </html>
        """)

        central_widget = QWidget()
        layout = QVBoxLayout(central_widget)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.browser)
        self.setCentralWidget(central_widget)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

In this example, the Backend class is a Python QObject with a slot that JavaScript can call directly. The QWebChannel script (included via qrc:///qtwebchannel/qwebchannel.js, which is bundled with Qt WebEngine) handles the communication plumbing. You type text in the HTML textarea, click the button, and Python processes it and returns the result.

This pattern scales well. You can expose multiple Python objects, emit signals from Python that JavaScript listens to, and build a rich two-way communication layer — all without running a separate server.

Which Approach Should You Choose?

For a Python developer building a desktop app that needs the styling flexibility of web technologies:

  • Qt WebEngine is often the best fit. You keep your application logic in Python, you get full CSS styling power for your UI, and you can mix web views with native Qt widgets where it makes sense. The cost is the Chromium dependency and its memory footprint.

  • Electron makes more sense if you're primarily a JavaScript developer, or if you're building something where the entire app lives in the browser and Python is only needed for occasional backend tasks.

  • Pywebview is worth exploring if bundle size and memory matter a lot to you, and you're willing to handle cross-platform rendering quirks.

  • Pure Qt widgets remain the right choice for apps where native look-and-feel is the priority and your UI needs are well-served by standard controls.

There's no single best answer — it depends on what matters most for your specific project. But for a writing application with custom text containers, drag-and-drop, and fine-grained visual control, the combination of PyQt6 and Qt WebEngine gives you a strong Python-native foundation with all the styling freedom of the web.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Packaging Python Applications with PyInstaller by Martin Fitzpatrick

This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

Martin Fitzpatrick

HTML, CSS and JS in a Desktop App... Qt WebEngine vs. Electron vs.? was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.