Files
Htmx/docs/Components/Card.md
T
2026-05-05 23:55:26 +05:00

5.0 KiB

Card

A bordered box for grouping related content — like a physical card you might hold in your hand. It has three distinct zones: a header (title + subtitle), a body (your content), and a footer (usually actions).


Quick example

new Card(
    content:     "<p>Your subscription renews on July 1.</p>",
    title:       "Billing",
    description: "Current plan: Pro")

All the options

public Card(
    string content,
    string title        = "",
    string description  = "",
    string footer       = "",
    string extraClasses = "")
Parameter What it does
content The body of the card — always shown. Raw HTML.
title Optional bold heading at the top of the card.
description Optional smaller subtitle below the title.
footer Optional section at the bottom, typically holding action buttons. Raw HTML.
extraClasses Additional Tailwind classes on the outer div — useful for max-w-sm, col-span-2, etc.

The header section (title + description) is omitted entirely when both are empty. Same for the footer.


Real-world examples

A stats card on a dashboard

new Card(
    title:       "Total Users",
    description: "All registered accounts",
    content:     $"<p class=\"text-4xl font-bold\">{userCount:N0}</p>")

Buttons and other components need to be pre-rendered to HTML strings when used inside content or footer:

string ToHtml(IHtmxComponent c)
{
    var w = new System.Buffers.ArrayBufferWriter<byte>();
    c.Render(new HtmxRenderContext(w));
    return System.Text.Encoding.UTF8.GetString(w.WrittenSpan);
}

new Card(
    title:       "Delete account",
    description: "This action cannot be undone.",
    content:     "<p>All your data will be permanently removed.</p>",
    footer:      ToHtml(new Button("Cancel",  variant: "outline"))
               + ToHtml(new Button("Delete",  variant: "destructive", type: "submit")))

A grid of cards

Cards are most commonly placed in a CSS grid in the page template:

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
  $$Card1$$
  $$Card2$$
  $$Card3$$
</div>

Constrained width (e.g. a login card)

new Card(
    content:      "...login form HTML...",
    title:        "Welcome back",
    description:  "Sign in to your account",
    extraClasses: "max-w-sm mx-auto")

How it works

Card uses CSS variables (bg-card, text-card-foreground, border-border) which automatically adapt to dark mode. The header and footer sections are skipped entirely in the rendered HTML when they are not needed — they do not leave empty divs behind.

All strings passed to content and footer are raw HTML. HTML-encode any user-supplied values before passing them in.


Complete page example

Templates/DashboardPage.htmx

<div class="max-w-5xl mx-auto py-10">
  <h1 class="text-2xl font-bold mb-8">Dashboard</h1>
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
    $$UsersCard$$
    $$RevenueCard$$
    $$OrdersCard$$
  </div>
</div>

Templates/DashboardPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class DashboardPage : DashboardPageBase
{
    private readonly IHtmxComponent _users;
    private readonly IHtmxComponent _revenue;
    private readonly IHtmxComponent _orders;

    public DashboardPage(DashboardStats stats)
    {
        _users = new Components.Card(
            title:       "Total users",
            description: "Registered accounts",
            content:     $"<p class=\"text-4xl font-bold\">{stats.UserCount:N0}</p>");

        _revenue = new Components.Card(
            title:       "Revenue",
            description: "This month",
            content:     $"<p class=\"text-4xl font-bold\">${stats.MonthlyRevenue:N2}</p>");

        _orders = new Components.Card(
            title:       "Open orders",
            description: "Awaiting fulfillment",
            content:     $"<p class=\"text-4xl font-bold\">{stats.OpenOrders}</p>",
            footer:      """<a href="/orders" class="text-sm text-primary underline">View all</a>""");
    }

    protected override void RenderUsersCard(HtmxRenderContext ctx)   => _users.Render(ctx.Next());
    protected override void RenderRevenueCard(HtmxRenderContext ctx) => _revenue.Render(ctx.Next());
    protected override void RenderOrdersCard(HtmxRenderContext ctx)  => _orders.Render(ctx.Next());
}

GET handler

[Handler]
[MapGet("/dashboard")]
public static partial class GetDashboardHandler
{
    public record Query();

    private static Task<IResult> HandleAsync(
        Query _, HttpContext ctx, CancellationToken ct)
    {
        var stats = new DashboardStats(UserCount: 1_204, MonthlyRevenue: 48_320.50m, OpenOrders: 37);
        return ctx.WriteHtmxPage(new DashboardPage(stats), title: "Dashboard");
    }
}

public record DashboardStats(int UserCount, decimal MonthlyRevenue, int OpenOrders);