Building reusable UI or report components? This track covers{% def %}, {% call %}, named slots, scoped slots, and kida check --validate-callsso component mistakes fail before render time.

Tip

Duration: ~30 min | Prerequisite: Python 3.14+, basic Kida syntax

1

Components

Build reusable UI components with defs, slots, typed props, and context propagation

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 propstitle: str is validated by kida check --validate-calls
  • Default valuesvariant falls back to "default"
  • Named slotsheader_actions and footerfor targeted content injection
  • Default slot{% slot %}captures the primary body content
  • Conditional renderinghas_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)}")
    if meta.depends_on:
        print(f"    reads: {', '.join(sorted(meta.depends_on))}")

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 noneliteral
str | None Union types (PEP 604 style)
list, dict, custom types Accepted as documentation; not validated statically

Validation

Enable compile-time checking withvalidate_calls=Trueon the Environment or via the CLI:

# Catches: unknown params, missing required params, type mismatches on literals
kida check templates/ --validate-calls
pages/dashboard.html:8: K-CMP-001: Call to 'card' - unknown params: titl
components/card.html:14: K-CMP-002: type: badge() param 'count' expects int, got str ("five")

Variable arguments are skipped — only literal values can be type-checked statically.

Validation also follows literal component imports:

{% from "components/card.html" import card %}

{{ card(titl="Settings") }}

Kida reads card's metadata from components/card.html, validates the call in the importing template, and reports the error at the call site. Dynamic imports are skipped because the target cannot be known at check time.

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

Usehas_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 withlet: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 errorvariable 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

2

Functions

Define reusable template functions and macros

Functions

Define reusable template functions within your templates.

Defining Functions

Use{% def %}to create a function:

{% def greet(name) %}
    <p>Hello, {{ name }}!</p>
{% end %}

{{ greet("Alice") }}
{{ greet("Bob") }}

Parameters

Required Parameters

{% def button(text, url) %}
    <a href="{{ url }}" class="button">{{ text }}</a>
{% end %}

{{ button("Click Me", "/action") }}

Default Values

{% def button(text, url, style="primary") %}
    <a href="{{ url }}" class="button button-{{ style }}">{{ text }}</a>
{% end %}

{{ button("Save", "/save") }}
{{ button("Cancel", "/cancel", style="secondary") }}

Keyword Arguments

{% def card(title, content="", footer=none) %}
    <div class="card">
        <h3>{{ title }}</h3>
        {% if content %}
            <p>{{ content }}</p>
        {% end %}
        {% if footer %}
            <footer>{{ footer }}</footer>
        {% end %}
    </div>
{% end %}

{{ card(title="Hello", content="World") }}
{{ card("Title Only") }}

Typed Parameters

Parameters can carry optional type annotations following Python syntax:

{% def card(title: str, items: list, footer: str | None = none) %}
    <h3>{{ title }}</h3>
    {% for item in items %}<p>{{ item }}</p>{% end %}
    {% if footer %}<footer>{{ footer }}</footer>{% end %}
{% end %}

Annotations are optional per-parameter — you can mix typed and untyped:

{% def mixed(name: str, options, count: int = 0) %}
    ...
{% end %}

Supported Syntax

Syntax Meaning
x: str Simple type
x: int Simple type
x: list Generic without params
x: dict[str, int] Generic with params
x: str | None Union (PEP 604 style)
x: MyModel Custom type name

What Annotations Do

Annotations are documentation and validation hints, not enforced at runtime. The template engine does not performisinstancechecks. Their value is:

  1. Compile-time call-site validation — wrong parameter names are caught immediately whenvalidate_calls=Trueis set on the Environment
  2. IDE support — annotations flow into the generated Python code, enabling autocomplete in tooling
  3. Self-documenting — makes component interfaces explicit

Call-Site Validation

Enablevalidate_callson the Environment to catch parameter errors at compile time:

from kida import Environment

env = Environment(validate_calls=True)

# This emits a warning: 'titl' is not a param of 'card'
env.from_string("""
    {% def card(title: str) %}{{ title }}{% end %}
    {{ card(titl="oops") }}
""")

Validation checks:

  • Unknown parameters — keyword args not in the definition
  • Missing required parameters — params without defaults not provided
  • *args / **kwargsin the definition relax validation accordingly

See Static Analysis for the programmatic API.


Capturing Content (Default Slot)

Functions can capture call-block content usingcaller():

{% def wrapper(title) %}
    <section>
        <h2>{{ title }}</h2>
        <div class="content">
            {{ caller() }}
        </div>
    </section>
{% end %}

{% call wrapper("Section Title") %}
    <p>This content is passed to the wrapper.</p>
    <ul>
        <li>Item 1</li>
        <li>Item 2</li>
    </ul>
{% end %}

caller()without arguments reads the default slot.

Named Slots

Kida also supports named slots for multi-region components:

{% def card(title) %}
    <article>
        <h2>{{ title }}</h2>
        <div class="actions">{% slot header_actions %}</div>
        <div class="body">{% slot %}</div>
    </article>
{% end %}

{% call card("Settings") %}
    {% slot header_actions %}<button>Save</button>{% end %}
    <p>Body content.</p>
{% end %}

How it works:

  • {% slot %} in a defis the default placeholder.
  • {% slot name %} in a defis a named placeholder.
  • Inside{% call %}, use {% slot name %}...{% end %}to provide named slot content.
  • caller("name") retrieves a named slot from inside a def.

Slot Forwarding with{% yield %}

When composing macros, you often need to forward the outer caller's slot content into a nested{% call %}. The {% slot %} tag has context-dependent meaning: inside {% call %} it defines content (a SlotBlock), not a render reference. Use {% yield %}when you want to render the enclosing def's caller slot regardless of block context:

{% def selection_bar() %}<bar>{{ caller() }}</bar>{% end %}
{% def resource_index() %}
  {% call selection_bar() %}
    {% yield selection %}
  {% end %}
{% end %}
{% call resource_index() %}
  {% slot selection %}Badges{% end %}
  Cards
{% end %}
  • {% yield %} — render the caller's default slot (same as {% slot %}inside a def).
  • {% yield name %} — render the caller's named slot name.

{% yield %} is self-closing (no {% end %}) and always produces a render reference, even inside {% call %}blocks. It resolves to the nearest enclosing def's caller, regardless of nesting depth. No caller means no output (silent no-op).

When to use: Prefer{% yield %} over the double-nesting workaround {% slot x %}{% slot x %}{% end %}when forwarding slots through nested calls.

Slot Context Inheritance

Slot content is rendered in the caller's context. Variables from the page or render context are available in slot content without| default():

{% def form(action, method="get") %}
<form action="{{ action }}" method="{{ method }}">
    {% slot %}
</form>
{% end %}

{% block page_content %}
{% call form("/search") %}
    {{ search_field("q", value=q) }}
    {% if selected_tags %}
        {{ hidden_field("tags", value=selected_tags | join(",")) }}
    {% end %}
{% end %}
{% end %}

When render_block("page_content", q="...", selected_tags=["a","b"]) is called, q and selected_tags are available inside the form slot because the slot body inherits the caller's render context. This works for both render() and render_block().

Slot Detection

When a function is called via{% call %}, it receives slot content accessible through caller(). Use the built-in has_slot() helper inside a {% def %}body to detect whether any call slot content was provided:

{% def card(title) %}
    <div class="card">
        <h3>{{ title }}</h3>
        {% if has_slot() %}
            <div class="card-body">
                {{ caller() }}
            </div>
        {% end %}
    </div>
{% end %}

When called directly, has_slot() returns false:

{{ card("Simple Card") }}
{# Output: <div class="card"><h3>Simple Card</h3></div> #}

When called with {% call %}, has_slot() returns true:

{% call card("Rich Card") %}
    <p>This content appears in the card body.</p>
    <button>Action</button>
{% end %}
{# Output includes the card-body wrapper #}

This pattern is useful for components that should adapt their markup depending on whether slot content is provided, for example rendering a wrapper <div>only when there is something to wrap.

Scoped Slots

Slots can also pass data back up to the caller usinglet:bindings. See [Scoped Slots](

docs/advanced/scoped-slots

) for the full guide.


Islands Wrapper Pattern

Functions are a good fit for reusable island mount wrappers in server-rendered apps:

{% def island_shell(name, attrs="") %}
<section{{ attrs }}>
    <div class="island-fallback">
        {% slot %}
    </div>
</section>
{% end %}

{% call island_shell("editor", attrs=island_attrs("editor", {"doc_id": doc.id}, "editor-root")) %}
    <p>Server-rendered fallback editor UI.</p>
{% end %}

Guidelines:

  • Keep fallback slot content usable without JavaScript.
  • Pass serialized props via helper globals (avoid manual inline JSON).
  • Prefer stable mount IDs for deterministic remount behavior.
  • Use explicit wrapper signatures when creating state primitives:
{% def grid_state_shell(state_key, columns, attrs="") %}
<section{{ attrs }}>
    {% slot %}
</section>
{% end %}

Regions

Regions are parameterized blocks that work as both blocks (forrender_block()) and callables (for {{ name(args) }}). Use them when you need parameterized fragments for HTMX partials, OOB updates, or layout composition.

Syntax

{% region name(param1, param2=default) %}
    ...body...
{% end %}

{{ name(value1, value2) }}

Block and Callable

A region compiles to both:

  • A block — calltemplate.render_block("name", param1=..., param2=...)
  • A callable — use{{ name(args) }}in the template body
{% region sidebar(current_path="/") %}
  <nav>{{ current_path }}</nav>
{% end %}

{% block content %}
  {{ sidebar(current_path="/about") }}
{% end %}
# From Python: render the region as a block
html = template.render_block("sidebar", current_path="/settings")

Outer Context

Region bodies can read variables from the outer render context (not just parameters):

{% region crumbs(current_path="/") %}
{{ breadcrumb_items | default([{"label":"Home","href":"/"}]) | length }}
{% end %}

{{ crumbs(current_path="/x") }}

When render_block("crumbs", ...) or {{ crumbs(...) }} is called, the region receives its params plus the caller's context. breadcrumb_itemscomes from the outer context.

Region default expressions

Optional parameters can use any expression as a default, not just simple variable names. Defaults are evaluated at call time from the caller's context:

{% region sidebar(section, meta=page.metadata) %}
  <nav>{{ meta.title }}</nav>
{% end %}

{% region stats(count=items | length) %}
  {{ count }} items
{% end %}

{% region header(title=page?.title ?? "Default") %}
  <h1>{{ title }}</h1>
{% end %}

Supported expressions include:

  • Simple namescurrent_page=page(zero-overhead inline lookup)
  • Attribute accessmeta=page.metadata
  • Filterscount=items | length
  • Optional chainingtitle=page?.title ?? "Default"
  • Null coalescingmeta=data?.info ?? {}

Static analysis (depends_on) correctly captures context paths from complex defaults for incremental build and cache scope inference.

Regions vs Defs

Use case Region Def
render_block() ✅ Yes — region is a block ❌ No — def is not a block
{{ name(args) }} ✅ Yes ✅ Yes
Slots /{% call %} ❌ No ✅ Yes
Outer-context access ✅ Yes ✅ Yes (via caller context)
Framework OOB discovery meta.regions() ❌ N/A

Use regions when you need parameterized blocks forrender_block(), HTMX OOB, or framework layout composition. Use defs when you need slots, {% call %}, or component composition.

Framework Integration

Frameworks like Chirp usetemplate_metadata().regions() to discover OOB regions at build time. Each region's BlockMetadata includes is_region, region_params, and depends_onfor cache scope inference. See Framework Integration.


Coming from Jinja2? (Macros)

Kida does not support{% macro %} / {% endmacro %}. Use {% def %} with unified {% end %}:

{# Kida — equivalent to Jinja2 {% macro %} #}
{% def input(name, value="", type="text") %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% end %}

{{ input("username") }}
{{ input("password", type="password") }}

Attempting {% macro %} raises K-PAR-001: Unknown block keyword: macro.

Importing Functions

Import functions from other templates:

{% from "macros.html" import button, input %}

{{ button("Submit", "/submit") }}
{{ input("email", type="email") }}

Import all with a namespace:

{% import "forms.html" as forms %}

{{ forms.input("name") }}
{{ forms.textarea("bio") }}

Best Practices

Single Responsibility

Each function should do one thing:

{# Good: Single purpose #}
{% def user_avatar(user, size=32) %}
    <img src="{{ user.avatar_url }}"
         alt="{{ user.name }}"
         width="{{ size }}"
         height="{{ size }}">
{% end %}

{# Avoid: Too much logic #}
{% def user_card_with_everything(user, show_bio, show_posts, ...) %}
    ...
{% end %}

Descriptive Names

{# Good: Clear purpose #}
{% def format_price(amount, currency="USD") %}
{% def user_badge(role) %}
{% def pagination_nav(current, total) %}

{# Avoid: Vague names #}
{% def render(x) %}
{% def do_thing(item) %}

Macro vs Context Variable Naming

When importing macros that render context variables, use different names so the macro does not shadow the variable. Prefer verb-prefixed names for macros and noun-like names for context variables:

{# Good: Macro and variable have different names #}
{% from "_route_tabs.html" import render_route_tabs %}
{% if route_tabs | default([]) %}
    {{ render_route_tabs(route_tabs, current_path) }}
{% end %}

{# Avoid: Same name causes shadowing — route_tabs may resolve to the macro #}
{% from "_route_tabs.html" import route_tabs %}
{% if route_tabs | default([]) %}  {# When route_tabs not in context, this is the macro (truthy) #}
    {{ route_tabs(route_tabs, current_path) }}  {# Passes macro as first arg → "not iterable" #}
{% end %}
Use for Naming Examples
Macros Verb-prefixed render_route_tabs, format_date, render_nav
Context variables Noun-like route_tabs, items, skills

If you seeCannot iterate over macro 'X', a macro is shadowing a context variable. Rename the macro (e.g. render_X) to avoid the collision.

See Also

3

Scoped Slots

Pass data from components back to their callers with let bindings

Scoped Slots

Scoped slots let a component expose data up to the caller. The component defines what data is available; the caller decides how to render it.

This is the same pattern as Svelte'slet:directive or Vue's scoped slots.

Basic usage

A component provides data withlet:name=expr on {% slot %}:

{# components/user-list.html #}
{% def user_list(users) %}
<ul>
  {% for user in users %}
    <li>{% slot let:item=user %}{{ item }}{% end %}</li>
  {% end %}
</ul>
{% end %}

The caller receives that data with let:name on {% call %}:

{% from "components/user-list.html" import user_list %}

{% call(let:item) user_list(users=people) %}
  <strong>{{ item.name }}</strong> &mdash; {{ item.email }}
{% end %}

The component iterates the data. The caller controls the markup. Neither is coupled to the other.

How it works

Side Syntax Purpose
Component (def) {% slot let:name=expr %} Push data to the caller
Caller (call) {% call(let:name) fn() %} Receive the data

Thelet:bindings flow up from the slot to the caller's block. Inside the {% call %}...{% end %}body, the bound names are available as local variables.

Multiple bindings

A slot can expose more than one value:

{% def data_table(rows) %}
<table>
  {% for row in rows %}
    <tr>{% slot let:item=row, let:index=loop.index0 %}{% end %}</tr>
  {% end %}
</table>
{% end %}
{% call(let:item, let:index) data_table(rows=data) %}
  <td>{{ index }}</td>
  <td>{{ item.name }}</td>
  <td>{{ item.value }}</td>
{% end %}

Expressions in bindings

Thelet:value can be any expression -- attribute access, filters, function calls:

{% slot let:label=item.name | upper, let:active=item.is_active %}
  {{ label }}
{% end %}

Default content

When the slot haslet:bindings and a body, that body is the default content -- rendered when no caller provides a block:

{% def card(user) %}
<div class="card">
  {% slot let:item=user %}
    {# Default: simple name display #}
    <span>{{ item.name }}</span>
  {% end %}
</div>
{% end %}

Called without a block, the default renders:

{{ card(user=alice) }}
{# <div class="card"><span>Alice</span></div> #}

Called with a block, the caller takes over:

{% call(let:item) card(user=alice) %}
  <img src="{{ item.avatar }}"> <b>{{ item.name }}</b>
{% end %}

Named slots

Scoped slots work with named slots too:

{% def layout(page) %}
<header>{% slot header %}<h1>{{ page.title }}</h1>{% end %}</header>
<main>{% slot body let:data=page.content %}{{ data }}{% end %}</main>
{% end %}
{% call(let:data) layout(page=p) %}
  {% slot header %}<h1 class="fancy">{{ p.title }}</h1>{% end %}
  <div class="prose">{{ data | markdown }}</div>
{% end %}

With provide / consume

Scoped slots andprovide/consumesolve different problems and coexist cleanly:

Pattern Direction Use case
Scoped slots (let:) Child to parent (up) Expose iteration data, computed values
provide/consume Parent to child (down) Theme, config, implicit context

They can be used together:

{% def themed_list(items) %}
{% provide theme = "dark" %}
<ul>
  {% for item in items %}
    <li>{% slot let:item=item %}{{ item }}{% end %}</li>
  {% end %}
</ul>
{% endprovide %}
{% end %}

{% call(let:item) themed_list(items=data) %}
  <span class="{{ consume("theme") }}">{{ item.name }}</span>
{% end %}

Real-world example: sortable table

{# components/sortable-table.html #}
{% def sortable_table(rows, columns) %}
<table>
  <thead>
    <tr>
      {% for col in columns %}
        <th>{{ col.label }}</th>
      {% end %}
    </tr>
  </thead>
  <tbody>
    {% for row in rows %}
      <tr>{% slot let:row=row, let:cols=columns %}
        {% for col in cols %}
          <td>{{ row[col.key] }}</td>
        {% end %}
      {% end %}</tr>
    {% end %}
  </tbody>
</table>
{% end %}

Default rendering works out of the box. Callers override when they need custom cell rendering:

{% call(let:row, let:cols) sortable_table(rows=users, columns=cols) %}
  <td>{{ row.name }}</td>
  <td>{{ row.email }}</td>
  <td>
    {% if row.active %}
      <span class="badge badge-green">Active</span>
    {% else %}
      <span class="badge badge-gray">Inactive</span>
    {% end %}
  </td>
{% end %}
4

Template Type Checking

Annotate expected context types with template declarations for compile-time validation

Template Type Checking

Kida can validate template variables against{% template %}declarations at compile time — catching typos, missing variables, and undeclared context access before any template is rendered.

Template Declarations

Add a{% template %}block at the top of a template to declare the expected context variables:

{% template page: Page, site: Site %}

<h1>{{ page.title }}</h1>
<p>Published on {{ site.name }}</p>

When a {% template %} declaration is present, the type checker validates that every variable used in the template is either declared, locally defined, or a built-in name. Templates without a {% template %}declaration are unconstrained — no type checking is performed.

Supported Types

The type checker validates variable names, not Python types. The type annotations in declarations (e.g.,Page, Site) serve as documentation. What the checker enforces is that every context variable accessed in the template appears in the declaration list.

Built-In Names

The following names are always available and never need to be declared:

Category Names
Python builtins range, len, str, int, float, bool, list, dict, set, tuple, min, max, sum, abs, round, sorted, reversed, enumerate, zip, map, filter, any, all, hasattr, getattr, isinstance, type
Boolean/None literals true, false, none, True, False, None
Template globals loop, caller, super
HTMX helpers hx_request, hx_target, hx_trigger, hx_boosted
Security tokens csrf_token, csp_nonce

Locally Defined Names

Variables introduced by template constructs do not need declaration. The type checker tracks names created by:

Construct Example
{% set %} {% set total = items | length %}
{% let %} {% let count = 0 %}
{% export %} {% export title = "Home" %}
{% capture %} {% capture sidebar %}...{% endcapture %}
{% for %} {% for item in items %} (binds item)
{% with %} {% with x = 1, y = 2 %}
{% def %} {% def button(text, url) %} (binds button, text, url)
{% import %} {% import "macros.html" as macros %}
{% from ... import %} {% from "macros.html" import button %}

The checker respects scoping: variables bound inside{% for %}, {% with %}, and {% def %}blocks are only visible within those blocks.

Usage

Python API

from kida.analysis.type_checker import check_types

# Parse and compile a template with a {% template %} declaration
template = env.get_template("page.html")

# Run the type checker against the AST
issues = check_types(template._optimized_ast)

for issue in issues:
    print(f"Line {issue.lineno}: [{issue.rule}] {issue.message}")

The check_types function accepts a parsed Template AST node and returns a list of TypeIssue findings sorted by line number. If the template has no {% template %}declaration, it returns an empty list.

CLI

kida check --typed templates/

For component prop validation, use:

kida check --validate-calls templates/

This checks calls to local {% def %} components and literal {% from "..." import ... %} components. It reports unknown keyword arguments, missing required props, and literal type mismatches with component error codes such as K-CMP-001 and K-CMP-002.

Error Messages

The type checker produces three categories of findings:

undeclared-var

A variable is used in the template but does not appear in the{% template %}declaration and is not locally defined or built-in.

Line 12: Variable 'author' used but not declared in {% template %}

Fix: Add the variable to the{% template %}declaration:

{% template page: Page, site: Site, author: Author %}

unused-declared

A variable is declared in{% template %}but never referenced anywhere in the template.

Line 1: Declared variable 'sidebar' is never used

Fix: Remove the unused variable from the declaration, or add usage in the template body.

typo-suggestion

A variable is undeclared, and its name is similar to a declared variable (edit distance of 1, or matching prefix). The checker suggests the likely intended name.

Line 8: Variable 'titl' used but not declared in {% template %} (did you mean 'title'?)

The typo detector uses two heuristics:

  • Prefix matching — the first 3 characters of the used name match a declared name
  • Edit distance — the used name differs from a declared name by exactly one character (insertion, deletion, or substitution)

TypeIssue

Each finding is aTypeIssuedataclass:

Field Type Description
lineno int Line number of the issue
col_offset int Column offset of the issue
rule str One of"undeclared-var", "unused-declared", "typo-suggestion"
message str Human-readable description
severity str "warning" (default) or "error"

Examples

Catching a typo

{% template user: User, items: list %}

<h1>{{ usr.name }}</h1>
<ul>
  {% for item in items %}
    <li>{{ item.title }}</li>
  {% endfor %}
</ul>
Line 3: Variable 'usr' used but not declared in {% template %} (did you mean 'user'?)

Catching an undeclared variable

{% template page: Page %}

<h1>{{ page.title }}</h1>
<p>{{ site.description }}</p>
Line 4: Variable 'site' used but not declared in {% template %}

Detecting unused declarations

{% template page: Page, nav_items: list, footer: dict %}

<h1>{{ page.title }}</h1>
Line 1: Declared variable 'nav_items' is never used
Line 1: Declared variable 'footer' is never used

Limitations

  • The type checker validates variable names only. It does not check that runtime values match the annotated types (e.g., it does not verify thatpage is actually a Pageobject).
  • Typo suggestions use simple heuristics (prefix matching and edit distance of 1). Longer or more creative misspellings may not produce suggestions.
  • Analysis is conservative for unused-declared checks: if a declared variable is only used inside a conditional branch, it is still counted as used.

See Also

5

CLI Reference

Command-line tools for checking, rendering, formatting, inspecting, and generating with Kida templates

CLI Reference

Kida ships eight subcommands:check, render, fmt, components, readme, extract, manifest, and diff. All are available through the kida entry point or python -m kida.

kida <command> [options]

Contract Status

The public CLI contract is the set of subcommands and flags documented here. Output text can become clearer, but machine-readable JSON shapes for components --json, readme --json, and manifestshould only change deliberately with docs and changelog updates when behavior changes.

kida check

Parse all.htmltemplates under a directory. Reports syntax errors, loader resolution failures, and optional lint checks.

kida check <template_dir> [flags]

Positional argument:

Argument Description
template_dir Root directory passed toFileSystemLoader. All *.htmlfiles are scanned recursively.

Flags:

Flag Description
--strict Fail on bare{% end %} closers. Requires explicit {% endif %}, {% endblock %}, {% endcall %}, etc.
--validate-calls Validate macro call sites against{% def %}signatures. Reports unknown parameters and missing required parameters. Duplicate keyword arguments are rejected earlier as parser errors.
--a11y Check templates for accessibility issues (missingaltattributes, heading order, etc.).
--typed Type-check templates against{% template %}declarations.
--lint-fragile-paths Suggest./relative paths for same-folder include, extends, embed, and import statements so folder moves stay zero-edit.

Examples

Basic syntax check:

kida check templates/

Strict mode with call validation:

kida check templates/ --strict --validate-calls

Full lint pass (all checks enabled):

kida check templates/ --strict --validate-calls --a11y --typed --lint-fragile-paths

Output format

Errors and warnings print to stderr, one per line:

layouts/base.html: unexpected tag 'endblock'
partials/nav.html:12: strict: unified {% end %} closes 'if' — prefer {% endif %}
components/card.html:8: a11y/img-alt [WARNING]: <img> missing alt attribute

--validate-calls diagnostics use stable K-CMP-*codes:

components/page.html:12: K-CMP-001: Call to 'card' — missing required: title
components/page.html:18: K-CMP-002: type: card() param 'count' expects int, got str ('many')

kida render

Render a single template to stdout. Supports HTML and terminal rendering modes.

kida render <template> [flags]

Positional argument:

Argument Description
template Path to the template file to render.

Flags:

Flag Default Description
--data FILE none JSON file providing template context variables.
--data-str JSON none Inline JSON string providing template context variables.
--mode {html,terminal,markdown} html Rendering mode.terminalenables ANSI styling and width-aware layout.
--width INT auto Override terminal width (terminal mode only).
--color {none,basic,256,truecolor} auto Override color depth (terminal mode only).
--data-format {json,junit-xml,sarif,lcov} json Format of the data file.
--set KEY=VALUE none Set template variables (repeatable). Values are parsed as JSON if valid, otherwise kept as strings.
--explain off Show which compile-time optimizations were applied.
--stream off Progressive output: reveal template chunks with a brief delay.
--stream-delay SECONDS 0.02 Delay between stream chunks. Requires--stream.

Examples

Render with inline data:

kida render page.html --data-str '{"title": "Hello"}'

Render from a JSON file in HTML mode:

kida render page.html --data context.json --mode html

Terminal mode with explicit width and color:

kida render dashboard.html --width 120 --color 256

Streaming output:

kida render report.html --data stats.json --stream --stream-delay 0.05

kida fmt

Auto-format Kida template files. Accepts individual files or directories (scans for*.htmlrecursively).

kida fmt <paths...> [flags]

Positional argument:

Argument Description
paths One or more files or directories to format.

Flags:

Flag Default Description
--indent INT 2 Spaces per indentation level.
--check off Check formatting without writing changes. Exits1if any file would be reformatted.

Examples

Format all templates in a directory:

kida fmt templates/

Format specific files with 4-space indent:

kida fmt layouts/base.html partials/nav.html --indent 4

CI check (no writes, non-zero exit on drift):

kida fmt templates/ --check

kida components

List all{% def %}components across templates in a directory. Useful for auditing component libraries and generating documentation.

kida components <template_dir> [flags]

Positional argument:

Argument Description
template_dir Root directory passed toFileSystemLoader. All *.htmlfiles are scanned recursively.

Flags:

Flag Default Description
--json off Output as JSON for machine consumption.
--filter NAME none Filter components by name (case-insensitive substring match).

Examples

List all components:

kida components templates/

Filter by name:

kida components templates/ --filter card

Machine-readable output:

kida components templates/ --json

Output format

Human-readable output groups by template file:

components/card.html
  def card(title: str, variant: str = ...)
    slots: (default), actions

components/nav.html
  def nav_link(href: str, label: str)

2 component(s) found.

JSON output produces an array of objects withname, template, lineno, params, slots, has_default_slot, depends_on, vararg, and kwarg fields. Each param includesname, annotation, has_default, and required.

kida manifest

Render templates with capture instrumentation and emit a render manifest as JSON. Frameworks can use this to track rendered block fragments and context keys.

kida manifest <template_dir> [flags]

Positional argument:

Argument Description
template_dir Root directory passed toFileSystemLoader. All *.html and *.kidafiles are scanned recursively.

Flags:

Flag Default Description
-o, --output FILE stdout Write manifest JSON to a file.
--data FILE none JSON object mapping template names to context dictionaries.
--search off Output a search manifest instead of the raw capture manifest.

kida diff

Compare two render manifests and report added, removed, and changed fragment content hashes.

kida diff <old_manifest> <new_manifest>

Positional arguments:

Argument Description
old_manifest Path to the previous manifest JSON file.
new_manifest Path to the new manifest JSON file.

kida readme

Auto-generate a README from project metadata. Detects project structure frompyproject.toml, filesystem, and git, then renders a styled markdown README using Kida's own template engine.

kida readme [root] [flags]

Positional argument:

Argument Default Description
root .(current directory) Project root directory to scan.

Flags:

Flag Default Description
-o, --output FILE stdout Write to file instead of stdout.
--preset {default,minimal,library,cli} auto-detected Built-in template preset. Auto-detected from project type if not specified.
--template FILE none Path to a custom Kida template (overrides--preset).
--set KEY=VALUE none Override detected values (repeatable). Value is parsed as JSON, falls back to string.
--depth INT 2 Directory tree depth for project scanning.
--json off Dump auto-detected context as JSON instead of rendering.

Examples

Generate a README for the current project:

kida readme

Write to a file with a specific preset:

kida readme -o README.md --preset library

Override detected values:

kida readme --set description="A fast template engine" --set license=MIT

Inspect detected metadata:

kida readme --json

Use a custom template:

kida readme --template .github/readme.kida -o README.md

Presets

Preset Best For
default General projects with standard structure
minimal Small projects or packages
library Python libraries with API documentation focus
cli CLI tools with command documentation focus

Python API

from kida.readme import detect_project, render_readme

# Auto-detect metadata
ctx = detect_project(root_path, depth=2)

# Render with a preset
md = render_readme(root_path, preset="library")

# Render with custom template and overrides
md = render_readme(
    root_path,
    template=Path("custom.kida"),
    context={"description": "Override"},
)

kida extract

Extract translatable messages from templates into a.pot(PO Template) file for internationalization workflows.

kida extract <template_dir> [flags]

Positional argument:

Argument Description
template_dir Root directory to scan for templates.

Flags:

Flag Default Description
-o, --output FILE stdout Write output to file instead of stdout.
--ext .EXT .html .kida .txt .xml File extensions to scan (repeatable).

Examples

Extract messages to stdout:

kida extract templates/

Write to a .potfile:

kida extract templates/ -o messages.pot

Scan only .html and .kidafiles:

kida extract templates/ --ext .html --ext .kida

Exit Codes

Code Meaning
0 Success. No errors or formatting drift.
1 One or more problems found (check), render failure, or formatting drift (fmt --check).
2 Invalid input: path not found, bad JSON data, or unknown command.

See Also

✓ Track Complete