NitroUI Skill Guide
A zero-dependency Python 3.8+ library for programmatic HTML generation. Build HTML with Python classes instead of string templates.
from nitro_ui import Div, H1, Paragraph
page = Div(
H1("Welcome"),
Paragraph("Built with NitroUI"),
cls="container"
)
print(page.render())
# <div class="container"><h1>Welcome</h1><p>Built with NitroUI</p></div>
Imports
Standard Import
from nitro_ui import Div, H1, Paragraph, UnorderedList, ListItem, Image
Core Classes
from nitro_ui import HTMLElement, Fragment, Component, Slot, Partial, from_html
Styling System
from nitro_ui.styles import CSSStyle, StyleSheet, Theme
All Tags
Document Structure
| PascalCase | HTML | Notes |
|---|---|---|
HTML | <html> | Includes <!DOCTYPE html>. Default lang="en", dir="ltr" (overridable) |
Head | <head> | |
Body | <body> | |
Title | <title> | |
Meta | <meta> | Self-closing |
Link | <link> | Self-closing |
Script | <script> | |
Style | <style> | |
Base | <base> | Self-closing |
Noscript | <noscript> | |
IFrame | <iframe> | |
Template | <template> | |
Svg | <svg> | SVG camelCase attrs supported (see below) |
Math | <math> |
Layout
| PascalCase | HTML |
|---|---|
Div | <div> |
Section | <section> |
Article | <article> |
Header | <header> |
Footer | <footer> |
Nav | <nav> |
Main | <main> |
Aside | <aside> |
HorizontalRule | <hr> (self-closing) |
Details | <details> |
Summary | <summary> |
Dialog | <dialog> |
Address | <address> |
Hgroup | <hgroup> |
Search | <search> |
Menu | <menu> |
Text
| PascalCase | HTML | Notes |
|---|---|---|
H1-H6 | <h1>-<h6> | |
Paragraph | <p> | |
Span | <span> | |
Strong | <strong> | |
Em | <em> | |
Bold | <b> | |
Italic | <i> | |
Underline | <u> | |
Strikethrough | <s> | |
Small | <small> | |
Mark | <mark> | |
Del | <del> | |
Ins | <ins> | |
Subscript | <sub> | |
Superscript | <sup> | |
Code | <code> | |
Pre | <pre> | |
Kbd | <kbd> | |
Samp | <samp> | |
Var | <var> | |
Blockquote | <blockquote> | |
Quote | <q> | |
Cite | <cite> | |
Abbr | <abbr> | |
Dfn | <dfn> | |
Time | <time> | |
Anchor | <a> | Preferred name for anchor links |
Href | <a> | Backward-compatible alias for Anchor |
Br | <br> | Self-closing |
Wbr | <wbr> | Self-closing |
Bdi | <bdi> | Bidirectional isolation |
Bdo | <bdo> | Bidirectional override |
Ruby | <ruby> | Ruby annotation |
Rt | <rt> | Ruby text |
Rp | <rp> | Ruby fallback parenthesis |
Data | <data> | Machine-readable value |
Lists
| PascalCase | HTML |
|---|---|
UnorderedList | <ul> |
OrderedList | <ol> |
ListItem | <li> |
DescriptionList | <dl> |
DescriptionTerm | <dt> |
DescriptionDetails | <dd> |
Forms
| PascalCase | HTML | Notes |
|---|---|---|
Form | <form> | Also has Form.with_fields() classmethod |
Input | <input> | Self-closing |
Button | <button> | |
Textarea | <textarea> | |
Select | <select> | Also has Select.with_items() classmethod |
Option | <option> | |
Optgroup | <optgroup> | |
Label | <label> | |
Fieldset | <fieldset> | |
Legend | <legend> | |
Output | <output> | |
Progress | <progress> | |
Meter | <meter> | |
Datalist | <datalist> |
Tables
| PascalCase | HTML |
|---|---|
Table | <table> |
TableHeader | <thead> |
TableBody | <tbody> |
TableFooter | <tfoot> |
TableRow | <tr> |
TableHeaderCell | <th> |
TableDataCell | <td> |
Caption | <caption> |
Colgroup | <colgroup> |
Col | <col> (self-closing) |
Table.from_json(path) and Table.from_csv(path) create tables from files (first row = headers in <thead>, rest in <tbody>).
Media
| PascalCase | HTML |
|---|---|
Image | <img> (self-closing) |
Video | <video> |
Audio | <audio> |
Source | <source> (self-closing) |
Track | <track> (self-closing) |
Picture | <picture> |
Figure | <figure> |
Figcaption | <figcaption> |
Canvas | <canvas> |
Embed | <embed> (self-closing) |
Object | <object> |
Param | <param> (self-closing) |
Map | <map> |
Area | <area> (self-closing) |
HTMLElement Constructor
HTMLElement(
*children, # HTMLElement, str, or nested lists (auto-flattened)
tag: str, # Required HTML tag name
self_closing: bool = False,
**attributes # HTML attributes as keyword arguments
)
Special attribute mappings:
clsorclass_name→ renders asclassclass_→ also maps toclass_namefor_elementorfor_→ renders asfordata_*→data-*(other underscores become hyphens)- SVG camelCase attrs via snake_case:
view_box→viewBox,preserve_aspect_ratio→preserveAspectRatio, etc. (52 SVG attributes supported)
Boolean attributes (disabled, checked, required, hidden, readonly, autofocus, autoplay, controls, loop, muted, multiple, open, selected, defer, async, novalidate, formnovalidate, reversed, allowfullscreen, inert, playsinline, nomodule, default, ismap, itemscope):
True→ bare attribute (e.g.<input disabled>)False→ attribute omitted entirelyNone→ attribute omitted entirely
div = Div(
H1("Title"),
"Some text",
id="main",
cls="container",
data_value="123"
)
# <div id="main" class="container" data-value="123"><h1>Title</h1>Some text</div>
# Boolean attributes
Input(type="checkbox", checked=True, disabled=False)
# <input type="checkbox" checked />
# SVG camelCase attributes
HTMLElement(tag="svg", view_box="0 0 100 100", preserve_aspect_ratio="xMidYMid")
# <svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"></svg>
Properties
| Property | Type | Mutable | Notes |
|---|---|---|---|
tag | str | Yes | HTML tag name |
children | List[HTMLElement] | Yes | Use methods preferred |
text | str | Yes | Text content |
attributes | dict | Yes | Setting invalidates style cache |
self_closing | bool | Yes |
All Methods
Children
| Method | Returns | Description |
|---|---|---|
append(*children) | self | Add children to end |
prepend(*children) | self | Add children to start |
clear() | self | Remove all children |
pop(index=0) | HTMLElement | Remove and return child |
remove_all(condition) | self | Remove matching children |
replace_child(index, new) | None | Replace child at index (not chainable) |
count_children() | int | Number of direct children |
first() | HTMLElement|None | First child |
last() | HTMLElement|None | Last child |
filter(cond, recursive=False, max_depth=1000) | Iterator | Matching children/descendants |
find_by_attribute(key, value, max_depth=1000) | HTMLElement|None | First descendant with matching attr |
Children can be HTMLElement, str, or nested lists. None is silently ignored. Other types raise ValueError.
Attributes
| Method | Returns | Description |
|---|---|---|
add_attribute(key, value) | self | Set single attribute |
add_attributes([(k,v),...]) | self | Set multiple attributes |
remove_attribute(key) | self | Remove attribute |
get_attribute(key) | str|None | Get attribute value |
has_attribute(key) | bool | Check existence |
get_attributes(*keys) | dict | Get all (or specified) attributes. Returns a copy. |
generate_id() | None | Add unique ID if none exists (not chainable) |
Inline Styles
| Method | Returns | Description |
|---|---|---|
add_style(key, value) | self | Set CSS property. Raises ValueError on dangerous values. |
add_styles(dict) | self | Set multiple CSS properties |
get_style(key) | str|None | Get CSS property value |
remove_style(key) | self | Remove CSS property |
CSS values are validated against injection patterns (javascript:, expression(), url(data:), etc.).
Rendering
| Method | Returns | Description |
|---|---|---|
render(pretty=False, max_depth=1000) | str | HTML string. Raises RecursionError if depth exceeded. |
str(element) | str | Same as render() |
Serialization
| Method | Returns | Description |
|---|---|---|
to_dict(max_depth=1000) | dict | {tag, self_closing, attributes, text, children} |
to_json(indent=None) | str | JSON string |
HTMLElement.from_dict(data, max_depth=1000) | HTMLElement | Reconstruct from dict. Validates input types. Restores subclass types via tag registry. |
HTMLElement.from_json(json_str) | HTMLElement | Reconstruct from JSON |
from_html(html_str, fragment=False) | HTMLElement|List|None | Parse HTML string. Preserves SVG camelCase attrs. |
from_html is also available as a standalone function: from nitro_ui import from_html
When fragment=True, returns List[HTMLElement]. When False, returns single element or None.
from_dict() reconstructs Fragment instances correctly and uses a tag registry to preserve subclass types where possible.
Event Callbacks (Override in Subclasses)
| Method | Called When |
|---|---|
on_load() | Element constructed |
on_before_render() | Before render() |
on_after_render() | After render() |
on_unload() | Element garbage collected |
Utility
| Method | Returns | Description |
|---|---|---|
clone() | HTMLElement | Deep copy |
__enter__/__exit__ | self | Context manager support |
Fragment (No Wrapper Tag)
Renders children without a wrapper element.
from nitro_ui import Fragment, H1, Paragraph
frag = Fragment(H1("Title"), Paragraph("Content"))
print(frag.render())
# <h1>Title</h1><p>Content</p>
Use cases: conditional rendering without wrapper divs, returning multiple elements from functions, list composition.
Partial (Raw HTML)
Embed raw HTML for trusted content. Bypasses escaping.
from nitro_ui import Head, Meta, Title, Partial
# Inline raw HTML
Head(
Meta(charset="utf-8"),
Partial("""
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script>gtag('config', 'GA_ID');</script>
"""),
Title("My Page")
)
# Or load from file (lazy-loaded at render time)
Partial(file="partials/analytics.html")
Warning: Only use with trusted content - bypasses XSS protections.
Component (Reusable Components)
Build reusable components with declarative templates and named slots.
from nitro_ui import Component, Slot, H3, Paragraph, Div, Button
class Card(Component):
tag = "div"
class_name = "card"
def template(self, title: str):
return [
H3(title, cls="card-title"),
Slot() # children go here
]
# Usage
card = Card("My Title",
Paragraph("Content goes here"),
id="card-1",
cls="featured"
)
# <div class="card featured" id="card-1">
# <h3 class="card-title">My Title</h3>
# <p>Content goes here</p>
# </div>
Class Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
tag | str | "div" | Root element tag |
class_name | str | None | Default CSS class(es) |
Template Method
Override template() to define the component structure. Parameters become props.
def template(self, title: str, level: str = "info"):
return [
Span(f"[{level.upper()}]"),
Span(title),
Slot()
]
Slots
Use Slot() for the default slot (receives *children), Slot("name") for named slots.
class Modal(Component):
tag = "div"
class_name = "modal"
def template(self, title: str):
return [
Div(H2(title), Slot("actions"), cls="header"),
Div(Slot(), cls="body"),
Div(Slot("footer", default=Button("Close")), cls="footer")
]
# Usage - named slots via kwargs
Modal("Confirm",
Paragraph("Are you sure?"), # default slot
actions=Button("X"), # named slot
footer=[Button("Cancel"), Button("OK")] # named slot (list)
)
Slot rules:
Slot()— default slot, receives positional*childrenSlot("name")— named slot, receivesname=kwargSlot("name", default=element)— fallback content if slot not provided- Named slot kwargs accept single element or list
- Empty slots render nothing
Props vs Attributes
Arguments are separated automatically:
- HTMLElement args → default slot children
- Non-HTMLElement positional args → props (matched to
template()params) - Kwargs matching template params → props
- Kwargs matching slot names → slot content
- Other kwargs → HTML attributes on root element
class Alert(Component):
def template(self, message: str, level: str = "info"):
return [Slot("icon"), Span(message)]
Alert("Hello", # prop: message
level="warning", # prop (template param)
icon=Icon("warning"), # slot
id="alert-1", # HTML attr
role="alert" # HTML attr
)
Class Name Merging
User-provided cls merges with the Component's default class_name (appends):
class Card(Component):
class_name = "card"
Card("Title", cls="featured")
# → <div class="card featured">...</div>
Lifecycle Hooks
Components inherit HTMLElement lifecycle hooks:
class MyComponent(Component):
def on_load(self): ... # After construction
def on_before_render(self): ... # Before render()
def on_after_render(self): ... # After render()
Styling System
CSSStyle
Represents CSS styles with pseudo-selectors and responsive breakpoints.
from nitro_ui.styles import CSSStyle
style = CSSStyle(
background_color="#007bff", # snake_case -> kebab-case
color="white",
padding="10px 20px",
border_radius="5px",
_hover=CSSStyle(background_color="#0056b3"), # pseudo-selector
_md=CSSStyle(padding="15px 30px") # breakpoint
)
Pseudo-selectors (prefix _): _hover, _active, _focus, _visited, _link, _first_child, _last_child, _nth_child, _before, _after
Breakpoints (prefix _): _xs (0px), _sm (640px), _md (768px), _lg (1024px), _xl (1280px), _2xl (1536px)
| Method | Returns | Description |
|---|---|---|
to_inline() | str | CSS string for style="..." (base styles only) |
merge(other) | CSSStyle | New merged style (other overrides) |
has_pseudo_or_breakpoints() | bool | Has pseudo/responsive styles |
is_complex(threshold=3) | bool | Has many properties |
to_dict() / CSSStyle.from_dict(data) | Serialization |
StyleSheet
Manages CSS classes and generates <style> tag content.
from nitro_ui.styles import StyleSheet, CSSStyle, Theme
# Optional theme for CSS variables
stylesheet = StyleSheet(theme=Theme.modern())
# Register classes
btn = stylesheet.register("btn-primary", CSSStyle(
background_color="#007bff",
color="white",
_hover=CSSStyle(background_color="#0056b3")
))
# BEM naming
card = stylesheet.register_bem("card", style=CSSStyle(padding="20px"))
# "card"
card_header = stylesheet.register_bem("card", element="header",
style=CSSStyle(font_weight="bold"))
# "card__header"
card_featured = stylesheet.register_bem("card", modifier="featured",
style=CSSStyle(border="2px solid blue"))
# "card--featured"
# Use in elements
Button("Click", cls=btn)
Div(cls=f"{card} {card_featured}")
# Generate output
css = stylesheet.render() # CSS string
tag = stylesheet.to_style_tag() # <style>...</style>
| Method | Returns | Description |
|---|---|---|
register(name, style) | str | Register style, returns class name |
register_bem(block, element=, modifier=, style=) | str | BEM class name |
get_style(name) | CSSStyle|None | Get registered style |
has_class(name) | bool | Check if registered |
unregister(name) | bool | Remove class |
clear() | None | Remove all classes |
set_breakpoint(name, value) | None | Set breakpoint value |
render(pretty=True) | str | CSS output |
to_style_tag(pretty=True) | str | <style> tag |
count_classes() | int | Number of classes |
get_all_class_names() | List[str] | All class names |
to_dict() / StyleSheet.from_dict(data, theme=) | Serialization |
Theme
Preset design systems with CSS variables.
from nitro_ui.styles import Theme
theme = Theme.modern() # Blue primary, Inter font, modern components
theme = Theme.classic() # Traditional blue, Georgia serif
theme = Theme.minimal() # Black/white, system fonts
# Custom
theme = Theme(
name="Custom",
colors={"primary": "#007bff", "secondary": "#6c757d"},
typography={"font_family": "Inter, sans-serif"},
spacing={"sm": "8px", "md": "16px", "lg": "24px"},
components={"button": {"primary": CSSStyle(...)}}
)
| Method | Returns | Description |
|---|---|---|
get_css_variables() | dict | {"--color-primary": "#007bff", ...} |
get_component_style(component, variant="default") | CSSStyle|None | Component style |
to_dict() / Theme.from_dict(data) | Serialization |
Common Patterns
Complete Page
from nitro_ui import HTML, Head, Body, Title, Meta, Div, H1, Paragraph
page = HTML(
Head(
Title("My Page"),
Meta(charset="utf-8"),
Meta(name="viewport", content="width=device-width, initial-scale=1")
),
Body(
Div(
H1("Welcome"),
Paragraph("Hello, world!"),
cls="container"
)
)
)
html = page.render(pretty=True)
Navigation
from nitro_ui import Nav, UnorderedList, ListItem, Anchor
navbar = Nav(
UnorderedList(
ListItem(Anchor("Home", href="/")),
ListItem(Anchor("About", href="/about")),
ListItem(Anchor("Contact", href="/contact")),
),
cls="navbar"
)
Form
from nitro_ui import Form, Label, Input, Button
login_form = Form(
Label("Email:", for_element="email"),
Input(type="email", id="email", name="email", required=True),
Label("Password:", for_element="password"),
Input(type="password", id="password", name="password", required=True),
Button("Log In", type="submit"),
action="/login",
method="post"
)
Table from Data
from nitro_ui import Table, TableHeader, TableBody, TableRow, TableHeaderCell, TableDataCell
data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
t = Table(
TableHeader(TableRow(TableHeaderCell("Name"), TableHeaderCell("Age"))),
TableBody(*[
TableRow(TableDataCell(row["name"]), TableDataCell(str(row["age"])))
for row in data
])
)
Dynamic List
from nitro_ui import UnorderedList, ListItem
items = ["Apple", "Banana", "Orange"]
list_element = UnorderedList(*[ListItem(item) for item in items])
Method Chaining
container = (Div()
.add_attribute("id", "hero")
.add_styles({"background": "#f0f0f0", "padding": "2rem"})
.append(H1("Welcome"))
.append(Paragraph("Get started today.")))
Custom Component (Recommended)
from nitro_ui import Component, Slot, Div, H2, Paragraph
class Card(Component):
tag = "div"
class_name = "card"
def template(self, title: str):
return [
H2(title, cls="card-title"),
Div(Slot(), cls="card-body")
]
card = Card("My Card", Paragraph("Card content here"), id="card-1")
Custom Component (Alternative - Direct Subclass)
from nitro_ui import HTMLElement, H2, Paragraph
class Card(HTMLElement):
def __init__(self, title, content, **kwargs):
super().__init__(tag="div", cls="card", **kwargs)
self.append(
H2(title, cls="card-title"),
Paragraph(content, cls="card-body")
)
card = Card("My Card", "Card content here", id="card-1")
SVG Elements
from nitro_ui import Svg, HTMLElement
# Using the Svg tag class with snake_case kwargs
icon = Svg(
HTMLElement(
HTMLElement(tag="circle", cx="12", cy="12", r="10"),
tag="g",
),
view_box="0 0 24 24",
preserve_aspect_ratio="xMidYMid meet",
cls="icon"
)
# Parsing SVG HTML preserves camelCase attributes
from nitro_ui import from_html
svg = from_html('<svg viewBox="0 0 100 100"><circle r="50"/></svg>')
print(svg.render())
# <svg viewBox="0 0 100 100"><circle r="50" /></svg>
Parsing and Modifying HTML
from nitro_ui import from_html, Paragraph
element = from_html('<div class="old"><h1>Title</h1></div>')
element.add_attribute("class", "new")
element.add_style("padding", "20px")
element.append(Paragraph("New content"))
html = element.render()
Serialization Round-Trip
from nitro_ui import Div, H1, HTMLElement
page = Div(H1("Title"), id="page")
json_str = page.to_json(indent=2)
loaded = HTMLElement.from_json(json_str)
html = loaded.render()
Page with StyleSheet
from nitro_ui import HTML, Head, Body, Button, Style
from nitro_ui.styles import CSSStyle, StyleSheet, Theme
theme = Theme.modern()
stylesheet = StyleSheet(theme=theme)
btn = stylesheet.register("btn", CSSStyle(
background_color="var(--color-primary)",
color="white",
padding="10px 20px",
_hover=CSSStyle(background_color="var(--color-primary-dark)")
))
page = HTML(
Head(Style(stylesheet.render())),
Body(Button("Click Me", cls=btn))
)
Framework Integration
FastAPI
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from nitro_ui import HTML, Head, Body, Title, H1
app = FastAPI()
@app.get("/", response_class=HTMLResponse)
async def home():
return HTML(
Head(Title("FastAPI + NitroUI")),
Body(H1("Hello!"))
).render()
Flask
from flask import Flask
from nitro_ui import HTML, Head, Body, Title, H1
app = Flask(__name__)
@app.route("/")
def home():
return HTML(
Head(Title("Flask + NitroUI")),
Body(H1("Hello!"))
).render()
Django
from django.http import HttpResponse
from nitro_ui import HTML, Head, Body, Title, H1
def home(request):
page = HTML(
Head(Title("Django + NitroUI")),
Body(H1("Hello!"))
)
return HttpResponse(page.render())
Form Builder
Generate HTML5 form fields with validation attributes using the Field class.
from nitro_ui import Form, Button, Field
form = Form(
Field.email("email", label="Email", required=True, placeholder="you@example.com"),
Field.password("password", label="Password", min_length=8),
Field.checkbox("remember", label="Remember me"),
Button("Log In", type="submit"),
action="/login",
method="post"
)
Text Fields
Field.text(name, label=None, required=False, min_length=None, max_length=None, pattern=None, placeholder=None, value=None, wrapper=None, **attrs)
Field.email(name, label=None, required=False, placeholder=None, value=None, wrapper=None, **attrs)
Field.password(name, label=None, required=False, min_length=None, max_length=None, placeholder=None, wrapper=None, **attrs)
Field.url(name, label=None, required=False, placeholder=None, value=None, wrapper=None, **attrs)
Field.tel(name, label=None, required=False, pattern=None, placeholder=None, value=None, wrapper=None, **attrs)
Field.search(name, label=None, required=False, placeholder=None, value=None, wrapper=None, **attrs)
Field.textarea(name, label=None, required=False, rows=None, cols=None, min_length=None, max_length=None, placeholder=None, value=None, wrapper=None, **attrs)
Numeric & Date Fields
Field.number(name, label=None, required=False, min=None, max=None, step=None, value=None, wrapper=None, **attrs)
Field.range(name, label=None, min=0, max=100, step=None, value=None, wrapper=None, **attrs)
Field.date(name, label=None, required=False, min=None, max=None, value=None, wrapper=None, **attrs)
Field.time(name, label=None, required=False, min=None, max=None, value=None, wrapper=None, **attrs)
Field.datetime_local(name, label=None, required=False, min=None, max=None, value=None, wrapper=None, **attrs)
Selection Fields
# Select with different option formats
Field.select("country", ["USA", "Canada", "Mexico"]) # strings
Field.select("status", [("active", "Active"), ("inactive", "Inactive")]) # tuples
Field.select("priority", [{"value": "1", "label": "Low", "disabled": True}]) # dicts
# Pre-selected value
Field.select("country", ["USA", "Canada"], value="Canada", label="Country")
# Checkbox (label wraps input)
Field.checkbox("terms", label="I agree to the Terms", required=True)
# Radio buttons (wrapped in fieldset)
Field.radio("plan", [("free", "Free"), ("pro", "Pro")], label="Select Plan", value="free")
Other Fields
Field.file(name, label=None, required=False, accept=None, multiple=False, wrapper=None, **attrs)
Field.hidden(name, value, **attrs)
Field.color(name, label=None, value=None, wrapper=None, **attrs)
Labels and Wrappers
# No label - just input
Field.text("username")
# With label
Field.text("username", label="Username")
# → <label for="username">Username</label><input ...>
# With wrapper div
Field.text("username", label="Username", wrapper="form-field")
# → <div class="form-field"><label>...</label><input ...></div>
# Wrapper with attributes
Field.text("username", label="Username", wrapper={"cls": "form-group", "id": "field-1"})
With HTMX
Field.text("search",
placeholder="Search...",
hx_get="/search",
hx_trigger="keyup changed delay:300ms",
hx_target="#results"
)
HTMX Integration
NitroUI works seamlessly with HTMX. Use hx_* kwargs — underscores convert to hyphens automatically.
Basic Usage
from nitro_ui import Button, Div, Input, Script
# Include HTMX
Script(src="https://unpkg.com/htmx.org@2.0.4")
# hx_get → hx-get, hx_target → hx-target, etc.
Button("Load More", hx_get="/items", hx_target="#list", hx_swap="beforeend")
Common Patterns
# Click to load
Button("Load", hx_get="/content", hx_target="#result")
# Delete with confirmation
Button("Delete", hx_delete="/items/1", hx_confirm="Are you sure?", hx_swap="outerHTML")
# Live search
Input(
type="text",
name="q",
hx_get="/search",
hx_trigger="keyup changed delay:300ms",
hx_target="#results"
)
# Form submission
Form(
Input(type="text", name="email"),
Button("Subscribe"),
hx_post="/subscribe",
hx_swap="outerHTML"
)
# Infinite scroll
Div(
hx_get="/items?page=2",
hx_trigger="revealed",
hx_swap="afterend"
)
# Polling
Div(id="notifications", hx_get="/notifications", hx_trigger="every 30s")
All HTMX Attributes
| Python kwarg | HTML attribute | Description |
|---|---|---|
hx_get | hx-get | GET request |
hx_post | hx-post | POST request |
hx_put | hx-put | PUT request |
hx_patch | hx-patch | PATCH request |
hx_delete | hx-delete | DELETE request |
hx_target | hx-target | Target element selector |
hx_swap | hx-swap | How to swap content |
hx_trigger | hx-trigger | Event that triggers request |
hx_confirm | hx-confirm | Confirmation dialog |
hx_indicator | hx-indicator | Loading indicator |
hx_push_url | hx-push-url | Push URL to history |
hx_select | hx-select | Select content from response |
hx_select_oob | hx-select-oob | Out-of-band select |
hx_swap_oob | hx-swap-oob | Out-of-band swap |
hx_vals | hx-vals | Additional values (JSON) |
hx_boost | hx-boost | Boost all links/forms |
hx_include | hx-include | Include additional inputs |
hx_params | hx-params | Filter parameters |
hx_preserve | hx-preserve | Preserve element |
hx_ext | hx-ext | Extensions |
HTMX Extensions
# Server-Sent Events
Div(hx_ext="sse", sse_connect="/events", sse_swap="message")
# WebSockets
Div(hx_ext="ws", ws_connect="/ws")
# JSON encoding
Form(hx_ext="json-enc", hx_post="/api/submit")
Security
- Automatic HTML escaping: All text content and attribute values are escaped
- CSS value validation:
add_style/add_styles/CSSStyle.to_inline()rejectjavascript:,expression(),url(data:), CSS hex escapes, etc. (raisesValueError). Uses a single shared validation implementation. - CSS class name validation: StyleSheet rejects class names containing injection patterns
- Tag name validation: Tag names must match
^[a-zA-Z][a-zA-Z0-9-]*$. Invalid names raiseValueError. - Attribute key validation: Invalid attribute keys are skipped with a warning during rendering.
- Boolean attribute safety:
disabled=Falsecorrectly omits the attribute (browsers treat any present attribute as truthy). - None attribute safety: Attributes set to
Noneare omitted from output (not rendered as literal"None"). - No raw HTML injection: Cannot inject unescaped HTML (use
Partialfor trusted raw HTML) - Recursion protection:
render(),filter(),find_by_attribute(),to_dict(),from_dict()all havemax_depthparameter (default 1000) to prevent stack overflow from circular references - Serialization validation:
from_dict()validates all field types.StyleSheet.from_dict()validates class names. - Child type validation: Only
HTMLElementandstraccepted as children.Nonesilently ignored, other types raiseValueError. - Self-closing element warnings: Adding children or text to self-closing elements (e.g.
<img>,<input>) emits aUserWarning.
Gotchas
- Package name mismatch: PyPI is
nitro-ui, import isnitro_ui - Anchor vs Href:
Anchoris the preferred name for<a>tags.Hrefis a backward-compatible alias. Both work identically. - Link is for
<link>tags:Linkrenders<link>(stylesheet/meta). UseAnchor(orHref) for<a>links. childrenproperty returns a copy: Mutating the returned list does not affect the element. Useappend()/prepend()to modify.get_attributes()returns a copy: Mutating the returned dict does not affect the element. Useadd_attribute()to modify.replace_child()andgenerate_id()returnNone: Not chainable, unlike other methods.from_dict()expects normalized keys: Designed for round-tripping withto_dict(). Attribute keys should already be in their final form (e.g.data-value, notdata_value).- SVG attributes require snake_case kwargs: Use
view_boxnotviewBoxwhen constructing via kwargs. The camelCase form is used for storage and rendering.get_attribute("viewBox")to access. - Boolean attributes:
disabled=Truerenders as baredisabled,disabled=Falseomits it entirely.disabled="False"still renders (as a string value).
Quick Checklist
When generating NitroUI code:
- Use PascalCase imports:
from nitro_ui import Div, H1, Paragraph - Use
clsfor CSS classes (e.g.,Div(cls="container")) - Use
for_elementnotfor(Python keyword) - Use
Anchor(orHref) for<a>tags,Linkfor<link>tags - Children go as positional args, attributes as keyword args
- Call
.render()to get HTML string - Use
pretty=Truefor readable output during development - All manipulation methods return
selffor chaining (exceptreplace_child,generate_id) - Use
Fragmentwhen you need multiple elements without a wrapper - Use
Partialfor raw HTML (analytics, embeds) - bypasses escaping - Use
Component+Slotfor reusable components with declarative templates - Use
Field.xyz()for form fields with HTML5 validation attributes - Boolean attrs: pass
True/False, not strings, fordisabled,checked,required, etc. - SVG attrs: use snake_case kwargs (
view_box,preserve_aspect_ratio) - auto-converted to camelCase