Delete this project?
Its exports and activity history are removed permanently. This can’t be undone.
A compound surface with typed title, body, divider, and footer slots.
app/components/nitro_kit/card.rbNitroKit::Card.new(size: :md, variant: :default) { |card| card.header { card.title; card.description; card.actions }; card.body; card.footer }A card groups related content on one surface. Title, description, body, and footer are all optional; use the ones the content needs.
Its exports and activity history are removed permanently. This can’t be undone.
test/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Card.new(id: "gallery-card-simple") do |card|
card.title("Delete this project?", level: 3)
card.description("Its exports and activity history are removed permanently. This can’t be undone.")
card.footer do
render NitroKit::Button.new("Delete project", variant: :destructive, size: :sm)
render NitroKit::Button.new("Cancel", size: :sm)
end
endThis is how others see you around the workspace.
test/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Card.new(id: "gallery-card-profile") do |card|
card.header do
card.title("Profile", level: 3)
card.description("This is how others see you around the workspace.")
card.actions do
render NitroKit::Dropdown.new(id: "gallery-card-profile-menu", placement: :bottom_end) do |menu|
menu.trigger(icon: :ellipsis, label: "Profile actions", size: :sm)
menu.item("Copy profile link", icon: :link)
menu.item("Reset to defaults", icon: :rotate_ccw)
end
end
end
card.divider
card.body do
form(id: "gallery-card-profile-form", action: "#profile", method: "post") do
render NitroKit::FieldGroup.new do
render NitroKit::Field.new(
nil,
:name,
id: "gallery-card-profile-name",
name: "profile[name]",
value: "Ada Lovelace",
label: "Name",
autocomplete: "name",
required: true,
html: { id: "gallery-card-profile-name-field" }
)
render NitroKit::Field.new(
nil,
:bio,
as: :textarea,
id: "gallery-card-profile-bio",
name: "profile[bio]",
label: "Bio",
placeholder: "A few words about yourself",
rows: 3,
html: { id: "gallery-card-profile-bio-field" }
)
end
end
end
card.divider
card.footer do
plain "Last saved 2 minutes ago"
card.actions do
render NitroKit::Button.new("Cancel", type: :reset, form: "gallery-card-profile-form", size: :sm)
render NitroKit::Button.new(
"Save",
id: "gallery-card-profile-save",
type: :submit,
form: "gallery-card-profile-form",
variant: :primary,
size: :sm
)
end
end
endSize coordinates padding, spacing, corner radius, and title size. Small suits dense widgets, medium most records, and large roomy forms.
test/dummy/app/components/gallery/components/card_page.rb{
sm: "Tight, for small widgets and stats.",
md: "The default, for most content.",
lg: "Roomy, for forms and settings."
}.each do |size, text|
sample(size.to_s, slug: "size-#{size}") do
render NitroKit::Card.new(size:) do |card|
card.header do
card.title("Weekly report", level: 3)
card.actions { render NitroKit::Button.new("Edit", size: :sm) }
end
card.divider
card.body(text)
end
end
endDefault is raised for primary content. Muted is a quiet tint for secondary panels, and outline keeps only the edge over whatever is behind it.
Raised, for primary content.
A quieter tint for secondary panels.
Just an edge over the surrounding surface.
test/dummy/app/components/gallery/components/card_page.rb{
default: "Raised, for primary content.",
muted: "A quieter tint for secondary panels.",
outline: "Just an edge over the surrounding surface."
}.each do |variant, text|
sample(variant.to_s, slug: "surface-#{variant}") do
render NitroKit::Card.new(size: :sm, variant:) do |card|
card.title(variant.to_s.capitalize, level: 3)
card.description(text)
end
end
end12 active members · Team plan
12 active members · Team plan
12 active members · Team plan
12 active members · Team plan
12 active members · Team plan
12 active members · Team plan
12 active members · Team plan
12 active members · Team plan
12 active members · Team plan
test/dummy/app/components/gallery/components/card_page.rbNitroKit::Card::SIZES.product(NitroKit::Card::VARIANTS).each do |size, variant|
sample("#{size} · #{variant}", slug: "#{size}-#{variant}") do
render NitroKit::Card.new(size:, variant:) do |card|
card.header do
card.title("Workspace", level: 3)
card.description("12 active members · Team plan")
end
card.divider
card.footer do
plain "Renews August 1"
card.actions { render NitroKit::Button.new("Manage", size: :sm) }
end
end
end
endDividers split a card into sections. A header or footer set apart by a divider becomes a band with even spacing above and below its content.
Keep your account protected.
Last changed 3 months ago
A code from your authenticator app at sign in
test/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Card.new(id: "gallery-card-security") do |card|
card.header do
card.title("Security", level: 3)
card.description("Keep your account protected.")
end
card.divider
card.header do
card.title("Password", level: 4)
card.description("Last changed 3 months ago")
card.actions { render NitroKit::Button.new("Change", size: :sm) }
end
card.divider
card.header do
card.title("Two-factor authentication", level: 4)
card.description("A code from your authenticator app at sign in")
card.actions { render NitroKit::Badge.new("On", color: :success, size: :sm) }
end
endChoose what you hear about.
Choose what you hear about.
Choose what you hear about.
test/dummy/app/components/gallery/components/card_page.rbNitroKit::Card::SIZES.each do |size|
sample(size.to_s, slug: "form-#{size}") do
form_id = "gallery-card-preferences-#{size}"
render NitroKit::Card.new(size:) do |card|
card.header do
card.title("Notifications", level: 3)
card.description("Choose what you hear about.")
end
card.body do
form(id: form_id, action: "#preferences", method: "get") do
render NitroKit::FieldGroup.new do
render NitroKit::Switch.new(label: "Product updates", name: "updates", checked: true)
render NitroKit::Switch.new(label: "Weekly digest", name: "digest")
render NitroKit::Switch.new(label: "Security alerts", name: "security", checked: true)
end
end
end
card.divider
card.footer do
card.actions do
render NitroKit::Button.new("Reset", type: :reset, form: form_id, size: :sm)
render NitroKit::Button.new("Save", variant: :primary, type: :submit, form: form_id, size: :sm)
end
end
end
end
endFull regions bleed to the card's edges and take its corner radius where they touch one. Only the region clips, so menus and tooltips can still escape the card.
September 12 · 8.4 km
September 12 · 8.4 km
September 12 · 8.4 km
test/dummy/app/components/gallery/components/card_page.rbNitroKit::Card::SIZES.each do |size|
sample(size.to_s, slug: "media-#{size}") do
render NitroKit::Card.new(size:) do |card|
card.full do
img(src: "/gallery/card-landscape.svg", alt: "Layered green hills beneath a warm sun", width: 800, height: 360)
end
card.header do
card.title("Morning hike", level: 3)
card.description("September 12 · 8.4 km")
card.actions { render NitroKit::Button.new(icon: :heart, label: "Save hike", size: :sm) }
end
end
end
end| Merchant | Category | Amount |
|---|---|---|
| Blue Bottle Coffee | Dining | −$6.50 |
| Figma | Software | −$15.00 |
| Client payment | Income | +$2,400.00 |
test/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Card.new(id: "gallery-card-transactions") do |card|
card.header do
card.title("Past transactions", level: 3)
card.actions { render NitroKit::Button.new("Export", icon: :download, size: :sm) }
end
card.full do
render NitroKit::Table.new do |table|
table.caption("Transactions this week")
table.thead do
table.tr do
table.th("Merchant")
table.th("Category")
table.th("Amount", align: :right)
end
end
table.tbody do
[
[ "Blue Bottle Coffee", "Dining", "−$6.50" ],
[ "Figma", "Software", "−$15.00" ],
[ "Client payment", "Income", "+$2,400.00" ]
].each do |merchant, category, amount|
table.tr do
table.th(merchant, scope: :row)
table.td(category)
table.td(amount, align: :right)
end
end
end
end
end
card.footer("Showing the last 7 days")
endtest/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Container.new(size: :sm) do
render NitroKit::Card.new(id: "gallery-card-media-only") do |card|
card.full do
img(src: "/gallery/card-landscape.svg", alt: "Layered green hills beneath a warm sun", width: 800, height: 360)
end
end
endCards hold ordinary Nitro components. Parents own the width and the layout of a collection.
test/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Grid.new(cols: "1 sm:2 lg:3", gap: 4) do
[
[ "Heading", "Create hierarchical section headings for pages and panels." ],
[ "Table", "Display structured data with sorting and row actions." ],
[ "Field", "Pair inputs with labels, descriptions, and errors." ]
].each do |title, text|
render NitroKit::Card.new(size: :sm) do |card|
card.header do
card.title(title, level: 3)
card.description(text)
card.actions { render NitroKit::Button.new(icon: :arrow_up_right, label: "Open #{title}", href: "#", size: :sm) }
end
end
end
endPost alerts to the team operations channel.
| Connected | |
|---|---|
| Delivery target | Team operations channel |
test/dummy/app/components/gallery/components/card_page.rbintegration = Gallery::Data.integrations.fetch(1)
render NitroKit::Card.new(id: "gallery-card-integration") do |card|
card.header do
card.title(integration.name, level: 3)
card.description(integration.description)
card.actions do
render NitroKit::Badge.new(
"Action required",
id: "gallery-card-integration-status",
color: :warning,
size: :sm
)
end
end
card.body do
render NitroKit::DetailsTable.new(
integration,
id: "gallery-card-integration-details",
label: "Slack integration details"
) do |details|
details.field(:connected_at, label: "Connected")
details.field(:delivery_target, label: "Delivery target", value: "Team operations channel")
end
end
card.divider
card.footer do
render NitroKit::ButtonGroup.new(
id: "gallery-card-integration-actions",
label: "Slack integration actions"
) do |group|
group.button(
"Reconnect",
id: "gallery-card-integration-reconnect",
variant: :primary,
size: :sm,
icon: :refresh_cw
)
group.button(
"Disconnect",
id: "gallery-card-integration-disconnect",
variant: :destructive,
size: :sm
)
end
end
endThe title slot emits the requested semantic heading level from one through six without changing its size.
Card titles follow the surrounding document hierarchy.
Card titles follow the surrounding document hierarchy.
Card titles follow the surrounding document hierarchy.
Card titles follow the surrounding document hierarchy.
Card titles follow the surrounding document hierarchy.
Card titles follow the surrounding document hierarchy.
test/dummy/app/components/gallery/components/card_page.rb(1..6).each do |level|
sample("Heading level #{level}", slug: "heading-#{level}") do
render NitroKit::Card.new(id: "gallery-card-heading-#{level}", size: :sm) do |card|
card.title("Level #{level} title", level:)
card.description("Card titles follow the surrounding document hierarchy.")
end
end
endIndividual slots and useful partial structures remain valid without placeholder content.
test/dummy/app/components/gallery/components/card_page.rbsample("Empty surface", slug: "empty") do
render NitroKit::Card.new(id: "gallery-card-empty") { nil }
end
sample("Title only", slug: "title-only") do
render NitroKit::Card.new(id: "gallery-card-title-only") do |card|
card.title("A title without supporting content", level: 3)
end
end
sample("Footer only", slug: "footer-only") do
render NitroKit::Card.new(id: "gallery-card-footer-only") do |card|
card.footer("Last synchronized July 13, 2026")
end
end
sample("Body and footer", slug: "body-footer") do
render NitroKit::Card.new(id: "gallery-card-body-footer") do |card|
card.body("Three pending invitations")
card.footer("Review access before the next billing cycle")
end
end
sample("Header with actions only", slug: "header-actions") do
render NitroKit::Card.new(id: "gallery-card-header-actions") do |card|
card.header do
card.title("Invitations", level: 3)
card.actions { render NitroKit::Button.new("Invite", size: :sm) }
end
end
end
sample("Full-width only", slug: "full-only") do
render NitroKit::Card.new(id: "gallery-card-full-only") do |card|
card.full { card.body("A full-width region can be the only declared slot.") }
end
endA card titled with a hundred-character name, carrying an unbroken-word badge, four paragraphs of pasted prose, a webhook URL and a 130-character email as links, and all twelve hostile actions in its footer; then an unbroken title in a small container.
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflege
test/dummy/app/components/gallery/components/card_page.rbNitroKit::Card::SIZES.product(NitroKit::Card::VARIANTS).each do |size, variant|
sample("#{size} · #{variant}", slug: "hostile-#{size}-#{variant}") do
render NitroKit::Card.new(size:, variant:) do |card|
card.full { Gallery::Hostile::LONG_WORD }
card.header do
card.title(Gallery::Hostile::LONG_NAME, level: 3)
card.description(Gallery::Hostile::LONG_WORD)
card.actions { render NitroKit::Button.new(Gallery::Hostile::ACTION_LABELS.first, size: :sm) }
end
card.body(Gallery::Hostile::LONG_EMAIL)
card.divider
card.footer do
plain Gallery::Hostile::LONG_URL
card.actions { render NitroKit::Button.new(Gallery::Hostile::ACTION_LABELS.last, size: :sm) }
end
end
end
endThis description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text. This description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text. This description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text. This description keeps going well past the point where a sensible product would have stopped, because real customers paste release notes, legal disclaimers, and entire support tickets into fields that were designed for a single line of text.
test/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Card.new(id: "gallery-card-hostile") do |card|
card.title(Gallery::Hostile::LONG_NAME, level: 3)
card.body do
render NitroKit::Badge.new(
Gallery::Hostile::LONG_WORD,
id: "gallery-card-hostile-status",
color: :warning,
size: :sm
)
p { Gallery::Hostile::LONG_PARAGRAPH }
p { a(href: Gallery::Hostile::LONG_URL) { Gallery::Hostile::LONG_URL } }
p { a(href: "mailto:#{Gallery::Hostile::LONG_EMAIL}") { Gallery::Hostile::LONG_EMAIL } }
end
card.divider
card.footer do
render NitroKit::ButtonGroup.new(id: "gallery-card-hostile-actions", label: "Hostile record actions") do |group|
Gallery::Hostile::ACTION_LABELS.each_with_index do |label, index|
group.button(label, id: "gallery-card-hostile-action-#{index + 1}", size: :sm)
end
end
end
endtest/dummy/app/components/gallery/components/card_page.rbrender NitroKit::Container.new(size: :sm, id: "gallery-card-hostile-narrow-container") do
render NitroKit::Card.new(id: "gallery-card-hostile-narrow") do |card|
card.title(Gallery::Hostile::LONG_WORD, level: 3)
card.body(Gallery::Hostile::LONG_EMAIL)
card.footer(Gallery::Hostile::HUGE_MONEY)
end
endConstructor options, rendered root, closed vocabularies, and compound boundary for this component, exactly as shipped.
docs/component_contracts.md · NitroKit::Card
size: :md, variant: :default, id: nilarticle[data-nk=card][data-size][data-variant]; sizes sm md lg; variants default muted outline; title levels 1..6header, title, description, actions, body, footer, divider, and full render immediately; their count and order are not constrained. title, description, body, and footer require non-blank text or a block; header, actions, and full require a block. header lays out a title and description beside trailing actions; actions inside a footer push to its end. The shadowed Phlex elements remain available as html_title, html_body, html_header, and html_footer. Sizes coordinate padding, gaps, corners, and title size. See Card sizing and surfaces below.Application conventions this component belongs to. Each summary is the leading section of its pattern document.
docs/patterns/inline_edit.md
422; successful updates redirect with 303.Instructions for coding agents, repeated on every component page so one fetched page has enough context. Humans can usually skip this section.
test/dummy/app/components/gallery/agent_rules.rb
render NitroKit::Button.new("Save", variant: :primary).ArgumentError; this component's accepted options are in the contract above.html: for HTML, aria: for ARIA, and data: for application data. class and style are forbidden, including inside html:.NitroKit::Component::RESERVED_DATA_ATTRIBUTES: data-nk, data-slot, data-variant, data-size, data-nk-escape, data-enhanced, data-state, data-disabled, data-required, data-orientation, data-presentation, data-placement, data-layout, data-side, data-field-type, data-dir, data-gap, data-align, data-justify, data-wrap, data-cols, data-mode, data-key. Do not pass them through data:. data-action, data-controller are additive and compose with Nitro behavior.desperately_need_a_class:. It requires a non-blank String and marks the exception with data-nk-escape="class".data-nk; owned parts emit component-qualified data-slot values such as field-control or card-title. Select on those attributes, never on classes.--nk-* custom properties in an application stylesheet. Variables beginning with --_nk-* are private component mechanics.NitroKit::Flex and NitroKit::Grid for layout. Parents own external placement and available width; components own their intrinsic geometry.data-state when styling or behavior also needs it.