Card
A container that groups related content and actions.
Installation
Included in poetry-ui — available as
poetry_card
the moment you've installed Poetry,
with no per-component step. To own the source and edit it, copy it into your app:
bin/rails g poetry:add card
Default
Revenue
<%= poetry_card(class: "w-full max-w-sm") do |card| %>
<% card.with_title { "Revenue" } %>
<% card.with_description { "Last 30 days" } %>
$12,345
<% end %>
Login form
<%# A form nested in a card - the header names it, the body holds the
fields, the footer stacks the submit and provider actions. %>
<%= poetry_card(class: "w-full max-w-sm") do |card| %>
<% card.with_title { "Sign in" } %>
<% card.with_description { "Enter your credentials to reach your workspace." } %>
<% card.with_action { poetry_button(href: "/signup", variant: :link, size: :sm) { "Create account" } } %>
<div class="grid gap-4">
<%= poetry_field(id: "login-email", label_text: "Email") do |field| %>
<%= poetry_input(type: "email", name: "email", placeholder: "you@example.com",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
<%= poetry_field(id: "login-password", label_text: "Password") do |field| %>
<%= poetry_input(type: "password", name: "password",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
<% card.with_footer do %>
<div class="flex w-full flex-col gap-2">
<%= poetry_button(class: "w-full") { "Sign in" } %>
<%= poetry_button(variant: :outline, class: "w-full") { "Continue with GitHub" } %>
</div>
<% end %>
<% end %>
Terms of service
Terms of service
By creating an account you agree to keep your login details private and to use the service only for lawful purposes.
Content you upload stays yours. You grant us permission to store and process it solely to operate the features you have turned on.
We may update these terms as the product changes. Continued use after an update means you accept the revised version.
You can close your account at any time. Exports remain available for thirty days after closure, after which stored data is removed.
<%# A scrollable body keeps long content boxed inside the card; the
footer carries the two decisions. %>
<%= poetry_card(class: "w-full max-w-md") do |card| %>
<% card.with_title { "Terms of service" } %>
<% card.with_description { "Review the agreement before you continue." } %>
<div class="h-48 space-y-3 overflow-y-auto pr-2 text-sm text-muted-foreground">
<p>By creating an account you agree to keep your login details private and
to use the service only for lawful purposes.</p>
<p>Content you upload stays yours. You grant us permission to store and
process it solely to operate the features you have turned on.</p>
<p>We may update these terms as the product changes. Continued use after an
update means you accept the revised version.</p>
<p>You can close your account at any time. Exports remain available for
thirty days after closure, after which stored data is removed.</p>
</div>
<% card.with_footer do %>
<div class="flex w-full justify-end gap-2">
<%= poetry_button(variant: :outline) { "Decline" } %>
<%= poetry_button { "Accept" } %>
</div>
<% end %>
<% end %>
With action and footer
Team
<%= poetry_card(class: "w-full max-w-sm") do |card| %>
<% card.with_title { "Team" } %>
<% card.with_description { "Active members" } %>
<% card.with_action { poetry_button(size: :sm, variant: :outline) { "Invite" } } %>
<% card.with_footer { "Updated hourly" } %>
24 members
<% end %>
With media
Coastal Trail Pass
One day pass covering every trailhead, the parking lots, and the shuttle loop that runs between them.
<%# A media-led card: a badge rides the header corner, the body opens
with a cover, and the footer holds the call to action. %>
<%= poetry_card(class: "w-full max-w-sm") do |card| %>
<% card.with_title { "Coastal Trail Pass" } %>
<% card.with_description { "Seven marked routes along the northern shore." } %>
<% card.with_action { poetry_badge { "Featured" } } %>
<div class="space-y-4">
<div class="flex aspect-video items-center justify-center rounded-md bg-muted text-sm text-muted-foreground">
Cover image
</div>
<p class="text-sm text-muted-foreground">
One day pass covering every trailhead, the parking lots, and the shuttle
loop that runs between them.
</p>
</div>
<% card.with_footer do %>
<%= poetry_button(class: "w-full") { "Get the pass" } %>
<% end %>
<% end %>
API
Poetry::Ui::Card::Component — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| content_class: | String | Extra classes merged into the body cell (caller classes win). A chat-in-a-card layout passes min-h-0 flex-1 p-0 so the transcript can flex and scroll. | |
| header_class: | String | Extra classes merged into the header row - border-b rules the title off from the body. | |
| title_tag: | Symbol | one of: h1, h2, h3, h4, h5, h6; defaults to :h3 |
The heading element for the title - pick it to fit the page outline. |
Slots
| Writer | Description |
|---|---|
| with_action | The header's trailing corner control (a button, menu, or link). |
| with_description | Muted one-liner under the title. |
| with_footer | The bottom row (actions/meta). class: merges into the footer div (a border-t divider is the canonical use); every other option (id:, data:, ...) rides onto the footer div verbatim. |
| with_title | The heading line, rendered as a real heading element (title_tag:). |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=card] | Root container - the vertical flex stack |
| [data-slot=card-header] | The title row grid - gains a trailing auto column when card-action is present |
| [data-slot=card-title] | The heading (title_tag, h3 by default) |
| [data-slot=card-description] | Muted one-liner under the title |
| [data-slot=card-action] | The header's trailing corner control |
| [data-slot=card-content] | The body - the content block renders here |
| [data-slot=card-footer] | The bottom row (actions/meta) |