# Creating a New Component A component is a **reusable stamp**. You design the stamp once (the `.htmx` template + `.htmx.cs` class), and then press it anywhere you need that piece of UI — on multiple pages, inside other components, even multiple times on the same page. Components are identical in structure to pages, with two key differences: 1. They live in `Templates/Components/` instead of `Templates/` 2. They are never responsible for HTTP routing — they just render HTML --- ## What you want to achieve By the end of this guide you will be able to build any reusable UI piece — a styled label, a card, a form field, or a wrapper that holds other components — and drop it anywhere on a page. --- ## The three patterns All components fit one of three shapes. Pick the one that matches what you are building. --- ### Pattern A — A simple label or display element Use this when the component just renders a styled string. It is the simplest case. **Goal:** a coloured status badge you can reuse in tables, cards, and headers. ```html $$Label$$ ``` ```csharp // Templates/Components/Badge.htmx.cs namespace Htmx.ApiDemo.Templates.Components; public sealed class Badge : BadgeBase { private readonly byte[] _labelData; private readonly byte[] _classesData; public Badge(string label, string variant = "default") { _labelData = label.ToUtf8Bytes(); var variantClasses = variant switch { "secondary" => "bg-secondary text-secondary-foreground", "destructive" => "bg-destructive text-destructive-foreground", "outline" => "border border-border text-foreground", _ => "bg-primary text-primary-foreground", }; _classesData = $"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold {variantClasses}" .ToUtf8Bytes(); } protected override void RenderLabel(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_labelData); protected override void RenderClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_classesData); } ``` The key principle: **all computation happens in the constructor**. By the time `RenderLabel` is called during a request, it is just writing pre-computed bytes — no string formatting, no allocations. --- ### Pattern B — A container with optional sections Use this when parts of the component are optional — for example a card that shows a header only when a title is provided. **Goal:** a card that always shows its body, but optionally shows a header and a footer. ```html
$$Header$$
$$Content$$
$$Footer$$
``` ```csharp // Templates/Components/Card.htmx.cs namespace Htmx.ApiDemo.Templates.Components; public sealed class Card : CardBase { private readonly byte[] _extraClassesData; private readonly byte[] _headerData; private readonly byte[] _contentData; private readonly byte[] _footerData; public Card( string content, string title = "", string description = "", string footer = "", string extraClasses = "") { _extraClassesData = extraClasses.ToUtf8Bytes(); _contentData = content.ToUtf8Bytes(); // Build header HTML in the constructor. If there's no title/description, // store an empty array — writing empty bytes is a no-op. _headerData = (string.IsNullOrEmpty(title) && string.IsNullOrEmpty(description)) ? [] : BuildHeader(title, description); _footerData = string.IsNullOrEmpty(footer) ? [] : $"""
{footer}
""".ToUtf8Bytes(); } private static byte[] BuildHeader(string title, string description) { var sb = new System.Text.StringBuilder(); sb.Append("""
"""); if (!string.IsNullOrEmpty(title)) sb.Append($"""

{title}

"""); if (!string.IsNullOrEmpty(description)) sb.Append($"""

{description}

"""); sb.Append("
"); return sb.ToString().ToUtf8Bytes(); } protected override void RenderExtraClasses(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_extraClassesData); protected override void RenderHeader(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_headerData); protected override void RenderContent(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_contentData); protected override void RenderFooter(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_footerData); } ``` --- ### Pattern C — A wrapper that holds other components Use this when a slot should be filled by another component rather than a string. **Goal:** a tooltip wrapper — the trigger is any component, and the tooltip text floats above it on hover. ```html $$Trigger$$ $$Text$$ ``` ```csharp // Templates/Components/Tooltip.htmx.cs namespace Htmx.ApiDemo.Templates.Components; public sealed class Tooltip : TooltipBase { private readonly IHtmxComponent _trigger; private readonly byte[] _textData; public Tooltip(string text, IHtmxComponent trigger) { _textData = text.ToUtf8Bytes(); _trigger = trigger; } protected override void RenderText(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_textData); // ctx.Next() increments the nesting depth counter. // The runtime throws if nesting exceeds 512 levels — this is the guard against infinite loops. protected override void RenderTrigger(HtmxRenderContext ctx) => _trigger.Render(ctx.Next()); } ``` --- ## Using a component inside a page Once you have a component, use it from a page's code-behind. The page stores the component as a field and delegates `Render` from its slot override: ```csharp // MyPage.htmx.cs public sealed class MyPage : MyPageBase { private readonly byte[] _headingData; private readonly IHtmxComponent _statusBadge; public MyPage(string heading, string status) { _headingData = heading.ToUtf8Bytes(); _statusBadge = new Badge(status, variant: "secondary"); } protected override void RenderHeading(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_headingData); protected override void RenderStatusBadge(HtmxRenderContext ctx) => _statusBadge.Render(ctx.Next()); } ``` Template: ```html

$$Heading$$

$$StatusBadge$$
``` --- ## A note on HTML safety `WriteUtf8` writes raw bytes directly to the HTTP response. It does **not** HTML-encode anything. - Strings you write in the constructor that come from your own code are fine — you control them. - Any value that comes from user input (a form field, a database value, a query parameter) **must be HTML-encoded before calling `ToUtf8Bytes()`**: ```csharp // Safe — encodes characters like < > " & _nameData = System.Web.HttpUtility.HtmlEncode(userInput).ToUtf8Bytes(); ``` Skipping this step is a cross-site scripting (XSS) vulnerability. --- ## File location and namespace | File location | C# namespace | |---|---| | `Templates/Components/MyComp.htmx` | `Htmx.ApiDemo.Templates.Components` | | `Templates/MyPage.htmx` | `Htmx.ApiDemo.Templates` | The source generator derives the namespace from the folder path relative to the project root. Always keep components in `Templates/Components/`. --- ## Checklist - [ ] `.htmx` template created in `Templates/Components/` with `$$PascalCase$$` slots - [ ] `.htmx.cs` class inherits the generated `XxxBase` class - [ ] All `RenderXxx` overrides implemented - [ ] Computation (string building, class selection) done in the constructor - [ ] User-provided strings HTML-encoded before `ToUtf8Bytes()` - [ ] Sub-component `Render` calls use `ctx.Next()` not bare `ctx` // Safe — user-supplied string is encoded first _displayNameData = System.Web.HttpUtility.HtmlEncode(userDisplayName).ToUtf8Bytes(); ``` The existing `MainLayout` constructor demonstrates this for the user initials section. --- ## Checklist - [ ] `MyComp.htmx` created in `Templates/Components/` - [ ] `MyComp.htmx.cs` created with class inheriting `MyCompBase` - [ ] All `$$Slot$$`s have a matching `RenderSlot` override - [ ] User-supplied strings are HTML-encoded before `ToUtf8Bytes()` - [ ] Build once to confirm the compiler catches any missing overrides