pywebview-htmx
pywebview-htmx (import as pywebview_htmx) brings HTMX-style declarative interactions to PyWebview apps.
You write mostly normal HTML, annotate interactive elements with py-* attributes,
and pywebview-htmx wires those elements to Python methods exposed through
window.pywebview.api.
What You Get
- Declarative Python calls from HTML (
py-call,py-trigger,data-py-params) - Declarative target updates (
py-target,py-swap) - Built-in loading state support (
py-wait+.py-waiting) - Event hooks for lifecycle instrumentation (
py:trigger,py:beforeSwap, etc.) - Concurrency control (
latest-winsordrop) without custom plumbing - Automatic processing of newly swapped HTML fragments
- Built-in theme system (multiple shipped themes + reusable component CSS)
Mental Model
pywebview-htmx is a tiny runtime that does four core jobs:
- Find elements with
py-call. - Bind an event listener (default:
click, override withpy-trigger). - Call your Python API method through
window.pywebview.api[method](params). - Swap returned HTML into the DOM according to
py-target+py-swap.
In other words: your Python methods are your "server handlers", and returned HTML is your "response body".
Installation
In this repository
pdm install
In another PDM project
pdm add pywebview-htmx
Maintainer Docs
For maintainer and agent-facing repository guidance, see
docs/index.md. Run pdm run check for lint, tests, and
offline documentation checks. The documentation check requires the native
perfect-doc executable and can run alone with pdm run check-docs.
Agent Skill
This repository includes an installable coding-agent skill at
skills/use-pywebview-htmx/.
The folder in this repository is the source for a skill that tools such as
npx skills can discover and install from GitHub.
List the available skill from this repository:
npx skills add btfranklin/pywebview-htmx --list --full-depth
Install just this skill:
npx skills add btfranklin/pywebview-htmx --skill use-pywebview-htmx
Other skill installers can consume the same skill folder from this repository. The skill includes:
- the core usage workflow
- a concise contract reference
- implementation patterns
- troubleshooting guidance
Quick Start
from html import escape
from pywebview_htmx import JsonValue, create_window
class API:
def greeting(self, params: dict[str, JsonValue]) -> str:
name = escape(str(params.get("name", "world")))
return f"<p>Hello, {name}!</p>"
html = """
<!doctype html>
<html>
<body>
<button
py-call="greeting"
py-target="#result"
data-py-params='{"name": "pywebview-htmx"}'>
Say hello
</button>
<div id="result"></div>
</body>
</html>
"""
create_window("Quickstart", html, js_api=API())
You do not manually include runtime.js when using create_window();
it is injected automatically.
Important Differences From HTMX
- There is no HTTP request layer.
pywebview-htmxcalls a Python method onwindow.pywebview.apidirectly. - Python handlers should return an HTML string fragment. Returning objects or other data types is treated as an error.
- For ordinary buttons/links, params still come from
data-py-params. - For forms with
submit,input, orchangetriggers, named fields are serialized automatically and merged overdata-py-params. - Returned HTML is inserted directly into the DOM, so escape untrusted values before returning markup.
API Reference (Python)
create_window(title, html, js_api=None, theme="aurora", start=True, **kwargs)
Creates a PyWebview window with injected theme CSS (optional) and injected
pywebview-htmx runtime script.
title: window titlehtml: HTML document stringjs_api: Python object exposed aswindow.pywebview.apitheme: bundled theme name ("aurora","paper","cybermind") orNonestart: whether to callwebview.start()automatically**kwargs: forwarded towebview.create_window()
The return type is webview.Window | None. PyWebview returns None if window
creation is canceled. Check for None before you use the returned window.
WindowOptions defines the supported keyword names and their types. Type
checkers reject unknown names and values with incorrect types.
When to set start=False:
- You need to create multiple windows before starting
- You want to manage PyWebview startup/lifecycle yourself
get_runtime_script()
Returns the bundled JavaScript runtime as a string.
inject_runtime(html)
Injects the runtime script tag into an HTML string (idempotent).
encode_params_attr(params)
Returns HTML-escaped JSON that is safe to place in a data-py-params
attribute.
Non-finite numbers (NaN and positive or negative infinity) raise ValueError.
JSON does not support these values.
The parameter type is JsonValue: None, booleans, numbers, strings, lists of
JSON values, or dictionaries with string keys and JSON values. Use
dict[str, JsonValue] for handler parameters and nested payloads. These type
annotations do not validate data received through the JavaScript bridge.
from pywebview_htmx import encode_params_attr
payload = encode_params_attr({"user_id": 42, "mode": "full"})
button = f'<button data-py-params="{payload}">Load user</button>'
Theme helpers
list_themes()-> sorted list of available themesget_theme_css(theme)-> base CSS + selected theme CSSinject_theme(html, theme)-> inject or replace theme<style>block
Constants
DEFAULT_THEME(currently"aurora")
Declarative Attribute Reference
py-call (required)
Python API method name to invoke.
<button py-call="fetch_user">...</button>
py-trigger (optional)
DOM event name to bind. Default is click.
<form py-call="submit_form" py-trigger="submit">...</form>
<div py-call="show_tip" py-trigger="mouseenter">...</div>
When the runtime handles a native action that would leave or destructively change the current document, it prevents that default action. This includes form submission, link navigation, and submit/reset controls associated with a form. Ordinary buttons and the native state changes of checkboxes, radio buttons, and labels are preserved.
A label that contains its control calls Python once per activation. Clicking the label text still changes the checkbox or radio state.
data-py-params (optional)
JSON payload passed to Python method.
<button data-py-params='{"user_id": 42, "mode": "full"}'>...</button>
Notes:
- If missing, params default to
{}. - Invalid JSON or a value that is not an object emits
py:error. Python is not called. - For forms with
submit,input, orchangetriggers, named fields are serialized automatically and merged over this payload.
py-target (optional)
CSS selector of the element to update. If omitted, the triggering element is updated.
py-swap (optional)
How returned HTML is applied:
innerHTML(default)outerHTMLappend
Unknown value falls back to innerHTML.
py-wait (optional)
CSS selector of element receiving .py-waiting while request is in flight.
If missing, empty, invalid, or unresolvable, the triggering element is used.
The selector is resolved when each accepted request starts, so replacement
markup can provide the wait target for later requests.
py-policy (optional)
Per-element concurrency override. If omitted, the global
window.pywebviewHtmx.config.requestPolicy value is used.
latest-winsdrop
Runtime Config (JavaScript)
window.pywebviewHtmx.config includes:
defaultSwapStyle(default:"innerHTML")swapDelay(ms, default:0)settleDelay(ms, default:20)requestPolicy("latest-wins"default, or"drop")requestTimeout(ms, default:0;py-timeoutoverrides it per element)
Runtime defaults are read for each accepted request. An element's explicit
py-swap continues to override defaultSwapStyle; otherwise, changing
defaultSwapStyle affects its next request even when the element is already
bound.
Example:
window.pywebviewHtmx.config.requestPolicy = "drop";
window.pywebviewHtmx.config.swapDelay = 150;
window.pywebviewHtmx.config.settleDelay = 50;
Lifecycle Events
pywebview-htmx dispatches custom events you can observe for telemetry, debugging, and UX:
py:triggerpy:beforeSwappy:afterSwappy:ignored(whenrequestPolicy="drop"and request is in flight)py:error
These lifecycle events are observational and non-cancelable. Calling
preventDefault() on them does not change request or swap behavior. Every
py:error event includes event.detail.stale, a boolean indicating whether a
newer request had already been issued in the same request scope when the error
was emitted. Errors from stale requests remain observable even though stale
successful responses do not update the DOM.
event.detail.element identifies the original control. If that control has
been removed, the runtime dispatches the error on document.body so application
listeners can still receive it.
Example:
document.body.addEventListener("py:error", (event) => {
console.error("pywebview-htmx error", {
error: event.detail.error,
stale: event.detail.stale,
});
});
Concurrency Behavior
For a nonempty py-target, pywebview-htmx scopes request state by its
normalized selector string. Controls with the same selector coordinate request
ordering and drop behavior even when a swap replaces the matching DOM node.
Selector state is discarded after its in-flight requests finish. Controls
without py-target use the triggering element as their request state scope.
The py-wait selector is resolved for each accepted request. Loading state is
counted separately per resolved wait element, so a shared spinner stays active
until every request using that particular element finishes.
The bundled loading styles show .py-waiting without blocking pointer input.
The request policy controls whether another trigger is accepted.
latest-wins (default)
Multiple rapid requests are allowed; stale responses in the same request state scope are ignored. Only the latest request for that scope updates the DOM.
drop
If a request is already in flight for the same request state scope, new triggers
are ignored and py:ignored is emitted.
Python Handler Concurrency
latest-wins does not cancel Python work. Each accepted call still runs, even
when its returned HTML is later ignored. PyWebview runs exposed handlers in
separate threads. Protect shared mutable data with a lock or a database
transaction when needed.
drop applies to the same request scope. It does not prevent calls from other
scopes from changing the same Python data.
Bridge Readiness
PyWebview creates the bridge after the document loads. Wait for the
pywebviewready event before starting programmatic calls:
window.addEventListener("pywebviewready", () => {
document.querySelector("#load-button").click();
});
A call made before the bridge is available emits py:error. It is not queued.
Dynamic Content Re-processing
After a swap, pywebview-htmx binds controls in the inserted content. Append
and outer replacement do not scan existing siblings again. This enables chained interactions in returned fragments
without manual re-init code.
Theme System
pywebview-htmx ships a reusable component styling system plus multiple themes.
from pywebview_htmx import create_window, list_themes
print(list_themes())
# ['aurora', 'cybermind', 'paper']
create_window("My App", html, js_api=api, theme="cybermind")
Set theme=None to disable automatic theme injection.
Built-in Component Classes
The bundled CSS exposes canonical pyh-* component classes:
- Layout:
.pyh-shell,.pyh-hero,.pyh-grid,.pyh-card - Controls:
.pyh-btn,.pyh-btn-primary,.pyh-btn-secondary,.pyh-btn-ghost,.pyh-btn-danger - Content:
.pyh-result,.pyh-note,.pyh-code,.pyh-chip,.pyh-muted - Logs/lists:
.pyh-activity-list,.pyh-event-log - Utilities:
.pyh-row,.pyh-full-width,.pyh-inline-config
Shipping Your Own Theme
Use the same token pattern as bundled themes (--pyh-* variables) and inject your
custom CSS before runtime initialization. If you want PyWebview HTMX-style replacement
behavior, add your own <style data-pywebview-theme="my-theme">...</style> block.
Practical Patterns
Pattern 1: Form submit to Python
- Use
py-trigger="submit"on<form> - Give fields
nameattributes so they can be serialized - Use
data-py-paramsonly for extra static values you want merged in py-targeta result card/summary region
Pattern 2: Append activity/log rows
- Use
py-swap="append"to add<li>rows - Return a single row snippet from Python
Pattern 3: Replace full component
- Use
py-swap="outerHTML" - Return full replacement markup with the same outer id/selector if future interactions should keep targeting that component
Pattern 4: Theme switch from Python
- Expose a
switch_themePython method - Return HTML containing
<style data-pywebview-theme="...">...</style> - Swap a wrapper section with
py-swap="outerHTML"
Security Notes
- Returned HTML is inserted directly into the DOM.
- Escape/sanitize untrusted data before returning markup.
- Treat Python API methods as privileged application logic.
Troubleshooting
"Nothing happens when I click"
Check:
py-callmatches an existing method onwindow.pywebview.api- method returns a string of HTML
- no JSON parse error in
data-py-params py-targetselector exists- for forms, fields have
nameattributes
"New buttons in swapped HTML do not work"
This should work by default via post-swap processing. If you perform manual DOM changes outside of pywebview-htmx swaps, call:
window.pywebviewHtmx.process(document.body);
"Loading state looks wrong"
Style .py-waiting globally and/or point py-wait at a dedicated element.
"I got a Python API return type error"
pywebview-htmx expects handlers to return an HTML string fragment. Convert
structured data into markup before returning it.
Common Mistakes
- Returning dicts/lists instead of HTML strings
- Forgetting to escape untrusted values before interpolating into markup
- Omitting
nameattributes on form fields and expecting them to serialize - Using
py-swap="outerHTML"but not preserving the target component's selector/id - Manually mutating the DOM outside of
pywebview-htmxswaps without callingwindow.pywebviewHtmx.process(...)
Demo
Run the full feature showcase:
pdm run python app.py
The demo includes:
- Runtime config controls
- Live event feed
- All swap modes
- Multiple trigger types
- Concurrency policy behavior
- Dynamic fragment processing
- Python-driven live theme switching
License
MIT (see LICENSE).
Request deadlines
Set py-timeout="2000" for a two-second Python response deadline. The global
requestTimeout default is 0 (no deadline). Values are milliseconds from
0 to 2147483647. A timeout emits py:error with detail.phase="timeout" and
error.name="TimeoutError". The loading indicator stops, and a late response
cannot change the page. A late Python failure still emits py:error with
detail.expired=true.
A deadline does not cancel Python work or retry the call. Under drop, the
scope stays locked until Python finishes. Show the timeout message and let the
application check the operation's result before another attempt. If Python
never returns, the lock remains until the page is reloaded; reloading does not
prove that the operation was cancelled. Use latest-wins for independent read
operations that can safely run again.
Delay rapid input calls
Use py-debounce="200" on a read operation to wait until input stops for 200 ms.
Each event replaces the pending call for that control. Only the final event's
parameter snapshot is sent. Forms with py-trigger="input" or change
serialize their named fields, as submit forms do. Input during text composition
is not sent; the final input event supplies the completed text.
<form py-call="search" py-trigger="input" py-debounce="200" py-target="#results">
<label for="query">Search</label>
<input id="query" name="query" type="search">
</form>
<div id="results"></div>
The delay defaults to zero. Clicks and submissions remain immediate unless a
control explicitly sets py-debounce. A removed control cannot send its pending
call while detached. Waiting state and request policy start when the call is
sent, not while its timer is pending. drop can still reject that call if
another operation is running; use latest-wins for live read results. The delay
does not cancel Python work that has already started.
Focus after replacement
Give controls stable, unique IDs. When a destructive swap removes the focused control, the runtime focuses the inserted control with the same ID without scrolling the page. Text inputs and textareas also retain their selection, limited to the replacement value's length.
Focus is read immediately before the swap. Moving to a control outside the updated region during a request is respected. A lifecycle listener that sets focus after the swap also takes precedence. Append operations do not change focus. If no matching control is inserted, the runtime does not guess a new focus target.
Accessible loading and theme states
While a request is waiting, its wait element has aria-busy="true" and
.py-waiting. Shared wait elements remain busy until all visible waits finish
or time out. The runtime then restores the element's original aria-busy
value, or removes the attribute if it was absent. It does not add live regions
to application content. Use role="status" on a short result or status message
when an announcement is useful.
Bundled themes provide readable text for each button variant, visible keyboard
focus, and the matching light or dark color scheme for native controls.
Reduced-motion settings remove button movement and spinner animation while
keeping the busy marker visible. Custom theme authors should define
--pyh-focus, --pyh-color-scheme, and the --pyh-btn-primary-text,
--pyh-btn-secondary-text, and --pyh-btn-danger-text tokens along with the
existing colors.
Metadata
Release files for pywebview-htmx 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pywebview_htmx-0.6.0.tar.gz | 47.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pywebview_htmx-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.4 kB
Release files / pywebview_htmx-0.6.0.tar.gz
| Download URL | pywebview_htmx-0.6.0.tar.gz |
|---|---|
| Size | 47.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9bab5d2f873a580674cf7d3f523a717647e338c918a4680c60f7c74400929157
|
|
BLAKE2b-256 checksum How to use checksums |
04aef9654513199151d29e642f0c2ee7b932cc50dd0886c84131878ee0bf6391
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / pywebview_htmx-0.6.0-py3-none-any.whl
| Download URL | pywebview_htmx-0.6.0-py3-none-any.whl |
|---|---|
| Size | 22.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bf7906133a64ef6702c764a2fcfd9868739f857188af8bac5430fb04f97fd0f2
|
|
BLAKE2b-256 checksum How to use checksums |
be24e4f6e869f54f164f78e7dd503a808143e3db45a2990453b59f6e5b93539d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for pywebview-htmx 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pywebview_htmx-0.6.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.