ワンクリックで
htag-development
Guidelines and best practices for building modern, state-of-the-art web applications using the htag v2 framework.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Guidelines and best practices for building modern, state-of-the-art web applications using the htag v2 framework.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | htag-development |
| description | Guidelines and best practices for building modern, state-of-the-art web applications using the htag v2 framework. |
Use this skill to design, implement, and refine web applications using the htag framework.
Tag)Every UI element in htag is a component created via Tag.
Tag factory for standard HTML elements (e.g., Tag.div, Tag.button).Tag.* class.+= or the <= operator (e.g., self <= Tag.p("hello") or self += Tag.p("hello"))..root property to get a reference to the main Tag.App instance (useful for triggering app-level events or modals)..parent to access the parent component, and .childs to access the list of child components.self.add(child) or self.add(lambda: ...) for explicit addition. This is particularly useful when returning components from reactive lambdas, as it ensures they are properly parented even if they're not direct children. Note that Tag.add, Tag.call, Tag.update, etc., are reserved names and will raise an AttributeError.self.clear(*content) to remove all children and optionally add new content in one call.XSS Protection: All strings added as children (e.g. Tag.div("hello")) are automatically HTML-escaped to prevent XSS (except for style and script tags).
# -*- coding: utf-8 -*-
from htag import Tag
class MyComponent(Tag.div):
def init(self, name: str, **kwargs: dict[str, Any]) -> None:
# Traditional way:
self <= Tag.h1(f"Hello {name}")
# New Context Manager way (preferred for complex trees):
with Tag.div(_class="container"):
Tag.h2("Subtitle")
Tag.p("Content goes here")
### 1a. Fragments (`Tag()`)
A fragment allows grouping multiple tags without rendering a wrapper tag (like `div`) in the final HTML.
- Use `Tag()` (without arguments) to create a fragment.
- **Limitation**: Since a fragment has no physical presence in the DOM, it cannot be targets of partial reactive updates. For elements that need to update their own `.text` or `_class` reactively, prefer `Tag.span()` or `Tag.div()`.
```python
with Tag() as fragment:
Tag.span("Part 1")
Tag.span("Part 2")
self <= fragment
### 2. Component Lifecycle
htag provides three lifecycle hooks to override on custom components:
- `init(**kwargs)`: Called exactly once at the end of component initialization. Use this instead of overriding `__init__` to avoid `super()` boilerplate.
- **Automatic Assignment**: Any non-prefixed keyword argument (not starting with `_` or `_on`) is automatically assigned as an instance attribute *before* `init` is called.
- **Example**: `Tag.div(toto=42)` will result in the instance having a `.toto` attribute set to `42`.
- **Positional Arguments**: `*args` are automatically appended as children before `init` is evaluated.
- `on_mount()`: Fired when the component is firmly attached to the main `App` tree (`self.root` is ready).
- **F5 / Page Load**: In `WebApp`, `on_mount()` is re-triggered on every initial page load (`GET /`), even if the session instance is reused. This allows components to reset ephemeral UI state (like status labels or timers) whenever the user refreshes the page.
- `on_unmount(self)`: Fired when the component is removed from the tree. In `WebApp`, it is **also called before every page refresh (F5)**, allowing you to properly clean up resources (e.g., cancelling background tasks) before the view is reset.
- `on_destroy(self)`: Fired when the application instance is definitively discarded by the runner. Unlike `on_unmount`, it is **not called on page refresh (F5)**, making it the ideal place to release session-level resources (e.g., closing database connections or stopping companion servers) without interrupting them during UI navigation. It can be synchronous or asynchronous.
> [!NOTE]
> **Multiple Mounts version Init**: You might notice `on_mount` called multiple times during the initial load if you use both `GTag`'s auto-stack mechanism and manual addition (e.g., `self.child = MyChild(); self <= self.child`). This is harmless but good to know when counting mounts in tests.
> **Progressive UI note**: Both `on_mount()` and `on_unmount()` support `yield` (or `yield` in `async` generators) for progressive, multi-step UI rendering. In `WebApp`, `on_unmount()` is correctly called before `on_mount()` version a page refresh (F5), ensuring resource cleanup (e.g. cancelling tasks). `htag` intelligently queues `on_mount` generators until the client connects. *Remember for `on_unmount`: store a reference to external targets before returning the generator (e.g., `self.app_root = self.root`), as the component is detached and `self.parent` relies on `None` when the generator executes.*
### 3. Composite Components
When creating complex UI components (like a Card or a Window), you should override the `add(self, o)` method so that when users do `my_card <= content`, the content goes into the correct inner container, not the root tag.
```python
class Card(Tag.div):
def init(self, title, **kwargs):
self["class"] = "card"
self <= Tag.h2(title)
self.body = Tag.div(_class="card-body")
# Use Tag.div.add to bypass the overridden add method during init
Tag.div.add(self, self.body)
def add(self, o):
# Redirect append operations (+-) to the body container
self.body <= o
htag supports both traditional "dirty-marking" and modern reactive State.
Multiple States: States Container (Recommended):
For any application managing more than one reactive variable, the States class is the preferred approach. It acts as a single container for multiple State objects, allowing for cleaner code and bulk data operations.
self.s.count += 1)..dump() to get a dictionary of all values and .load(dict) to restore them at once.from htag import States
self.s = States(count=0, loading=False, user={"name": "Guest"})
self.s.count += 1 # Triggers re-render for count observers
self.s.user["name"] = "Alice" # Triggers re-render for user observers
Single State Object:
Declare individual state variables when you only need one or are passing it as a component argument: self.count = State(0).
State constructor is idempotent. You can safely "promote" any input to a State using s = State(value). If value is already a State (or a _StateProxy), it is returned as-is, preserving its identity and observers. This is the recommended way for component developers to handle potentially reactive arguments.self.count += 1, if self.count > 0: ... (full support for comparison and math).self.items.append("new").self.data["users"].append(new_user) works automatically (nested dicts, lists, sets, and tuples are proxied).for item in self.items: item.done = True works reactively because iteration yields proxies.self.user.name = "Bob" (where self.user is a State(User())) delegates setting to the underlying object and notifies automatically.int(self.count), bool(self.status) work transparently.Tag.div(self.count) or Tag.div(lambda: f"Count: {self.count}").self.count.get() to retrieve the underlying value regardless of its type. If the wrapped object has a .get() method (such as a dictionary) and arguments are provided, it automatically delegates to it (e.g. state.get("key", default)).Reactive & Boolean Attributes:
State objects directly for dynamic updates: Tag.button("Submit", _disabled=self.loading)._disabled, _checked, _required) are handled automatically:
True: Renders the attribute name only (e.g., disabled).False or None: Omits the attribute entirely.Rapid Content Updates:
.text property to quickly replace all text content of a tag: self.my_label.text = "New Status". This completely clears existing children and replaces them with a single string.
Background Tasks & update():State, mutations from background tasks (started via asyncio.create_task) automatically trigger UI synchronization..update() method (e.g., self.update()) which schedules a throttled UI broadcast. By default, updates are throttled at 50ms (configurable). You can adjust this on the fly: use self.update(throttle=0.1) for 100ms throttle, or self.update(throttle=0) to force an immediate broadcast.on_mount and on_unmount for component initialization and cleanup. Both hooks fully support synchronous/asynchronous generators (for yielding intermediate UI states) as well as standard async coroutines (async def without yield) which are executed once the client connects. (This pattern is now robust against page refreshes in WebApp).class App(Tag.App):
def init(self):
self.count = State(0)
Tag.div(self.count)
def on_mount(self):
async def run():
while True:
self.count += 1
await asyncio.sleep(1)
self.task = asyncio.create_task(run())
def on_unmount(self):
self.task.cancel()
Traditional Reactivity (HTML Attributes & Events):
Tag.div(_class="btn", _id="myid"). This is only for the constructor.self: ALWAYS use dictionary syntax. This handles attributes with dashes perfectly and is the modern standard: self["style"] = "color:red", self["disabled"] = True, self["data-test"] = 123.self.style (without underscore or brackets) merely sets a private Python attribute that won't be rendered in HTML._class="btn" (in init), self["class"] = "btn".class="btn", self._class = "btn" (deprecated).on are mapped to Python callbacks.
self["onclick"] = my_funcCSS Class Helpers:
tag.add_class("active") — adds a class if not already present.tag.remove_class("active") — removes a class if present.tag.toggle_class("hidden") — adds or removes a class.tag.has_class("active") — returns bool.class attribute is defined via a lambda or a State object. htag automatically wraps lambdas or updates the State value to preserve reactivity while applying the requested changes.find_tag)You can recursively search for a tag within a tree using self.find_tag(root, tag_id).
id attributes.GTag object or None.# In test.py example:
target = self.find_tag(self, "my-custom-id")
if target:
target.text = "Found and updated!"
htag supports setting custom HTML IDs via _id="myid".
data-htag-id attribute. The internal communication bridge uses this to ensure partial DOM updates still target the correct element even if the HTML id attribute is overridden.htag automatically synchronizes browser state for all form elements (input, textarea, select) before any Python callback is executed.
Tag instance:
["value"]: Current string value for text/select/number inputs.["checked"]: Boolean state for checkboxes and radios.["name"]: The original name of the input.e.value: The primary value (string for text, boolean for checkboxes).e.checked: Specifically for checkboxes/radios, always provides the boolean state.e.target["value"] / e.target["checked"]: Guaranteed to reflect reality.Form Submission:
When using a Tag.form, the submit event (e.g. _onsubmit) receives an Event object where e.value is a dictionary containing all named form fields (_name="fieldname").
e["fieldname"] (this is a shortcut for e.value["fieldname"]).e.value dictionary.class MySearch(Tag.form):
def init(self, term=""):
self <= Tag.input(_name="q", _value=term)
self <= Tag.input(_type="submit", _value="Search")
self["onsubmit"] = self.do_search
@prevent
def do_search(self, e):
# e.value is {'q': '...'}
print(f"Searching for: {e['q']}")
htag implements a robust 3-level transport fallback mechanism to ensure connectivity in restricted environments:
_fallback_queue./event POST request retrieves and returns all accumulated updates in a single JSON response.Key Robustness Rules:
eval() calls can always reach them, all transport references should be stored on the global window object (e.g., window.ws, window.sse, window.use_fallback).console.log("htag (WS): updates...", ...) vs htag (HTTP): ....X-HTAG-TOKEN). Parano mode (XOR cipher) provides lightweight obfuscation against MITM proxies.When using WebApp, every tag has access to the current Starlette Request or WebSocket via the self.request property. This allows for direct access to the session, headers, and other request data.
class MyComponent(Tag.div):
def init(self):
# Access the Starlette session directly!
username = self.request.session.get("user", "Guest")
self <= f"Hello {username}"
def on_click(self, e):
# Mutate the session
self.request.session["last_click"] = "now"
htag includes a built-in visual aid mechanism to help developers track bugs:
Runner(App, debug=True) (Default): During development, ANY error that occurs (a Python exception in a callback, a JavaScript error, or a network disconnection) is visually reported via a Shadow DOM overlay in the screen (displaying js/traceback errors).Runner(App, debug=False): Use this for production. Tracebacks are logged internally on the server, and only generic "Internal Server Error" messages are shown in the client UI to prevent sensitive data leakage.When building complex hierarchical structures (like file trees or nested menus), choosing the right rendering strategy is critical for both performance and reliability.
Dynamic (Lambda-based) Rendering:
Tag.div(lambda: self.render_items(self.data))Persistent (Init-based) Rendering (Recommended for complex trees):
init() and toggle their visibility.def build_tree(self, folder_data):
for item in folder_data:
# 1. Create the toggleable branch container
with Tag.div() as container:
# 2. Add the header/toggle
Tag.div(item.name, _onclick=lambda e, p=item.path: self.toggle(p))
# 3. Create the children container with REACTIVE visibility
with Tag.div(_class=lambda p=item.path: "" if p in self.expanded else "hidden"):
if item.children:
self.build_tree(item.children) # Recurse
hidden class is much faster than htag's engine adding/removing elements from the DOM.State(set()), always use os.path.normpath() to avoid mismatching due to trailing slashes or different separators._onclick=lambda e, p=current_path: self.do(p).Modern web applications should avoid blocking alert() boxes in favor of non-intrusive toast notifications.
Structure:
Implementation Pattern (JS-side Timer):
Prefer using client-side timers (setTimeout) to trigger the auto-dismissal. This is much more efficient than using asyncio.sleep in Python, as it offloads the timing logic to the browser and avoids keeping Python tasks alive for simple UI effects.
class App(Tag.App):
def init(self):
# 1. Stacking container
self.toasts = Tag.div(_class="toast-container")
self += self.toasts
def notify(self, message, type="info"):
# 2. Add individual toast with an 'expire' event
t = Tag.div(message, _class=f"toast toast-{type}",
_onexpire=lambda e: t.remove())
self.toasts.add(t)
# 3. Trigger dismissal via client-side setTimeout
# Best Practice: Always pass an empty object {} if no DOM event is forwarded
t.call_js(f"setTimeout(() => htag_event('{t.id}', 'expire', {{}}), 5000)")
Manual JS Events (htag_event):
The global htag_event(id, event_name, event) function is the bridge that triggers Python callbacks from JS:
id: The target component's ID (self.id).event_name: The callback name (e.g., 'click', 'expire').event: (Optional) Data object passed to the Python event argument. Always pass {} if no specific data is needed to ensure robustness across browser environments.Premium Aesthetics: Use CSS keyframes for entry animations (e.g., sliding up or fading in) and distinct background colors for Success, Error, and Info states.
htag supports "simple events" where the htag_event function can be used to pass primitive values (strings, numbers) or custom objects from JavaScript to Python, bypassing the standard DOM Event extraction.
onhashchange support:
The framework automatically handles the hashchange event. When self["onhashchange"] is set, the Python callback receives an event object with newURL and oldURL attributes.
class App(Tag.App):
def init(self):
self["onhashchange"] = self.on_hash
def on_hash(self, e):
# e.newURL and e.oldURL are available
print(f"Navigated to: {e.newURL}")
Custom simple events: You can trigger custom events from JavaScript with any data:
# In Python
tag["oncustom"] = lambda e: print(f"Received: {e.value}")
# In JavaScript (via call_js or statics)
htag_event('tag_id', 'custom', 'any string or number')
htag_event('tag_id', 'custom', {any: 'object'})
For multi-page behavior in a Single Page Application (SPA), use the built-in Router. It maps URL hashes (e.g., #/users/42) to component classes and handles synchronization with the browser history.
Key Features:
on_mount() and on_unmount() are called automatically when swapping pages.:id) are injected directly as keyword arguments into the page's init().router.navigate("/new-path") to steer the UI from Python.router.path is a State("") reflecting the current route, enabling reactive tab/link styling.from htag import Tag, Router
class HomePage(Tag.div):
def init(self):
Tag.h1("Home")
Tag.a("Go to User 42", _href="#/users/42")
class UserPage(Tag.div):
def init(self, id: str): # 'id' is extracted from the URL
self.user_id = id
Tag.h1(f"User Profile: {id}")
def on_mount(self):
# Triggered when navigating TO this page
print(f"Loading user {self.user_id}...")
class App(Tag.App):
def init(self):
# 1. Instantiate the Router
self.router = Router()
# 2. Register routes
self.router.add_route("/", HomePage)
self.router.add_route("/users/:id", UserPage)
# 3. Navigation with active link highlighting (reactive!)
with Tag.nav():
Tag.a("Home", _href="#/", _class=lambda: "active" if self.router.path == "/" else "")
Tag.a("Users", _href="#/users/42", _class=lambda: "active" if str(self.router.path).startswith("/users") else "")
# 4. Add it to the UI
self <= self.router
Custom 404:
Provide a component to handle unmatched routes using router.set_not_found(My404Component). The component will receive the requested path as a path keyword argument (if its init() accepts it).
If you are migrating legacy htag v1 components, be aware of these core framework changes:
1. Event Callbacks & Custom Attributes (e.target):
def method(self, o):), and you accessed custom properties directly (o.info).Event object (def method(self, e):). To access custom attributes passed during component instantiation (like info=dict(...)), you MUST use the target property: e.target.info.@prevent on the callback and access form values directly from e.value (e.g., q = e.value["q"]).2. Component Content Replacement (.clear()):
.set() or Content() wrappers from external component libraries like htbulma to replace the inner HTML of a component..clear(new_child). The .set() method does not exist on native v2 Tag instances.3. Move to Composition & Tailwind:
b.VBox, b.Progress()) with native CSS composition (e.g., Tag.div(_class="flex flex-col"), Tag.div(_class="animate-spin")) leveraging the .statics injection of modern CSS frameworks like Tailwind.htag_mode cookie)In some network environments (e.g., restrictive corporate proxies), WebSockets or SSE might be technically "available" but extremely slow to time out, causing a frustrating delay before falling back to a working mode.
htag supports a special cookie htag_mode to manually force a specific transport protocol, bypassing the standard auto-detection/timeout logic:
htag_mode=http: Forces the application into pure HTTP mode immediately. Both WebSocket and EventSource are mocked to fail instantly.htag_mode=sse: Forces the application to use SSE (Server-Sent Events) by making the initial WebSocket connection fail immediately.How to use:
document.cookie="htag_mode=http;path=/").GET / request and injects a specialized JS bootstrap patch into the page.statics class attribute on your main App class.Tag.style and Tag.script. Remember to use _src for script/image URLs.# -*- coding: utf-8 -*-
class App(Tag.App):
statics = [
Tag.script(_src="https://cdn.tailwindcss.com"),
Tag.style("body { background-color: #f8fafc; }")
]
Use the styles class attribute for component-scoped CSS. The framework auto-prefixes every CSS rule with .htag-ClassName and adds it to the component's root element:
class MyCard(Tag.div):
styles = """
.title { color: #1e40af; font-weight: bold; }
.content { padding: 16px; border: 1px solid #e2e8f0; }
"""
def init(self, title):
self <= Tag.h2(title, _class="title")
self <= Tag.p("Styles are scoped!", _class="content")
The generated CSS will be .htag-MyCard .title { ... } — no style leaking. The scoped <style> is injected once per class, even with multiple instances. Supports @media queries, @keyframes, pseudo-selectors (:hover, ::before), and comma-separated selectors.
Note:
stylesis declarative (class-level, processed once at init). For dynamic styling during interactions, use dictionary syntax or class helpers:self["style"] = "color: red;" # inline style self.toggle_class("active") # toggle CSS class Tag.div(_class=lambda: "on" if s else "off") # reactive (init)
Tag.statics)In addition to scoped styles, you can inject global dependencies or static assets (like external CSS/JS) for a specific component using the class attribute statics.
statics must be a list of Tag elements (usually Tag.script or Tag.style or Tag.link).<head> exactly once, regardless of how many instances of the component you create.class MapWidget(Tag.div):
statics = [
Tag.link(_rel="stylesheet", _href="https://unpkg.com/leaflet/dist/leaflet.css"),
Tag.script(_src="https://unpkg.com/leaflet/dist/leaflet.js")
]
def init(self):
self.id = "map-container"
### Page Title
To set the `<title>` of your application's tab, include a `Tag.title` element in the `statics` list of your main `App` class. htag automatically extracts it to set the initial page title and prevents duplicate title tags in the `<head>`. If omitted, it defaults to the App's class name.
```python
class MyApp(Tag.App):
statics = [Tag.title("My Awesome App")]
### Event Control
Use decorators to control event behavior:
- `@prevent`: Calls `event.preventDefault()` on the client side.
- `@stop`: Calls `event.stopPropagation()` on the client side.
### Use `yield` for UI Rendering (Stepping)
In event handlers, you can use `yield` to trigger partial UI updates. Combined with `async def`, this allows for clean, multi-step asynchronous progressions.
```python
async def _onclick(self, event):
self.text = "Step 1: Processing..."
yield # UI updates immediately to show "Processing..."
await asyncio.sleep(1) # Asynchronous wait (non-blocking)
self.text = "Step 2: Nearly done..."
yield
await asyncio.sleep(1)
self.text = "Done!"
ChromeApp: Primary choice for local/desktop usage. Attempts to launch a clean desktop-like Kiosk window via Chromium/Chrome binaries. If none are found, it falls back to opening the default system browser via webbrowser.open.htag_app.app into a Starlette instance.Hot-Reloading for Development:
To prevent constantly closing and re-opening your application window during development, pass reload=True to the runner. The master process will watch for changes and restart the child worker server seamlessly.
Ephemeral Ports (port=0):
If you run multiple htag apps or tests, you might encounter "Address already in use" errors. Pass port=0 to the runner to pick an available port automatically. htag will resolve the port and open the browser on the correct URL.
if __name__ == "__main__":
from htag import ChromeApp
# Picks a random available port automatically and opens the browser
ChromeApp(MyApp).run(port=0, reload=True)
When you are in developpment using "uv" (and htag is installed in the venv).
Use uv run htagm build <path> to build a standalone executable for your htag app.
PYTHONIOENCODING="utf-8" uv run htagm build main.py
To ensure each user has their own isolated session/state:
Tag.App class to the runner, NOT an instance.if __name__ == "__main__":
from htag import ChromeApp
ChromeApp(MyApp).run() # Correct: unique instance per user