# Getludic Ludic Ludic

> Ludic Components

- Skill: `tomevault-io/getludic-ludic-ludic` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/getludic-ludic-ludic`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/getludic-ludic-ludic/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/getludic-ludic-ludic

---


# Ludic Components

Ludic is a Python framework for building HTML pages with a typed, React-like component model that pairs with htmx. v1.x targets **Python 3.14+** and uses **PEP 750 t-strings** (template strings) for templating.

This skill helps you write idiomatic Ludic components: well-composed, type-safe under mypy (with the Ludic mypy plugin), safe by default, and legible. For wiring components into a web app (endpoints, request handling, URL generation, framework integrations), use the companion `ludic-web` skill.

## Mental model

Three layers, top to bottom:

1. **HTML elements** in `ludic.html` (`div`, `a`, `html`, `head`, `body`, `table`, `form`, ...). Each element has a typed signature describing its allowed children and attributes (e.g. `br()` accepts no children; `html(...)` requires `head` as the first child, `body` as the second).
2. **Components** in `ludic.components` — `Component[TChildren, TAttrs]` and the variadic `ComponentStrict[*TChildrenArgs, TAttrs]`. You subclass these and implement `render()`, which returns an element tree. Components are the unit of reuse and can carry CSS via class-level `classes` and `styles` declarations and read the current `Theme` via `self.theme`.
3. **Catalog** in `ludic.catalog` — opinionated widgets built on top of the core: layouts (`PageLayout`, `Stack`, `Cluster`, `Sidebar`), forms (`Form`, `FormField`), tables (`Table`, `TableHead`, `TableRow`), navigation, typography, buttons, messages, etc. Reach for these before composing layouts out of raw `div`s — they encode sensible defaults.

The rendering pipeline runs once when you call `.to_html()`: it walks children, expands any t-string `Template` instances, escapes untrusted text, formats attributes (aliases like `class_` → `class`, dict-style `style={...}`, list-style `classes=[...]`), and emits a single HTML string.

## The two things that surprise people

### 1. Use t-strings, not f-strings

In v1.x, code that mixes HTML elements with interpolated text uses **t-strings** (PEP 750), not f-strings:

```python
# correct — v1.x
div(t"Hello {b('World')}!")

# wrong — f-strings eagerly stringify b(...), losing escaping and child-tracking
div(f"Hello {b('World')}!")
```

The `t"..."` literal produces a `Template` object that Ludic's element constructor detects and expands. If you see `f"... {some_element} ..."` mixing HTML elements with text, change it to `t"..."`.

### 2. The trusted/untrusted content boundary

Everything is HTML-escaped by default. To opt out — for content you've already verified is safe HTML — wrap it in `Safe`:

```python
from ludic.types import Safe

div("Hello <b>World</b>").to_html()
# → '<div>Hello &lt;b&gt;World&lt;/b&gt;</div>'   (escaped — safe default)

div(Safe("Hello <b>World</b>")).to_html()
# → '<div>Hello <b>World</b></div>'              (raw — you take responsibility)
```

Only use `Safe` for content you control or have sanitized. **Never wrap user input in `Safe`** — that's the XSS hole this whole system exists to prevent. There's also `JavaScript(Safe)` for inline JS bodies.

## Writing a component

Two component classes:

- `Component[TChildren, TAttrs]` — accepts a variable number of children all of one type, plus a single `TAttrs` TypedDict for attributes.
- `ComponentStrict[*TChildrenArgs, TAttrs]` — uses a `TypeVarTuple` to enforce an exact positional sequence of child types (e.g. "exactly one `TableHead` followed by zero-or-more `TableRow`").

Use `ComponentStrict` when the shape of children matters structurally; use `Component` when children are a homogeneous list.

```python
from typing import override
from ludic import Attrs, Component
from ludic.html import a

class LinkAttrs(Attrs):
    to: str

class Link(Component[str, LinkAttrs]):
    classes = ["link"]                      # CSS classes added to the rendered root
    styles = {"a.link": {"color": "blue"}}  # collected by the style system

    @override
    def render(self) -> a:
        return a(
            *self.children,
            href=self.attrs["to"],
            style={"color": self.theme.colors.primary},
        )
```

A few rules the Ludic mypy plugin enforces — make sure your app's `pyproject.toml` (or `mypy.ini`) has `plugins = ["ludic.mypy_plugin"]`, otherwise you'll get false positives on every component:

- The generic parameters on `Component[...]` / `ComponentStrict[...]` synthesize `__init__`, so call sites get checked against `TChildren` and `TAttrs`.
- `Attrs` (and the per-element TypedDicts in `ludic.attrs`) describe **allowed** attributes. Unknown attributes are a type error.
- For HTML attribute names that collide with Python keywords or contain dashes, use the Python-side alias: `class_` → `class`, `for_` → `for`, `hx_get` → `hx-get`, etc. The `style=` attribute accepts a dict; `classes=` accepts a list.

## htmx attributes on elements

htmx attributes are exposed as `hx_*` keyword arguments on every element with `GlobalAttrs`:

```python
button(
    "Load more",
    hx_get="/items?page=2",
    hx_target="#items",
    hx_swap="beforeend",
)
```

When you serve the rendered HTML from an endpoint, htmx swaps it into the page. The endpoint side is covered in the `ludic-web` skill — including how to generate the URLs you pass to `hx_get` / `hx_post` safely.

## Catalog: prefer it to raw HTML

Before composing a layout out of `div`s, check `ludic.catalog`:

- `catalog.layouts` — `PageLayout`, `Stack`, `Cluster`, `Sidebar`, `Box`, `Center`, `Cover`, `Grid`, `Switcher`, `Frame`, `Reel`, `Imposter` (adapted from *Every Layout*).
- `catalog.forms` — `Form`, `FormField`, `InputField`, `TextAreaField`, `SelectField`, `CheckboxField`.
- `catalog.tables` — `Table`, `TableHead`, `TableRow`.
- `catalog.navigation`, `catalog.buttons`, `catalog.typography`, `catalog.messages`, `catalog.loaders`, `catalog.headers`, `catalog.items`, `catalog.lists`, `catalog.quotes`, `catalog.icons`, `catalog.pages`.

These are also the best place to learn idiomatic Ludic — they exercise the full surface area of the framework.

## Theming and styles

`Component` subclasses can declare:

- `classes: list[str]` — appended to the root element's class list at render time.
- `styles: GlobalStyles` — collected at startup so you can emit a single `<style>` block for the page.

Read the active theme via `self.theme` (returns the default theme if none is in scope). Themes flow down the tree automatically, so children see the same theme as the parent rendering them.

## A complete worked example

```python
from typing import override
from ludic import Attrs, Component
from ludic.catalog.layouts import Stack
from ludic.catalog.typography import Paragraph
from ludic.html import b, h2

class GreetingAttrs(Attrs):
    name: str

class Greeting(Component[str, GreetingAttrs]):
    classes = ["greeting"]

    @override
    def render(self) -> Stack:
        return Stack(
            h2(t"Hello, {b(self.attrs['name'])}!"),
            Paragraph(*self.children),
        )

# Usage
Greeting("Welcome to Ludic.", name="Ada").to_html()
```

What to notice:

- `Component[str, GreetingAttrs]` says: children are strings, attrs match `GreetingAttrs`.
- The t-string in `h2(t"Hello, {b(...)}!")` keeps `b(...)` as a real element — it isn't stringified.
- `self.attrs['name']` flows into `b(...)` and is HTML-escaped on output (because it isn't wrapped in `Safe`).
- `Stack` from the catalog handles vertical spacing — no need to roll a `div` with margin classes.

## When in doubt

- The [Ludic docs](https://getludic.dev/docs/) cover the public API end-to-end.
- The [catalog source on GitHub](https://github.com/getludic/ludic/tree/main/ludic/catalog) is the fastest read for idiomatic component patterns.
- The [examples folder](https://github.com/getludic/ludic/tree/main/examples) shows real components used in htmx patterns (click-to-edit, infinite scroll, lazy loading, file upload, ...).

---
> Source: [getludic/ludic](https://github.com/getludic/ludic) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-21 -->

