# Write Cicadel components with a React mental model

Start here when writing UI. Use [the component reference](https://cicadel.fauzaanu.com/en/dev/topics/component-ui/index.md) for the
full contract, [Tailwind](https://cicadel.fauzaanu.com/en/dev/howto/tailwind/index.md) for styling, and
[`$cicadel-components`](https://cicadel.fauzaanu.com/en/dev/sources/skills/cicadel-components/SKILL.md) as the reusable agent skill.
The examples below are copied into a generated app and executed by
`TestComponentAuthoringGuide`; they are not pseudocode.

## Ask your agent

```text
Read https://cicadel.fauzaanu.com/en/dev/howto/component-authoring/index.md.
Add [screen] to my application using typed Cicadel components.
Inspect the current app structure, compose the page from explicit props and slots,
and connect its handler and route. Build the UI and test its rendered output.
```

## Translate what you know

| React habit | Cicadel equivalent |
| --- | --- |
| Function component with typed props | Private `.templ` function; public `ui.Definition[Props, Slots]` contract |
| `<Card title="Hello">…</Card>` | `@Card.Call(CardProps{Title: "Hello"}, CardSlots{Children: ...})` |
| `children` / a ReactNode prop | `ui.Content` fields named `Children`, `Footer`, etc. |
| Fragment `<>…</>` | Private markup helper via `ui.Fragment(helper(...))`, or `ui.Group(...)` |
| `{title}` | `{ p.Title }` (text escapes automatically) |
| `{items.map(...)}` | A multiline Go `for` block |
| Conditional JSX | A multiline Go `if` or `switch` block |
| Default/destructured props | Go zero values plus a pure `Defaults` function |
| `className`, `htmlFor` | HTML `class`, `for` |
| Boolean HTML prop | `disabled?={ p.Disabled }` |
| Utility classes | Tailwind v4, complete class strings in source |
| Context provider / hook state | Explicit typed inputs; handlers load data and process POSTs |
| `onClick` / hydration | Not part of this server engine; use links/forms for server interactions |
| `key` | No reconciliation runtime, so no key prop; keep DOM IDs unique for labels |

Think “props down, markup composed, HTML returned.” Cicadel borrows composition,
not React's JavaScript runtime. Do not introduce JSX, hooks, npm or a browser CDN
script to author these components. Tailwind uses a pinned standalone build tool.

## A complete card and list page

Generate a project with module path `example.com/componentguide`, run `go mod tidy`,
and install the CSS compiler once with `go tool cicadel ui css install`. Add the
following files. Their package is an ordinary local Go app; no new module is needed.

### Public contract: apps/dashboard/card.go

<!-- file: apps/dashboard/card.go -->
```go
package dashboard

import (
    "fmt"
    "github.com/fauzaanu/cicadel/ui"
)

type CardProps struct {
    Title string
    Tone string
}
type CardSlots struct { Children, Footer ui.Content }

var Card = ui.Definition[CardProps, CardSlots]{
    Name: "dashboard:card",
    Source: "apps/dashboard/card.templ",
    Defaults: func(p CardProps) CardProps {
        if p.Tone == "" { p.Tone = "neutral" }
        return p
    },
    Validate: func(p CardProps, s CardSlots) error {
        if err := ui.Required("Title", p.Title); err != nil { return err }
        if s.Children.Empty() { return fmt.Errorf("Children slot is required") }
        if p.Tone != "neutral" && p.Tone != "success" {
            return fmt.Errorf("unknown Tone %q", p.Tone)
        }
        return nil
    },
    Body: card,
}

func cardClasses(tone string) string {
    if tone == "success" { return "rounded-xl border border-emerald-300 bg-emerald-50 p-6" }
    return "rounded-xl border border-slate-200 bg-white p-6"
}
```

Zero `Tone` means neutral; zero `Title` or absent children is an error. Footer is
optional. False/zero are not universally “missing.” Use pointer props only when
omission must differ from a supplied false/zero. Defaults must not mutate objects
referenced by caller-owned maps, slices, or pointers.

### Markup: apps/dashboard/card.templ

<!-- file: apps/dashboard/card.templ -->
```templ
package dashboard

templ card(p CardProps, s CardSlots) {
    <section class={ cardClasses(p.Tone) }>
        <h2 class="text-xl font-semibold text-slate-900">{ p.Title }</h2>
        <div class="mt-3 text-slate-700">@s.Children</div>
        if !s.Footer.Empty() {
            <footer class="mt-4 text-sm text-slate-500">@s.Footer</footer>
        }
    </section>
}
```

`@s.Children` inserts a typed fragment. `{ p.Title }` inserts escaped text. They
are deliberately different operations. Use complete literal utility names in Go
variants; Tailwind cannot evaluate `"bg-" + color + "-50"`.

### Composition: apps/dashboard/page.templ

<!-- file: apps/dashboard/page.templ -->
```templ
package dashboard

import "github.com/fauzaanu/cicadel/ui"
import "github.com/fauzaanu/cicadel/ui/layout"

templ itemFooter() {
    <a class="underline hover:text-indigo-600" href="/">Back to home</a>
}

templ dashboardBody(rows []Item) {
    <div class="mx-auto max-w-3xl space-y-6 px-6 py-12">
        <h1 class="text-3xl font-bold">Dashboard</h1>
        if len(rows) == 0 {
            <p>No items yet.</p>
        }
        for _, row := range rows {
            @Card.Call(CardProps{Title: row.Title, Tone: row.Tone}, CardSlots{
                Children: ui.Text(row.Description),
                Footer: ui.Fragment(itemFooter()),
            })
        }
    </div>
}

templ dashboardPage(rows []Item) {
    @layout.Page.Call(layout.Props{Title: "Dashboard", Stylesheet: "/static/app.css"},
        layout.Slots{Children: ui.Fragment(dashboardBody(rows))})
}
```

Private helpers need no separate public definition when they don't declare a
reusable validated contract. `ui.Text` is for request text; `ui.Fragment` adapts
compiled markup, never strings. `ui.Group` combines already-typed fragments.

### Handler and registration: apps/dashboard/app.go

<!-- file: apps/dashboard/app.go -->
```go
package dashboard

import (
    "context"
    "log/slog"
    "net/http"
    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/response"
    "github.com/fauzaanu/cicadel/ui"
    "github.com/fauzaanu/cicadel/urls"
)

type Item struct { Title, Description, Tone string }
type LoadItems func(context.Context) ([]Item, error)

func New(load LoadItems) cicadel.App {
    return cicadel.App{
        Name: "dashboard",
        Components: []ui.Export{Card.Export(1)},
        Routes: []urls.Route{urls.Path("GET", "/dashboard", "dashboard:index",
            func(w http.ResponseWriter, r *http.Request) {
                rows, err := load(r.Context())
                if err != nil { response.Error(w, 500); return }
                err = (ui.Renderer{}).HTML(r.Context(), w, 200, ui.Fragment(dashboardPage(rows)))
                if err != nil {
                    slog.ErrorContext(r.Context(), "render dashboard", "error", err)
                    if !ui.Committed(err) { response.Error(w, 500) }
                }
            }),
        },
    }
}
```

The loader is explicit and can query through your application's DB; the component
never discovers a request-wide map. Keep permissions in the handler/query scope.
For the runnable demo, replace the starter's empty app list with this loader:

<!-- file: project/apps.go -->
```go
package project

import (
    "context"
    "example.com/componentguide/apps/dashboard"
    "github.com/fauzaanu/cicadel"
    "gorm.io/gorm"
)

func InstalledApps(_ *gorm.DB) ([]cicadel.App, error) {
    return []cicadel.App{dashboard.New(func(ctx context.Context) ([]dashboard.Item, error) {
        return []dashboard.Item{{Title: "Ready", Description: "Typed props and slots work.", Tone: "success"}}, ctx.Err()
    })}, nil
}
```

For your own module, substitute its import path. Local direct calls to `Card` use
that definition. For an overridable library, resolve through `ComponentRegistry`
and inject the resulting definition into the page factory; see
[the override recipe](https://cicadel.fauzaanu.com/en/dev/sources/skills/cicadel-components/references/patterns.md#libraries-and-project-overrides).

### Behavioral tests: apps/dashboard/app_test.go

<!-- file: apps/dashboard/app_test.go -->
```go
package dashboard

import (
    "context"
    "strings"
    "testing"
    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/apptest"
    "github.com/fauzaanu/cicadel/ui"
)

func TestDashboard(t *testing.T) {
    attack := `<script>alert("x")</script>`
    app := apptest.New(t, apptest.Settings(), []cicadel.App{
        New(func(context.Context) ([]Item, error) {
            return []Item{{Title: attack, Description: attack}}, nil
        }),
    })
    res := apptest.NewClient(app).Get(t, "/dashboard").Status(t, 200)
    res.Contains(t, `rel="stylesheet"`).Contains(t, `href="/static/app.css"`)
    res.Contains(t, "Back to home").Contains(t, "border-slate-200")
    if strings.Contains(string(res.Body), "<script>") { t.Fatal("unescaped input") }
}

func TestCardContractAndEmptyList(t *testing.T) {
    renderer := ui.Renderer{}
    if _, err := renderer.Render(t.Context(), Card.Call(CardProps{Title: "x"}, CardSlots{})); err == nil {
        t.Fatal("missing children accepted")
    }
    if _, err := renderer.Render(t.Context(), Card.Call(CardProps{Title: "x", Tone: "invalid"}, CardSlots{Children: ui.Text("x")})); err == nil {
        t.Fatal("invalid tone accepted")
    }
    out, err := renderer.Render(t.Context(), ui.Fragment(dashboardPage(nil)))
    if err != nil || !strings.Contains(string(out), "No items yet.") { t.Fatalf("%s: %v", out, err) }
}
```

Tests inspect behavior rather than accepting an entire HTML snapshot. Add assertions
for your fields, labels, error relationships, links and authorization requirements.

## The short edit loop

```sh
go tool cicadel ui build
go tool cicadel ui check --json
go test -race ./...
go run manage.go check
```

Restart `go run manage.go runserver` to load rebuilt embedded markup/CSS. Commit
`.templ`, ordinary Go, CSS input/config, generated Go and generated CSS together.
`ui check` reports missing/stale artifacts; `ui build` fixes them. Do not hand-edit
generated output. The initial starter CSS is already compiled, so first run does
not download a compiler or require Node. After UI changes, install the tool once
and rebuild. Installed library class sources belong in `ui.css.json` `packages`.

## Common mistakes to avoid

- Write multiline `if`/`for` blocks, not JavaScript ternaries or inline JSX blocks.
- Use `class`, not `className`; complete utility names, not dynamic name fragments.
- `ui.Merge` merges tokens, not Tailwind conflict groups. `p-2 p-6` does not promise
  that the last prop wins. Select a single spacing variant instead.
- Pass slot values explicitly. `{ children... }` is intentionally unsupported.
- Keep CSS outside markup. Inline styles, scripts, event attributes and active SVG
  are not in the initial component contract; don't work around that with raw HTML.
- Preserve CSRF and accessible errors using `ui/form`, with explicit session tokens.
- Fix failures in authored source. JSON diagnostics point to source components and
  preserve generated Go positions; never fix a type error in `*_templ.go`.

For targeted form/default/forwarding repairs, see the
[skill's pattern reference](https://cicadel.fauzaanu.com/en/dev/sources/skills/cicadel-components/references/patterns.md).

## Make the skill available to your coding agent

The repository keeps the portable skill at `skills/cicadel-components/`, including
its focused patterns and debugging reference. Repository `AGENTS.md` points agents
to it for UI tasks. With Codex, copy that directory into
`${CODEX_HOME:-$HOME/.codex}/skills/` (or use your skill installer), then invoke
`$cicadel-components`. Keep the reference directory with `SKILL.md`; it does not
require browser tools or a runtime plugin. Other coding agents can read the same
Markdown instructions directly. Refresh/reopen the agent session if its skill
catalog was loaded before installation.
