# Components URL: /docs/usage/components/ Section: usage Tags: usage, components, composition -------------------------------------------------------------------------------- Components Kida's component model gives you the composition patterns of React or Svelte — typed props, named slots, context propagation, error boundaries — without a build step or JavaScript runtime. Everything compiles to Python AST and renders server-side. This guide covers the blessed patterns for writing, organizing, and consuming components. Anatomy of a Component A Kida component is a {% def %} with typed parameters and slots: {% def card(title: str, variant: str = "default") %} <article class="card card--{{ variant }}"> <header class="card__header"> <h3>{{ title }}</h3> {% slot header_actions %} </header> <div class="card__body"> {% slot %} </div> {% if has_slot("footer") %} <footer class="card__footer"> {% slot footer %} </footer> {% end %} </article> {% end %} This single definition gives you: Typed props — title: str is validated by kida check --validate-calls Default values — variant falls back to "default" Named slots — header_actions and footer for targeted content injection Default slot — {% slot %} captures the primary body content Conditional rendering — has_slot("footer") only renders the footer wrapper when content is provided Using Components Inline Call (No Slots) For simple components that don't need body content: {{ card("Settings") }} Call Block (With Slots) Use {% call %} to pass slot content: {% call card("Settings", variant="elevated") %} {% slot header_actions %} <button class="btn btn--icon">Save</button> {% end %} <form> <label>Theme</label> <select name="theme"> <option>Light</option> <option>Dark</option> </select> </form> {% slot footer %} <button type="submit">Apply</button> {% end %} {% end %} Bare content inside {% call %} fills the default slot {% slot name %}...{% end %} inside {% call %} fills a named slot Unfilled slots produce no output Component Organization One Component Per File For shared components, define one primary component per file: templates/ components/ card.html button.html modal.html nav/ breadcrumbs.html sidebar.html {# components/button.html #} {% def button(label: str, variant: str = "primary", disabled: bool = false) %} <button class="btn btn--{{ variant }}" {% if disabled %}disabled{% end %}> {{ label }} </button> {% end %} Import and use: {% from "components/button.html" import button %} {% from "components/card.html" import card %} {% call card("User Profile") %} {{ button("Edit", variant="secondary") }} {% end %} Grouped Components When small components are logically related, group them in one file: {# components/forms.html #} {% def text_input(name: str, label: str, value: str = "", required: bool = false) %} <div class="form-field"> <label for="{{ name }}">{{ label }}</label> <input type="text" id="{{ name }}" name="{{ name }}" value="{{ value }}" {% if required %}required{% end %}> </div> {% end %} {% def select_input(name: str, label: str, options: list) %} <div class="form-field"> <label for="{{ name }}">{{ label }}</label> <select id="{{ name }}" name="{{ name }}"> {% for opt in options %} <option value="{{ opt.value }}">{{ opt.label }}</option> {% end %} </select> </div> {% end %} {% def checkbox(name: str, label: str, checked: bool = false) %} <label class="checkbox"> <input type="checkbox" name="{{ name }}" {% if checked %}checked{% end %}> {{ label }} </label> {% end %} Import selectively: {% from "components/forms.html" import text_input, select_input %} Discovering Components Use the CLI to list all components across your project: kida components templates/ # Filter by name kida components templates/ --filter card # Machine-readable output kida components templates/ --json Or use the Python API: template = env.get_template("components/card.html") for name, meta in template.def_metadata().items(): params = ", ".join( f"{p.name}: {p.annotation}" if p.annotation else p.name for p in meta.params ) print(f" def {name}({params})") if meta.slots: print(f" slots: {', '.join(meta.slots)}") Typed Props Type annotations on parameters serve as documentation and enable static validation: {% def badge(label: str, count: int, variant: str | None = none) %} <span class="badge {% if variant %}badge--{{ variant }}{% end %}"> {{ label }}: {{ count }} </span> {% end %} Supported Types Annotation Validates Literals str String literals int Integer literals float Float and integer literals bool true / false None none literal str | None Union types (PEP 604 style) list, dict, custom types Accepted as documentation; not validated statically Validation Enable compile-time checking with validate_calls=True on the Environment or via the CLI: # Catches: unknown params, missing required params, type mismatches on literals kida check templates/ --validate-calls components/card.html:14: type: badge() param 'count' expects int, got str ("five") Variable arguments are skipped — only literal values can be type-checked statically. Slot Patterns Default Slot Only For components with a single content area: {% def panel(title: str) %} <section class="panel"> <h2>{{ title }}</h2> {% slot %} </section> {% end %} Named Slots for Multi-Region Layout When a component has distinct content regions: {% def page_layout(title: str) %} <div class="layout"> <header> <h1>{{ title }}</h1> {% slot toolbar %} </header> <aside>{% slot sidebar %}</aside> <main>{% slot %}</main> <footer>{% slot footer %}</footer> </div> {% end %} Conditional Slots Use has_slot() to adapt markup based on whether content was provided: {% def alert(message: str, variant: str = "info") %} <div class="alert alert--{{ variant }}" role="alert"> <p>{{ message }}</p> {% if has_slot() %} <div class="alert__actions"> {% slot %} </div> {% end %} </div> {% end %} Scoped Slots Components can pass data back to the caller with let: bindings: {% def data_table(items: list) %} <table> {% for item in items %} <tr> {% slot row let:item=item let:index=loop.index %} <td>{{ item }}</td> {% end %} </tr> {% end %} </table> {% end %} {% call data_table(users) %} {% slot row %} <td>{{ item.name }}</td> <td>{{ item.email }}</td> {% end %} {% end %} See Scoped Slots for the full guide. Slot Forwarding When wrapping a component in another component, use {% yield %} to forward slots: {% def selection_bar() %}<nav>{{ caller() }}</nav>{% end %} {% def resource_index() %} {% call selection_bar() %} {% yield selection %} {% end %} {% end %} {% call resource_index() %} {% slot selection %}Badges{% end %} Cards {% end %} {% yield name %} renders the enclosing def's caller slot, even inside a nested {% call %} block. Context Propagation The Prop Drilling Problem Without context propagation, deeply nested components need every intermediate layer to pass props through: {# Every layer must forward "theme" — fragile and verbose #} {% def page_shell(theme: str) %} {{ sidebar(theme=theme) }} {{ content_area(theme=theme) }} {% end %} Provide / Consume Use {% provide %} to push state and consume() to read it anywhere in the subtree: {# Layout sets theme once #} {% provide theme = "dark" %} {% call page_shell() %} {% slot sidebar %} {{ sidebar_nav() }} {% end %} Main content here. {% end %} {% end %} {# sidebar_nav.html — reads theme without it being passed as a prop #} {% def sidebar_nav() %} <nav class="sidebar sidebar--{{ consume('theme') }}"> ... </nav> {% end %} consume("key") returns the nearest ancestor's provided value Nested {% provide %} blocks shadow outer values (like CSS custom properties) Works across slot boundaries, includes, and imported components See Provide / Consume for the full guide. Error Boundaries Wrap component calls in {% try %}...{% fallback %} to prevent one broken component from crashing the entire page: {% try %} {% call user_card(user) %} {{ render_activity_feed(user.id) }} {% end %} {% fallback error %} <div class="card card--error"> <p>Could not load user card.</p> </div> {% end %} When an error occurs inside the {% try %} block, Kida discards its partial output and renders the {% fallback %} block instead. The error variable contains the exception. This is especially useful for: Components that depend on external data (API calls, database queries) Third-party components you don't control Graceful degradation in production See Error Boundaries for the full guide. Component Styles with Push/Stack Use content stacks to co-locate CSS with the component that needs it: {# components/tooltip.html #} {% def tooltip(text: str) %} {% push "styles" %} <style> .tooltip { position: relative; display: inline-block; } .tooltip__text { visibility: hidden; position: absolute; z-index: 1; background: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 0.875rem; } .tooltip:hover .tooltip__text { visibility: visible; } </style> {% end %} <span class="tooltip"> {% slot %} <span class="tooltip__text">{{ text }}</span> </span> {% end %} {# base.html #} <head> <link rel="stylesheet" href="/css/main.css"> {% stack "styles" %} </head> The {% push "styles" %} block inside the component sends its CSS to the {% stack "styles" %} in the base layout. Styles are only included when the component is actually used. See Content Stacks for details. Introspection API Kida provides programmatic access to component metadata for framework authors and tooling: template = env.get_template("components/card.html") # List all defs print(template.list_defs()) # ["card"] # Full metadata meta = template.def_metadata() card = meta["card"] print(card.params) # (DefParamInfo(name='title', annotation='str', ...), ...) print(card.slots) # ('header_actions', 'footer') print(card.has_default_slot) # True print(card.depends_on) # frozenset() Block-Level Metadata For templates using {% block %} inheritance: meta = template.template_metadata() for name, block in meta.blocks.items(): if block.is_cacheable(): print(f"{name}: cache_scope={block.cache_scope}") See Static Analysis for the full analysis API. Quick Reference Pattern Syntax Use When Define component {% def name(props) %}...{% end %} Always Inline call {{ name(args) }} No body content needed Call with slots {% call name(args) %}...{% end %} Passing body/slot content Default slot (def) {% slot %} Single content area Named slot (def) {% slot name %} Multiple content regions Fill slot (call) {% slot name %}...{% end %} Providing named content Conditional slot has_slot() / has_slot("name") Adapt markup to slot presence Scoped slot {% slot let:x=expr %} Pass data back to caller Forward slot {% yield name %} Wrap component in another Context push {% provide key = val %} Avoid prop drilling Context read consume("key") Read ancestor-provided value Error boundary {% try %}...{% fallback %}...{% end %} Isolate failures Co-located styles {% push "styles" %}...{% end %} Component-scoped CSS Type annotation param: str Self-documenting + validation See Also Functions — Full {% def %} syntax reference Scoped Slots — Data-up pattern with let: bindings Provide / Consume — Context propagation Error Boundaries — {% try %}/{% fallback %} Content Stacks — {% push %}/{% stack %} Jinja2 vs Kida Components — Side-by-side comparison -------------------------------------------------------------------------------- Metadata: - Word Count: 1633 - Reading Time: 8 minutes