Files
Htmx/docs/Components/Card.md
T
2026-05-04 19:57:48 +05:00

5.7 KiB

Card

A styled container with optional header (title + description) and footer sections. The body content is always rendered; header and footer are conditionally included.


HTML structure

div.rounded-lg.border.border-border.bg-card.text-card-foreground.shadow-sm.{extraClasses}
  div.flex.flex-col.space-y-1.5.p-6          ← header (omitted when no title/description)
    h3.text-2xl.font-semibold                ← title
    p.text-sm.text-muted-foreground          ← description
  div.p-6.pt-0                               ← content (always present)
    {content}
  div.flex.items-center.p-6.pt-0            ← footer (omitted when empty)
    {footer}

CSS mechanics

Class Effect
bg-card text-card-foreground Pulls from CSS variables — dark mode works automatically
rounded-lg border border-border shadow-sm Subtle rounded box with border and drop shadow
p-6 pt-0 on content Full padding except top (header provides the top spacing)
space-y-1.5 on header Controlled gap between title and description

Constructor signature

public Card(
    string content,
    string title        = "",
    string description  = "",
    string footer       = "",
    string extraClasses = "")
Parameter Description
content Raw HTML for the card body (always rendered)
title Optional heading in the header area
description Optional subheading below the title
footer Optional raw HTML in the footer area
extraClasses Additional Tailwind classes on the outer div

Usage examples

Simple content card

new Card(content: "<p>Your subscription renews on July 1.</p>")

Card with title and description

new Card(
    content:     "<p>Manage your billing details and invoices.</p>",
    title:       "Billing",
    description: "Your current plan: Pro")
new Card(
    content:     "<p>Are you sure you want to cancel your account?</p>",
    title:       "Delete account",
    description: "This action cannot be undone.",
    footer:      """
        <button class="inline-flex h-9 rounded-md border border-input px-4 text-sm mr-2">Cancel</button>
        <button class="inline-flex h-9 rounded-md bg-destructive text-destructive-foreground px-4 text-sm">Delete</button>
    """)

Constrained width

new Card(
    content:     "<p>Hello world</p>",
    title:       "Welcome",
    extraClasses: "max-w-sm mx-auto")

Embedding a component as content

// Render a Badge to a string then embed in the card body
var writer = new System.Buffers.ArrayBufferWriter<byte>();
new Badge("Active").Render(new HtmxRenderContext(writer));
var badgeHtml = System.Text.Encoding.UTF8.GetString(writer.WrittenSpan);

new Card(
    content:     $"<p class='mb-2'>Status:</p>{badgeHtml}",
    title:       "Account")

Tips and tricks

  • content, footer, title, and description are inserted as raw HTML — HTML-encode any user-supplied strings before passing them in.
  • Use extraClasses to set max-width, margin, or custom background without subclassing.
  • If you need a completely custom header layout, omit title and description and build the header HTML in content, adding p-6 padding yourself.
  • Cards can be placed in a CSS grid: <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">.
  • Cards can be placed in a CSS grid: <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">.

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);