Card
A flexible, plug-and-play card system. Every slot is optional — skip any piece and the card still looks right.
Key Features
- Six independent slots —
media,icon,header,body,list,footer - Fixed render order — slots always appear in the same sequence regardless of call order
- Shortcut API —
title:/description:/icon:for the common icon + heading + description case - Whole-card link mode —
href:wraps the card in a single<a>with hover lift - Four variants —
:elevated,:outline,:flat,:soft - Semantic colors — icon tiles, soft variant, footer links resolve through
config.colors - No JS controller — pure server-rendered, dark mode included
- Starter tier — available on all RRUI licenses
Basic Usage
Pass a block and populate slots with with_* calls. Slots render in a fixed order: media → icon/header → body → divider → list → footer.
Timed Online
<%= rui_card do |c| %>
<% c.with_icon(name: "clock") %>
<% c.with_header(title: "Timed Online") %>
<% c.with_body do %>
A flexible online sale format that allows buyers more time to review and bid.
<% end %>
<% end %>
Slots
All slots are optional. Order is fixed regardless of the order you call with_* in.
Icon Slot
A tinted rounded-square tile. Accepts a Lucide name: or a content block for custom markup. placement: :top (default, own row) or :inline (beside the header).
Top placement
Inline placement
Icon tile beside the header.
<% c.with_icon(name: "clock") %>
<% c.with_icon(name: "headphones", color: :warning, placement: :inline) %>
<% c.with_icon do %>
<span class="text-2xl font-bold">42</span>
<% end %>
Header Slot
title: is required in-slot. Optional subtitle: and as: heading tag override (default :h3).
Strong sale-day support
Dedicated support from listing to final result.
<% c.with_header(title: "Timed Online") %>
<% c.with_header(title: "Support", subtitle: "Dedicated support from listing to final result.") %>
<% c.with_header(title: "Section", as: :h2) %>
Body Slot
Muted description text. Use the text: param or a block — same block-or-text pattern as Alert's message slot.
Livestock Selling
<% c.with_body(text: "A flexible online sale format.") %>
<% c.with_body do %>
A flexible online sale format that allows buyers more time to review and bid.
<% end %>
List Slot
Checklist rows with circled-icon tiles. with_list yields itself so you can call with_item repeatedly. A divider renders above the list automatically when a header or body precedes it.
Timed Online
- Keeps buyers active with outbid alerts
- Set and forget max bid functionality
- Custom icon per item
<% c.with_list do |l| %>
<% l.with_item(text: "Keeps buyers active with outbid alerts") %>
<% l.with_item(text: "Set and forget max bid functionality") %>
<% l.with_item(text: "Custom icon per item", icon: "star", color: :warning) %>
<% end %>
Media Slot
An image flush to the card's top edge. Corners follow the card's shape: via overflow-hidden on the root.
Olive Grove Sale
<% c.with_media(src: "/sales/olive-grove.jpg", alt: "Olive Grove yards", aspect: :video) %>
Key/Value List Rows (v0.58.0 / v0.58.1)
trailing: pushes content to a list item's right edge; leader: (list-level) draws a decorative hairline rule filling the gap. icon: false drops the icon tile span entirely, for rows that have no icon at all — a fee table, spec sheet, or pricing tier row.
Selling fees
- 1 to 250$7.80
- 251 to 1,000$6.20
- 1,001+$4.90
<%= rui_card do |c| %>
<% c.with_header(title: "Selling fees") %>
<% c.with_list(variant: :pill, leader: true) do |l| %>
<% l.with_item(text: "1 to 250", icon: false, trailing: "$7.80") %>
<% l.with_item(text: "251 to 1,000", icon: false, trailing: "$6.20") %>
<% l.with_item(text: "1,001+", icon: false, trailing: "$4.90") %>
<% end %>
<% end %>
trailing: accepts a plain string or a block for markup (the block wins if both are given) — l.with_item(text: "Status") { rui_badge(text: "Active", color: :success) }.
leader: is list-level — one flag styles every row — and off by default. Per item, the rule only draws when that item also has trailing: set; leader: true with no trailing: skips the rule for that row rather than raising, since a rule to nowhere is meaningless. It's decorative (aria-hidden="true"), so a screen reader reads the label then the value with nothing between.
icon: false (or icon: nil, treated identically) renders the row with no icon tile <span> in the DOM at all — not an empty one, not a transparent one. Omitting icon: still defaults to "check", and any truthy icon name renders byte-identical to pre-0.58.1.
Content & Divider Overrides (v0.57.0)
content_class: merges onto the card's content wrapper; divider_class: merges onto the divider rule. Both go through tailwind_merge, so the conflicting default is dropped from the class attribute rather than being out-ordered by it. Both are nil by default.
Agents
- Efficient listing tools
- National buyer reach
The gap either side of the divider is not a divider property. It is the content wrapper's inter-slot gap-4, which is why tightening it rides on content_class (gap-3) rather than divider_class. content_class is also the only way to reach a padding value between the none/sm/md/lg tiers (0/16/24/32px).
<%= rui_card(content_class: "p-5 gap-3", divider_class: "border-sky-200") do |c| %>
<% c.with_header(title: "Agents") %>
<% c.with_body(text: "A streamlined solution that supports relationships.") %>
<% c.with_list do |l| %>
<% l.with_item(text: "Efficient listing tools", icon: "clipboard-list") %>
<% end %>
<% end %>
Card Options
Card-level options control the surface, shape, padding, and semantic color.
:elevated
:outline
:flat
:soft
rui_card(
variant: :elevated, # :elevated | :outline | :flat | :soft
shape: :rounded, # :rounded | :square | :pill
padding: :md, # :none | :sm | :md | :lg
color: :primary, # semantic color for icon / soft / footer / list
href: nil, # whole-card link mode
divider: true, # divider above list slot
class: nil
) do |c|
# ...slots
end
Whole-Card Link
When href: is set, the root element becomes an <a> with hover shadow lift. aria-label is set from the header title or shortcut title: param. The footer degrades to styled text — never a nested <a>.
<%= rui_card(href: "/lots/482") do |c| %>
<% c.with_header(title: "Lot 482 — Angus Steers") %>
<% c.with_body(text: "45 head, 380-420kg, Wagga Wagga NSW.") %>
<% c.with_footer(text: "View lot", href: "/lots/482") %>
<% end %>
Shortcut API
For the common icon + heading + description case, skip the block entirely. href: combines with the shortcut too.
Advertise with Us
<%= rui_card(title: "Advertise with Us", description: "Reach the people driving Australian agriculture", icon: "megaphone") %>
<%= rui_card do |c| %>
<% c.with_icon(name: "megaphone") %>
<% c.with_header(title: "Advertise with Us") %>
<% c.with_body(text: "Reach the people driving Australian agriculture") %>
<% end %>
Reference Recipes
Four common card shapes from real-world usage.
Full card
Icon tile → heading → description → divider → checklist → footer bar.
Timed Online
- Keeps buyers active with outbid alerts
- Set and forget max bid functionality
Compact horizontal
Icon tile inline beside title and description. No divider, no footer.
Strong sale-day support
Dedicated support from listing to final result.
Service CTA card
Header + body + highlights list + CTA footer — no icon tile.
Livestock Selling
- National buyer network
- Weekly market reports
API Reference
rui_card
Flexible card with six optional slots and a shortcut API for icon + heading + description
| Parameter | Type | Default | Description |
|---|---|---|---|
| title | String | — | Shortcut API — renders header title when no block slot is set |
| description | String | — | Shortcut API — renders body text when no block slot is set |
| icon | String/Symbol | — | Shortcut API — Lucide icon name for the icon tile |
Appearance
Visual styling options
| Parameter | Type | Default | Description |
|---|---|---|---|
| variant | Symbol |
:elevated
|
Card surface style
:elevated
:outline
:flat
:soft
|
| shape | Symbol |
:rounded
|
Corner radius
:rounded
:square
:pill
|
| padding | Symbol |
:md
|
Inner padding
:none
:sm
:md
:lg
|
| color | Symbol |
:primary
|
Semantic color for icon tile, soft variant, footer link, and list tiles |
| divider | Boolean |
true
|
Show divider above list when header or body precedes it |
| class | String | — | Additional root classes merged via tailwind_merge |
Linking
Whole-card link mode
| Parameter | Type | Default | Description |
|---|---|---|---|
| href | String | — | Wraps the whole card in a single <a> with hover shadow lift and aria-label from title |
Key/Value List Rows (v0.58.0 / v0.58.1)
with_item(trailing:) and with_list(leader:) build label/value rows with a rule between; with_item(icon: false) drops the icon tile entirely for rows that have no icon at all
| Parameter | Type | Default | Description |
|---|---|---|---|
| trailing | String | — | with_item(trailing:) — content pushed to the row's right edge. Plain string or a block for markup (the block wins if both are given, same contract as Table::Row#with_cell/Kanban::Column#with_card). nil renders no trailing span, byte-identical to pre-0.58.0. |
| leader | Boolean |
false
|
with_list(leader:) — draws a decorative hairline rule (aria-hidden) filling the gap between an item's text and its trailing: content, on every row in that list. Per item, the rule only draws when that item also has trailing: set — leader: true with no trailing: skips the rule for that row rather than raising. |
| icon | String/Symbol/false/nil |
"check"
|
with_item(icon:) — false (or nil, treated identically) renders the row with NO icon tile <span> in the DOM at all, not an empty or transparent one. Any truthy icon name (including the default) renders byte-identical to pre-0.58.1. Before 0.58.1, icon: nil rendered a '?' placeholder — that was a bug, not a documented affordance. |
Content & Divider Overrides (v0.57.0)
Reach the content wrapper's padding/gap and the divider rule from the call site — nil by default, byte-identical to pre-0.57.0 output
| Parameter | Type | Default | Description |
|---|---|---|---|
| content_class | String | — | Merges onto the content wrapper via tailwind_merge. Reaches the padding (the none/sm/md/lg tier scale is 0/16/24/32px, so in-between values were unreachable) and the fixed gap-4, which is what actually controls the space either side of the divider. e.g. "p-5 gap-3". |
| divider_class | String | — | Merges onto the divider <hr> via tailwind_merge. Before 0.57.0 DIVIDER_CLASSES was a hardcoded literal, so the rule's colour and weight could not be changed at all. e.g. "border-sky-100 border-t-2". |
Slots
Content slots for customizing component parts
| Slot | Description |
|---|---|
| icon | with_icon(name:, color:, placement: :top/:inline) — tinted Lucide tile or custom block content |
| header | with_header(title:, subtitle:, as: :h3) — heading and optional subtitle |
| body | with_body(text:) or block — muted description text |
| list | with_list(leader:) { |l| l.with_item(text:, icon:, color:, trailing:) } — checklist rows with circled icons; icon: false drops the tile, trailing:/leader: build key/value rows |
| media | with_media(src:, alt:, aspect: :video/:square/:wide/:none) — image flush to top edge |
| footer | with_footer(text:, href:, arrow: :right/:left/:none, class:, wrapper_class:) or block — full-width footer bar |