How to Create Custom Air Tags for HTML Rendering: A Complete Guide

Create custom Air Tags by subclassing BaseTag, Transparent, or UnSafeTag from the feldroy/air repository, overriding __init__ to set defaults, and letting the automatic __init_subclass__ registration handle the rest.

Creating custom Air Tags for HTML rendering allows you to extend the Air framework with reusable, type-safe components that integrate seamlessly with Python's syntax. This guide covers the core architecture of the feldroy/air repository and provides practical examples for building everything from simple styled buttons to transparent layout containers.

Core Architecture of Air Tags

BaseTag and Automatic Registration

In src/air/tags/models/base.py, the BaseTag class serves as the foundation for all HTML elements. The __init_subclass__ method automatically registers every new subclass in a global registry when the class is defined, making it available for reconstruction via air.Tag.from_html without manual configuration.

Specialized Base Classes

Located in src/air/tags/models/special.py, these bases provide specific rendering behaviors for different use cases:

  • Transparent: Renders only children without producing a wrapper element.
  • UnSafeTag: Bypasses HTML escaping for raw content injection.
  • Tag/Tags/Children/Fragment: Convenient aliases for Transparent.

Creating a Simple Custom Air Tag

Subclassing Existing Tags

The most straightforward way to create custom Air Tags for HTML rendering is to subclass an existing tag like air.Button or air.Div. This preserves correct void/pair element behavior and attribute handling while allowing you to inject default values.

import air

class MyButton(air.Button):
    """A button with default CSS classes and optional type attribute."""
    
    def __init__(self, *children: air.Renderable, *, type_: str = "button", **attrs: air.AttributeType):
        default_attrs = {"class_": "my-button", "type_": type_}
        super().__init__(*children, **default_attrs | attrs)

Usage in Application Code

@app.page
def home():
    return air.Html(
        air.Body(
            air.H1("Welcome"),
            MyButton("Click Me", id_="cta"),
        )
    )

This renders:

<button class="my-button" type="button" id="cta">Click Me</button>

Creating Transparent Container Tags

For layout helpers that group children without generating a wrapper element, subclass Transparent from src/air/tags/models/special.py. This keeps the DOM clean while allowing you to enforce structure.

class Card(air.Transparent):
    """A card component that wraps content in a styled DIV."""
    
    def __init__(self, *children: air.Renderable, **attrs: air.AttributeType):
        container = air.Div(*children, class_="card", **attrs)
        super().__init__(container)

The Card class itself never appears in the final HTML—only the div class="card" wrapper does.

Bypassing HTML Escaping with UnSafeTag

To inject raw HTML without escaping, create a custom tag inheriting from UnSafeTag in src/air/tags/models/special.py.

class RawSvg(air.UnSafeTag):
    """Embeds SVG content without HTML escaping."""
    
    def __init__(self, svg_content: str):
        super().__init__(svg_content)

Usage:

svg = """<svg width="100" height="100"><circle cx="50" cy="50" r="40"/></svg>"""
page = air.Html(air.Body(RawSvg(svg)))

Warning: UnSafeTag performs no sanitization. Use only with trusted content to avoid XSS vulnerabilities.

Complete Example: Building a Reusable Card Component

Here is a comprehensive example combining multiple techniques:

import air

class Card(air.Transparent):
    """A reusable card with title and body sections."""
    
    def __init__(
        self,
        title: str,
        *body: air.Renderable,
        **attrs: air.AttributeType,
    ):
        header = air.H2(title, class_="card-title")
        content = air.Div(*body, class_="card-body")
        wrapper = air.Div(header, content, class_="card", **attrs)
        super().__init__(wrapper)

# Application usage

@app.page
def dashboard():
    return air.Html(
        air.Body(
            Card(
                "Statistics",
                air.P("Visitors: 1234"),
                air.P("Sign-ups: 56"),
            ),
            Card(
                "About",
                air.P("This app demonstrates custom Air tags."),
                class_="highlight",
            ),
        )
    )

This generates semantic HTML with proper nesting and CSS classes while maintaining clean Python syntax.

Key Source Files for Custom Tag Development

When creating custom Air Tags for HTML rendering, reference these files in the feldroy/air repository:

File Purpose
src/air/tags/models/base.py Contains BaseTag class and __init_subclass__ registration logic.
src/air/tags/models/special.py Defines Transparent, UnSafeTag, and container aliases.
src/air/tags/__init__.py Re-exports all built-in tags for easy importing.
examples/tags_render.py Reference implementation showing rendering patterns for built-in tags.

Best Practices for Custom Air Tags

  • Inherit from concrete tags when possible (e.g., air.Button instead of BaseTag) to preserve correct void/pair element behavior and attribute handling.

  • Use locals_cleanup when overriding __init__ to prevent internal variables from leaking into attribute dictionaries. Most built-in tags use locals_cleanup(locals()) to strip helper variables before passing to super().__init__.

  • Prefer Transparent for layouts when you need grouping logic without adding DOM noise, keeping the output HTML clean.

  • Reserve UnSafeTag for trusted content only—never use it with user input due to the lack of HTML escaping and potential XSS vulnerabilities.

  • Document your defaults in docstrings to maintain IDE autocomplete support and clarify the component interface for other developers.

Summary

  • Subclass BaseTag or specialized bases like air.Button to create custom Air Tags for HTML rendering with automatic registration via __init_subclass__ in src/air/tags/models/base.py.
  • Use Transparent from src/air/tags/models/special.py for container components that render only their children without generating wrapper elements.
  • Leverage UnSafeTag when you need to inject raw HTML without escaping, but only with trusted content to avoid security risks.
  • Reference key files: src/air/tags/models/base.py for core logic, src/air/tags/models/special.py for specialized behaviors, and examples/tags_render.py for rendering patterns.
  • Follow best practices: inherit from concrete tags, use locals_cleanup to prevent attribute leakage, and document your component interfaces.

Frequently Asked Questions

How do I register a custom Air Tag so it can be reconstructed from HTML?

Air Tags are automatically registered when you define the class thanks to the __init_subclass__ method in src/air/tags/models/base.py. Simply subclass BaseTag or any existing tag like air.Button, and the framework handles registration immediately. This enables reconstruction via air.Tag.from_html without requiring manual registry configuration.

What is the difference between BaseTag and Transparent when creating custom tags?

BaseTag in src/air/tags/models/base.py is the foundation for all HTML elements and renders a proper HTML tag with attributes and children. Transparent in src/air/tags/models/special.py is a specialized base that renders only its children without producing any wrapper element, making it ideal for layout helpers that need to group content without adding DOM noise.

How can I prevent HTML escaping in my custom Air Tag?

To bypass HTML escaping, inherit from UnSafeTag defined in src/air/tags/models/special.py. This base class skips the normal escaping logic used by BaseTag, allowing raw HTML strings to be emitted verbatim. Use this only with trusted content, as UnSafeTag performs no sanitization and can introduce XSS vulnerabilities if used with user input.

Where should I place my custom Air Tag definitions in my project?

Place custom tag definitions in a dedicated module within your project, such as myapp/tags.py or components/ui.py. Import them alongside the air module using import air; from myapp.tags import MyButton. Since registration happens automatically via __init_subclass__ in src/air/tags/models/base.py, no additional configuration is required as long as the module containing your custom tag is imported during application startup.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →