QWebEngineView change anchor behavior

How to intercept link clicks in QWebEngineView and open them in a new window or the system browser
Heads up! You've already completed this tutorial.

I'm using QWebEngineView to display HTML that's been produced from Markdown. Clicking a link causes the loaded web page to replace the entire contents of the QWebEngineView widget. How can I launch the page in a separate browser window instead? Is there something I can override to intercept the event and handle it myself?

When you load HTML content into a QWebEngineView, any links that the user clicks will navigate inside that same widget by default. The HTML you were displaying gets replaced with whatever the link points to, which usually isn't what you want — especially if your QWebEngineView is being used to show rendered Markdown or documentation.

The good news is that Qt gives you a clean way to intercept navigation requests and decide what to do with them. You can open links in a brand new window, reuse a single external window, or even hand links off to the user's default system browser.

Let's walk through each approach.

How navigation works in QWebEngineView

Every QWebEngineView has an associated QWebEnginePage that handles the actual page logic — loading content, running JavaScript, and processing navigation requests. When a user clicks a link, the page calls a method named acceptNavigationRequest(). This method receives the URL, the type of navigation, and whether it's happening in the main frame. By returning True, navigation proceeds as normal. By returning False, you block it.

By subclassing QWebEnginePage and overriding acceptNavigationRequest(), you gain full control over what happens when a link is clicked. This follows the same pattern used throughout Qt, where you subclass existing widgets to customize behavior.

Setting up the project

Before we start, make sure you have PyQt6 and PyQt6-WebEngine installed:

sh
pip install PyQt6 PyQt6-WebEngine

The web engine components are shipped as a separate package, so you need both.

The most straightforward approach is to open every clicked link in its own QWebEngineView window. Here's a custom page class that does exactly that:

python
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView


class CustomWebEnginePage(QWebEnginePage):
    """Custom WebEnginePage to customize how we handle link navigation."""

    # Store external windows so they aren't garbage collected.
    external_windows = []

    def acceptNavigationRequest(self, url, _type, isMainFrame):
        if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            w = QWebEngineView()
            w.setUrl(url)
            w.show()

            # Keep a reference so the window stays alive.
            self.external_windows.append(w)
            return False

        return super().acceptNavigationRequest(url, _type, isMainFrame)

When acceptNavigationRequest is called, we check whether the navigation type is NavigationTypeLinkClicked. If it is, we create a new QWebEngineView, set its URL, and show it. We return False to prevent the original view from navigating away.

One thing to note: we store a reference to each new window in the external_windows list. Without this, Python's garbage collector would destroy the window object as soon as the method returns, and you'd never see it appear. This is the same challenge you encounter when creating multiple windows in PyQt6 — you always need to keep a reference.

For any other type of navigation — like the initial page load — we pass the request through to the default handler via super().

To use this custom page, you set it on your QWebEngineView with .setPage():

python
from PyQt6.QtCore import QUrl

self.browser = QWebEngineView()
self.browser.setPage(CustomWebEnginePage(self))
self.browser.setUrl(QUrl("https://www.example.com"))

Reusing a single external window

If you'd rather send all clicked links to the same external window instead of opening a new one every time, you can keep a single reference and reuse it:

python
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView


class CustomWebEnginePage(QWebEnginePage):
    """Custom WebEnginePage that reuses a single external window."""

    external_window = None

    def acceptNavigationRequest(self, url, _type, isMainFrame):
        if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            if self.external_window is None:
                self.external_window = QWebEngineView()

            self.external_window.setUrl(url)
            self.external_window.show()
            return False

        return super().acceptNavigationRequest(url, _type, isMainFrame)

The first time a link is clicked, a new window is created. After that, every subsequent click updates the same window's URL. This gives a cleaner experience if you don't want multiple windows piling up.

Sometimes you don't want to manage extra windows at all — you just want links to open in whatever browser the user normally uses (Chrome, Firefox, etc.). Python's QDesktopServices makes this easy:

python
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWebEngineCore import QWebEnginePage


class CustomWebEnginePage(QWebEnginePage):
    """Custom WebEnginePage that opens links in the system browser."""

    def acceptNavigationRequest(self, url, _type, isMainFrame):
        if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            QDesktopServices.openUrl(url)
            return False

        return super().acceptNavigationRequest(url, _type, isMainFrame)

QDesktopServices.openUrl() hands the URL off to the operating system, which opens it in the default browser. This is often the best choice when your QWebEngineView is displaying local or generated content and external links should leave your application entirely.

You can also combine these approaches. For example, you might want internal links (same domain) to navigate normally inside the view, while external links open in the system browser:

python
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWebEngineCore import QWebEnginePage


class CustomWebEnginePage(QWebEnginePage):
    """Open external links in system browser, allow internal navigation."""

    def acceptNavigationRequest(self, url, _type, isMainFrame):
        if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            if url.host() != "myapp.local":
                QDesktopServices.openUrl(url)
                return False

        return super().acceptNavigationRequest(url, _type, isMainFrame)

Here, any link that doesn't point to myapp.local gets sent to the system browser. Links within myapp.local are allowed to navigate normally. You can adjust the condition to match whatever domain or URL pattern makes sense for your application.

Complete working example

Here's a full application that loads some HTML containing links, and opens any clicked link in the system's default browser. If you're new to building PyQt6 applications, you might want to start with the first window tutorial before diving into this example.

python
import sys

from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget


class CustomWebEnginePage(QWebEnginePage):
    """Custom WebEnginePage that opens links in the system browser."""

    def acceptNavigationRequest(self, url, _type, isMainFrame):
        if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            QDesktopServices.openUrl(url)
            return False

        return super().acceptNavigationRequest(url, _type, isMainFrame)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QWebEngineView Link Handling")
        self.setMinimumSize(800, 600)

        # Create the web view with our custom page.
        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self.browser))

        # Load some HTML with links to demonstrate the behavior.
        html = """
        <html>
        <head>
            <style>
                body {
                    font-family: sans-serif;
                    padding: 40px;
                    line-height: 1.6;
                }
                a {
                    color: #1a73e8;
                }
            </style>
        </head>
        <body>
            <h1>Link Handling Demo</h1>
            <p>
                This content is displayed inside a QWebEngineView.
                Clicking any of the links below will open them in your
                system's default browser, rather than navigating away
                from this page.
            </p>
            <ul>
                <li><a href="https://www.python.org">Python.org</a></li>
                <li><a href="https://www.qt.io">Qt.io</a></li>
                <li><a href="https://www.pythonguis.com">PythonGUIs.com</a></li>
            </ul>
        </body>
        </html>
        """
        self.browser.setHtml(html, QUrl("https://myapp.local/"))

        # Set up the layout.
        container = QWidget()
        layout = QVBoxLayout()
        layout.addWidget(self.browser)
        container.setLayout(layout)
        self.setCentralWidget(container)


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

Run this and you'll see a simple page with three links. Click any of them and they'll open in your default web browser, while the original content stays exactly as it was in the QWebEngineView.

You can swap CustomWebEnginePage for either of the other versions shown earlier (new window per link, or single reused window) depending on the behavior you need. The pattern is always the same: subclass QWebEnginePage, override acceptNavigationRequest, and set your custom page on the view with setPage(). Once your application is ready for distribution, you can package it with PyInstaller for easy deployment.

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

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick

(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

Martin Fitzpatrick

QWebEngineView change anchor behavior 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.