Overriding acceptNavigationRequest in QWebEnginePage

Intercept link clicks in QWebEngineView and handle navigation yourself
Heads up! You've already completed this tutorial.

I'm trying to override acceptNavigationRequest on a QWebEnginePage so I can intercept link clicks and handle them myself. It works when I load a URL using .load(), but when I switch to .setHtml() to set HTML content directly, the override stops working — links navigate to a new page instead of being intercepted. Why does setHtml behave differently, and how can I fix it?

When you're building a PyQt6 application that uses QWebEngineView to render HTML content as a UI — rather than as a full web browser — you'll often want to intercept link clicks and decide what happens yourself. The way to do this is by subclassing QWebEnginePage and overriding its acceptNavigationRequest method.

This works well when you load content via a URL (using .load()), but there's a subtle gotcha when you use .setHtml() instead. Let's walk through how this all works and how to avoid the common pitfall.

Setting up a custom QWebEnginePage

To intercept navigation requests, you create a subclass of QWebEnginePage and override acceptNavigationRequest. This method is called every time the web engine is about to navigate to a new URL — whether that's because the user clicked a link, a form was submitted, or the page was initially loaded.

Here's a minimal custom page class:

python
from PyQt6.QtWebEngineCore import QWebEnginePage


class CustomPage(QWebEnginePage):
    def acceptNavigationRequest(self, url, nav_type, is_main_frame):
        if nav_type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            print(f"Link clicked: {url.toString()}")
            # Return False to block navigation
            return False
        # Allow all other navigation (e.g. initial page load)
        return True

When a link is clicked, this returns False to prevent the web engine from navigating away. For all other navigation types (like the initial page load), it returns True so the content can be displayed. This approach relies on signals and slots — the core mechanism PyQt6 uses for communication between objects.

Using .load() vs .setHtml()

If you load your HTML from a file using .load(), everything works as expected. The page loads, your acceptNavigationRequest override fires on link clicks, and you're in control.

But when you switch to .setHtml() to inject HTML directly from a string, you might find that link clicks stop being intercepted. The page tries to navigate away, and your override seems to be ignored.

Why .setHtml() behaves differently

When you call .setHtml() without a second argument, the content is loaded with no base URL. Internally, QWebEngine assigns it an empty or about:blank origin. When you then click a link — even a simple anchor like <a href="internal://some-action"> — the web engine treats it as a cross-origin navigation from a blank page. Depending on the URL scheme and the engine's internal security policies, this can cause the navigation to be handled in a way that bypasses your acceptNavigationRequest.

The fix is to provide a base URL as the second argument to .setHtml(). This gives your content a proper origin, and the web engine processes link clicks through the normal navigation pipeline — which means your override gets called.

python
from PyQt6.QtCore import QUrl

html = """
<html>
<body>
    <h1>Hello</h1>
    <a href="action://do-something">Click me</a>
</body>
</html>
"""

# Without a base URL — acceptNavigationRequest may not fire on link clicks
# page.setHtml(html)

# With a base URL — works correctly
page.setHtml(html, QUrl("local://"))

The base URL doesn't need to be a real, reachable address. It just needs to be a valid URL that gives the page an origin. Using something like QUrl("local://") or QUrl("app://internal/") works well and makes it clear this isn't a real web address.

Complete working example

Here's a full application that loads HTML via .setHtml() with a base URL and intercepts link clicks using a custom QWebEnginePage. If you're new to building PyQt6 applications, you may want to start with creating your first window before diving into this example:

python
import sys

from PyQt6.QtCore import QUrl
from PyQt6.QtWidgets import QApplication, QMainWindow
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEnginePage


class CustomPage(QWebEnginePage):
    def acceptNavigationRequest(self, url, nav_type, is_main_frame):
        if nav_type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            print(f"Intercepted link click: {url.toString()}")
            # Handle the action however you like here.
            # Return False to prevent the web engine from navigating.
            return False

        # Allow the initial page load and other non-link navigation.
        return True


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("acceptNavigationRequest Example")
        self.resize(600, 400)

        self.browser = QWebEngineView()
        self.page = CustomPage(self)
        self.browser.setPage(self.page)

        html = """
        <html>
        <head>
            <style>
                body { font-family: sans-serif; padding: 20px; }
                a { display: inline-block; margin: 10px 0; font-size: 18px; }
            </style>
        </head>
        <body>
            <h1>Custom Navigation Example</h1>
            <p>Click a link below. The navigation will be intercepted.</p>
            <a href="action://greet">Say Hello</a><br>
            <a href="action://farewell">Say Goodbye</a><br>
            <a href="https://www.example.com">External Link (also blocked)</a>
        </body>
        </html>
        """

        # Provide a base URL so acceptNavigationRequest works correctly.
        self.browser.setHtml(html, QUrl("local://"))

        self.setCentralWidget(self.browser)


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

When you run this, clicking any of the links will print a message to the console instead of navigating away. The acceptNavigationRequest method fires for each click, and returning False keeps the current page content in place.

Since you're intercepting the URLs, you can use them to trigger specific behavior in your application. For example, you could define your own URL scheme (like action://) and dispatch based on the path:

python
class CustomPage(QWebEnginePage):
    def acceptNavigationRequest(self, url, nav_type, is_main_frame):
        if nav_type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
            if url.scheme() == "action":
                action = url.host()
                if action == "greet":
                    print("Hello!")
                elif action == "farewell":
                    print("Goodbye!")
                else:
                    print(f"Unknown action: {action}")
            else:
                # For real URLs, you could open them in the system browser:
                # QDesktopServices.openUrl(url)
                print(f"Blocked external navigation to: {url.toString()}")
            return False

        return True

This pattern is very useful when you're rendering Markdown or other generated HTML content and want to use links as interactive UI elements rather than actual web navigation. If you want to build a more complete browser-like experience instead, take a look at our web browser example for a full working implementation, or explore actions, toolbars and menus to add navigation controls to your application.

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

Overriding acceptNavigationRequest in QWebEnginePage 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.