Components
PyWire components are .wire files that can be imported and used inside other pages or components. They follow standard Python import conventions, support props for data passing, and provide scoped styling.
Creating a Component
Section titled “Creating a Component”Any .wire file can be a component. Place your components in a directory (e.g., components/) to keep them organized.
---from pywire import props
@propsclass Props: name: str greeting: str = "Hello"---<div class="greeting"> <h2>{props.greeting}, {props.name}!</h2></div>
<style scoped> .greeting { padding: 1rem; border: 1px solid #e2e8f0; border-radius: 0.5rem; }</style>Importing Components
Section titled “Importing Components”Import components using Python’s standard dot notation. The class name matches the file name in PascalCase.
---from components.greeting import Greeting---<div> <Greeting name="Alice" /> <Greeting name="Bob" greeting="Welcome" /></div>Nested directories work naturally:
from lib.ui.buttons.primary_button import PrimaryButtonfrom shared.layouts.sidebar import SidebarProps define the data a component accepts from its parent. Use the @props decorator on a class to declare them.
---from pywire import props
@propsclass Props: title: str # Required prop count: int = 0 # Optional with default color: str = "blue" # Optional with default---<h1 style={f"color: {props.color}"}>{props.title} ({props.count})</h1>Props are accessed via the props object in templates and the Python block. See the API Reference for more details.
Attribute Spreading
Section titled “Attribute Spreading”Any attributes passed to a component that aren’t declared in @props are collected into attrs. Use {**attrs} to spread them onto an element:
<button class="btn" {**attrs}> {$render children}</button><!-- Usage: all extra attributes pass through to <button> --><Button type="submit" disabled={!valid} aria-label="Save"> Save</Button>Children and Named Snippets
Section titled “Children and Named Snippets”Every component receives an implicit children prop holding whatever markup the parent wrote between the tags. Render it with {$render children}:
<div class="card"> {$render children}</div><!-- Usage --><Card> <h2>Card Title</h2> <p>Card content goes here.</p></Card>For multiple named regions (header, footer, sidebar, etc.), define a named snippet in the parent and render it in the component. The paired form provides a fallback when the snippet isn’t supplied:
<div class="card"> <header>{$render header}Untitled{/render}</header> {$render children}</div><!-- Usage --><Card> {$snippet header}Product Details{/snippet} <p>Body content here.</p></Card>Snippets can also take parameters — useful for list rendering where the component owns the loop:
---from pywire import props
@propsclass Props: items: list---<ul> <li $for={item in props.items}> {$render row(item)} </li></ul><!-- Usage --><List items={todos}> {$snippet row(todo)} <input type="checkbox" checked={todo.done}> {todo.text} {/snippet}</List>Typing children and named snippets
Section titled “Typing children and named snippets”PyWire exposes two annotation aliases for snippet-valued props:
| Annotation | Meaning |
|---|---|
Snippet | Any snippet value. Most general. |
Snippet[A, B] | Snippet taking typed parameters (A, B). Carried through to tooling. |
Child | Single-child shorthand. Alias of Snippet — documents that the caller should pass exactly one fragment. |
Children | Many-children shorthand. Same runtime representation as Snippet. |
Children[n] / Children.of(min=, max=, n=) | Declares expected cardinality for editor tooling and future validation. |
---from pywire import propsfrom pywire import Snippet, Child, Children
@propsclass Props: children: Child # exactly one child header: Snippet # zero-arg snippet row: Snippet[dict] # called with one dict arg per row tabs: Children.of(min=1) # at least one tab---These are type hints first — they describe intent and are read by the LSP and prettier. Runtime cardinality enforcement for Children[...] is a planned follow-up; today the values themselves behave identically to Snippet.
Scoped Styles
Section titled “Scoped Styles”Add a <style scoped> block to any .wire file to scope CSS to that component. Scoped styles won’t leak to parent or sibling components.
<button class="btn">Click me</button>
<style scoped> .btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; } .btn:hover { background: #2563eb; }</style>Raw HTML ({$html})
Section titled “Raw HTML ({$html})”By default, all interpolated values are HTML-escaped. To render trusted HTML strings, use the {$html ...} directive:
---bio = "<strong>Alice</strong> is a developer."---<div>{$html bio}</div>[!CAUTION] Never use
{$html ...}with untrusted user input — it bypasses XSS protection.
Memoization
Section titled “Memoization”Components are memoized by default. A component’s template body only re-renders when:
- one of its props changes (by equality), or
- a wire it read during the previous render is written.
Unrelated wire updates elsewhere on the page don’t invalidate the component. Pure components compose freely without paying for re-render work they don’t need.
To opt a single call site out of memoization — for example to force a fresh render every cycle when the component reads impure values like datetime.now() or uuid.uuid4() — wrap the call in {$dynamic}:
<!-- Memoized: re-renders only when `count` changes --><Counter label="memo" initial={count.value} />
<!-- Bypassed: body re-runs every page render -->{$dynamic} <Counter label="always-fresh" initial={count.value} />{/dynamic}Memoization safety is a property of how a component is used, not how it’s defined — wrap at the call site, not the definition.
Component Refs
Section titled “Component Refs”Parents can get a reference to a child component using $ref and call any method decorated with @expose:
---from pywire import reffrom components.modal import Modal
modal_ref = ref()---<button @click={modal_ref.open()}>Open</button><Modal $ref={modal_ref}>Content</Modal>