Accessible wx.html2.WebView surfaces for wxPython (content view, modal dialog, live preview, chat) that read correctly in NVDA and JAWS.
Project description
wx-accessible-webview
Accessible wx.html2.WebView surfaces for wxPython — render web content
screen readers can actually read, verified in NVDA and JAWS.
One small toolkit covering every WebView surface a real app needs:
AccessibleWebView— an embeddable content view (live region, status, JS bridge, Escape/F6 key bridges, open-links-in-browser, text fallback);SidePreview— a live preview pane that updates in place as you type;AccessibleHtmlDialog— a modal dialog with an HTML body and real wx buttons (plusshow_message/confirmhelpers);AccessibleChatView— a chat surface with an in-page composer and suggestion chips.
The problem
wx.html2.WebView is a factory-created native control (Edge WebView2 on
Windows, WKWebView on macOS, WebKitGTK on Linux). Because it's native and
factory-built, you can't meaningfully subclass it to fix accessibility — and
embedded in a wxPython app it reads inconsistently in NVDA and often not at all
in JAWS. This has blocked accessible rich/HTML content (Markdown, chat,
previews) in wxPython apps for years.
The idea
A WebView's accessibility is driven by the HTML you render into it, not by the wx widget. So instead of fighting the control, render a semantic, screen-reader-friendly document — and the screen reader follows it like any web page.
AccessibleWebView gives you:
- a semantic page —
lang, viewport, readable + high-contrast /forced-colorsCSS; - an optional ARIA live region (
role="log" aria-live="polite") so appended content is announced automatically; - an assertive
role="status"region for transient announcements; - a JS→Python bridge (
window.<name>.post(...)) so the page can send events back; - optional Escape-to-close, bridged out of the native control (which swallows it);
- focus management into the content;
- a graceful read-only text fallback when no WebView backend is present.
Install
pip install wx-accessible-webview
(Requires wxPython 4.2+.)
Usage
import wx
from wx_accessible_webview import AccessibleWebView
app = wx.App()
frame = wx.Frame(None, title="Demo")
view = AccessibleWebView(
frame,
title="Conversation",
live_region=True, # appended content is announced
handler_name="demo", # window.demo.postMessage(...)
on_message=lambda data: print("from page:", data),
escape_to_close=True,
on_close=lambda: frame.Close(),
)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(view.control, 1, wx.EXPAND)
frame.SetSizer(sizer)
view.append("<article><h2>Quill</h2><p>Hi! New messages are announced.</p></article>")
view.status("Ready")
frame.Show()
app.MainLoop()
Render Markdown by converting it to HTML yourself (any Markdown library) and
passing the result to append() / set_content() — this package stays
dependency-light (just wxPython) and leaves rendering choices to you.
API
AccessibleWebView — embeddable content view
AccessibleWebView(parent, *, title, lang="en", live_region=True, handler_name="awv", on_message=None, on_close=None, on_return=None, escape_to_close=False, open_links_externally=False, initial_html="", styles=...)
.control— the underlying wx control (WebView, or the text fallback)..using_webview—Trueif a real WebView backend is in use..append(html_fragment)— append HTML to the content area (announced iflive_region)..set_content(html_body)— replace the content area..status(text)— announce transient status (assertive region)..clear()— reset to an empty document..focus()— move focus into the content..run_js(script)— run arbitrary JS in the page.
on_return (Escape or F6) is handy for handing focus back from a preview
pane to an editor; open_links_externally opens http(s) links in the system
browser instead of navigating the embedded view.
SidePreview — live preview pane
SidePreview(parent, *, title="Preview", lang="en", on_return=None, open_links_externally=True, styles=...)
.update(body_html)— replace the body in place (scroll/SR position preserved)..control,.focus().
AccessibleHtmlDialog — modal dialog
AccessibleHtmlDialog(parent, title, body_html, buttons=None, *, size=(640, 560), open_links_externally=True, lang="en", styles=...)
buttons— list of(label, return_id); the last is the default. Defaults to a single Close button..show_modal() -> int— returns the chosen button id (orwx.ID_CANCELon Escape/close).- Helpers:
show_message(parent, title, body_html, ...)(one button) andconfirm(parent, title, body_html, ...) -> bool(OK/Cancel).
AccessibleChatView — chat surface
AccessibleChatView(parent, *, title="Conversation", intro=None, suggestions=(), placeholder=..., composer_label=..., send_label="Send", on_send=None, on_close=None, lang="en")
.append_message(speaker, body_html)— add a turn (announced via live region)..status(text),.hide_suggestions(),.set_input_enabled(enabled),.focus(),.clear().
The transcript, suggestion chips, and the message edit field all live inside one accessible page; Enter sends, Shift+Enter inserts a newline, suggestions hide after the first message (like Apple Intelligence).
Examples
examples/demo.py— the minimal content view.examples/showcase.py— content view +AccessibleHtmlDialog+confirm.examples/markdown_chat.py— a fullAccessibleChatViewMarkdown chat.
Born out of Quill
This came out of building Quill — the screen-reader-first writing and document environment (a Community Access project).
Accessible embedded web content in wxPython is a problem people had been chipping at for years. The usual experience: it'd sometimes read in NVDA, and basically never in JAWS — JAWS just wouldn't treat the embedded WebView as a real web document. It had eaten a lot of hours across a lot of attempts.
Quill needed it badly — the AI chat emits Markdown (headings, lists, code), so it wanted real HTML, which meant the WebView, which meant the wall. Rather than keep fighting the native control, the breakthrough was to stop trying to "fix" the widget and instead drive accessibility entirely through the HTML/ARIA rendered into it — and that finally read correctly in NVDA and JAWS.
So after a lot of tries, we got it working. This library is that solution, broken out so the whole wxPython community can use it. In Quill it now powers the AI chat, the Markdown/HTML preview, the About dialog, and the update dialogs.
Created by
Made by Taylor Arndt, a Community Access open-source project — built to be contributed to.
Contributing
Anyone can contribute — and we want you to. Bug reports, features, docs, and
especially screen-reader testing (NVDA / JAWS / Narrator / VoiceOver / Orca)
are all welcome. Open an issue or a pull request — see CONTRIBUTING.md.
Contributors
Everyone who contributes is recognized here automatically:
Contributor image via contrib.rocks.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wx_accessible_webview-0.2.0.tar.gz.
File metadata
- Download URL: wx_accessible_webview-0.2.0.tar.gz
- Upload date:
- Size: 17.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc907ebf5211c0ea06a9a7e6e310e564a2be7637cf57daeca050dd912a7a357e
|
|
| MD5 |
805b71bbf63a82cfa1d62315b0e2fd72
|
|
| BLAKE2b-256 |
3d3cbdbd37c34e0e3bb44d4e1b1298e52db851d077afab23f1d9da211b364bc9
|
File details
Details for the file wx_accessible_webview-0.2.0-py3-none-any.whl.
File metadata
- Download URL: wx_accessible_webview-0.2.0-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
841492bd95e38eddc7d914f0e9fd6d1a113db75ab62548468e65074cd70de636
|
|
| MD5 |
1bed535d616e2ee136bd007fc71d3e0d
|
|
| BLAKE2b-256 |
06f9ed8651c960737a3ef9b5c23d2d98bff157417922f7384ca8609694f506a0
|