Card
A container with header, title, description, content, and footer sub-components.
Installation
bin/rails g wabi:add cardExample
Notifications
You have 3 unread messages.
Manage how and when you receive notifications.
render Components::UI::Card.new(class: "max-w-sm") do
render Components::UI::CardHeader.new do
render Components::UI::CardTitle.new { "Notifications" }
render Components::UI::CardDescription.new { "You have 3 unread messages." }
end
render Components::UI::CardContent.new do
p { "Manage how and when you receive notifications." }
end
render Components::UI::CardFooter.new do
render Components::UI::Button.new { "Mark all as read" }
end
end
Standalone (no header)
For cards without a CardHeader, pass padding: :standalone to CardContent for symmetric vertical padding.
Storage
8.2 GB of 15 GB used
render Components::UI::Card.new(class: "max-w-sm") do
render Components::UI::CardContent.new(
padding: :standalone,
class: "flex items-center justify-between gap-3"
) do
div do
p(class: "font-medium") { "Storage" }
p(class: "text-sm text-muted-foreground") { "8.2 GB of 15 GB used" }
end
render Components::UI::Button.new(appearance: :outline, size: :sm) { "Upgrade" }
end
end
Source
app/components/ui/card.rb
# frozen_string_literal: true
module Components
module UI
class Card < Wabi::Base
variants do
base "rounded-lg border bg-card text-card-foreground shadow-sm"
end
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
div(**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
app/components/ui/card_header.rb
# frozen_string_literal: true
module Components
module UI
class CardHeader < Wabi::Base
variants do
base "flex flex-col space-y-1.5 p-6"
end
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
div(**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
app/components/ui/card_title.rb
# frozen_string_literal: true
module Components
module UI
class CardTitle < Wabi::Base
variants do
base "text-2xl font-semibold leading-none tracking-tight"
end
# level: (Integer, default 3) — heading level 1-6; allows callers to
# match the document outline when the card is a top-level content block.
def initialize(level: 3, **attrs)
@level = level.to_i.clamp(1, 6)
@attrs = attrs
end
def view_template(&)
user_class = @attrs.delete(:class)
send(:"h#{@level}", **@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
app/components/ui/card_description.rb
# frozen_string_literal: true
module Components
module UI
class CardDescription < Wabi::Base
variants do
base "text-sm text-muted-foreground"
end
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
p(**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
app/components/ui/card_content.rb
# frozen_string_literal: true
module Components
module UI
class CardContent < Wabi::Base
variants do
base "px-6"
# `:with_header` (default) mantiene el comportamiento shadcn: sin padding
# arriba porque CardContent va debajo de un CardHeader.
# `:standalone` agrega padding vertical simétrico para cards sin header.
variant :padding, {
with_header: "pt-0 pb-6",
standalone: "py-6"
}, default: :with_header
end
def initialize(padding: nil, **attrs)
@padding = padding
@attrs = attrs
end
def view_template(&)
user_class = @attrs.delete(:class)
div(**@attrs, class: merge_class(tokens(padding: @padding), user_class), &)
end
end
end
end
app/components/ui/card_footer.rb
# frozen_string_literal: true
module Components
module UI
class CardFooter < Wabi::Base
variants do
base "flex items-center p-6 pt-0"
end
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
div(**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
Accessibility
- Card is a structural container with no implicit semantics; the meaning comes from contents.
- CardTitle renders as h3 by default — verify the heading level fits the page outline.
- footer actions should be real <button> or <a> elements (the Button component is correct here), not styled <div>s.