Card
Live Demo: Card
Open the demo in a separate tab.
Live Demo: Cards
Open the demo in a separate tab.
A Card is a container for related content.
It provides a visual boundary around a group of components so that information, controls, and other content can be presented as a single coherent unit. A Card does not own the meaning of its contents, does not own application state, and does not decide what the application should do with the information inside it.
Cards are intentionally simple. They provide structure, presentation, composition, and optional interaction without becoming an application-specific abstraction.
The Card provides the surface. Its contents provide the meaning.
Basic Card
The simplest Card contains one piece of content.
#![allow(unused)]
fn main() {
card()
.children(text(localize("user.profile")))
}
More commonly, a Card contains several components:
#![allow(unused)]
fn main() {
card()
.children([
text(localize("user.name")),
text(localize("user.email")),
button().text(localize("user.edit")),
])
}
The Card does not need to understand what the content represents. It simply provides a container in which the components can be composed.
This makes the Card useful for dashboards, settings, profiles, documents, agent activity, data records, notifications, and almost any other interface where related information benefits from a visual boundary.
Composition
children() is the fundamental composition mechanism.
A Card can contain a single value:
#![allow(unused)]
fn main() {
card()
.children(text(localize("user.profile")))
}
Or many values:
#![allow(unused)]
fn main() {
card()
.children([
text(localize("user.profile")),
text(localize("user.description")),
button().text(localize("user.edit")),
])
}
Those children can themselves be Cards or any other Beverly components:
#![allow(unused)]
fn main() {
card()
.children([
card()
.children(text(localize("user.account"))),
card()
.children(text(localize("user.preferences"))),
])
}
There is no separate composition system for Cards. A Card uses the same children() primitive as the rest of Beverly.
Build the primitive once. Compose it everywhere.
Named Sections
Cards implement .header(...), .body(...), and .footer(...) as top,
middle, and bottom content regions. Regions always render in that order,
regardless of builder-call order. Each accepts one UiElement or an iterable
of elements; repeated calls append to the same region.
#![allow(unused)]
fn main() {
card()
.header(text(localize("user.profile")))
.body([
text(localize("user.name")),
text(localize("user.email")),
])
.footer(button().text(localize("user.edit")))
}
These sections are implemented by Beverly as child entities carrying
CardHeader, CardBody, and CardFooter markers. They do not replace
children(): when sections are used, ordinary children join the body before
explicit body content. The body grows to fill available card height, keeping
the header at the top and footer at the bottom. Without sections, ordinary
children retain their existing direct-child layout.
The concrete fluent constructor is UiElement::card(), available through
beverly::prelude::*:
#![allow(unused)]
fn main() {
UiElement::card()
.header(text("Account"))
.body(text("Account information"))
.footer(button().text("Save"))
}
The same sections can accept one value or multiple values:
#![allow(unused)]
fn main() {
card()
.header([
icon("user"),
text(localize("user.profile")),
])
.body([
text(localize("user.name")),
text(localize("user.email")),
badge(localize("user.active")),
])
}
This gives applications a useful vocabulary for common layouts while keeping the underlying composition model simple.
Styling
A Card is commonly configured through chained style methods.
#![allow(unused)]
fn main() {
card()
.padding(16)
.radius(12)
.shadow(4)
.children([
text(localize("user.profile")),
text(localize("user.description")),
])
}
Styling remains part of the component configuration rather than requiring a separate stylesheet or styling abstraction.
Because these methods compose naturally, the same Card primitive can be used across an application’s design system:
#![allow(unused)]
fn main() {
card()
.padding(24)
.radius(16)
.children([
icon("database"),
text(localize("database.status")),
badge(localize("database.connected")),
])
}
The exact visual treatment can belong to the application’s design system while the underlying Card remains the same reusable primitive.
Card State
Cards can respond to state when the application needs state-specific presentation.
#![allow(unused)]
fn main() {
UiElement::card()
.hover(|style| style.scale(1.02))
.success_style(|style| style.border_width(2).border_color("#16a34a"))
.children([text("Remote resource")])
}
Hover styling reads pointer interaction on the Card or its descendants. A Card
is not focusable by default, so focused styles require the app to make it
focusable and request focus. To use app-owned status styling, attach a
FormStatusState to the Card entity:
#![allow(unused)]
fn main() {
let card = UiElement::card()
.success_style(|style| style.border_color("#16a34a"))
.body(text("Remote resource"))
.spawn_in_context(&mut context);
context.world.entity_mut(card).insert(FormStatusState {
status: FormStatus::Success,
});
}
Card style callbacks do not accept model predicates. Set status on the entity
from app code, or attach a SemanticNode/interaction component for its
corresponding interaction state. Card itself does not have a disabled policy.
Interactive Cards
A Card is not inherently interactive.
If an application wants a Card to respond to pointer or keyboard interaction, that interaction should be explicitly declared rather than being assumed from the component’s appearance.
#![allow(unused)]
fn main() {
card()
.label(localize("document.card.label"))
.aria(localize("document.card.description"))
.on("click", Command::Document::Open)
.children([
text(Document::title),
text(Document::summary),
])
}
The important part is that the Card still does not know what Command::Document::Open means. The Card produces a UI Event, and application wiring determines what happens next.
For a UI-specific interaction, the Card can expose an abstract UI Event:
#![allow(unused)]
fn main() {
card()
.label(localize("document.card.label"))
.aria(localize("document.card.description"))
.on("click_start", Ui::DocumentCard::Pressed)
.on("click_end", Ui::DocumentCard::Released)
.on("click", Ui::DocumentCard::Clicked)
}
The distinction is the same as with Button:
UI interaction
↓
Ui::DocumentCard::Clicked
↓
application wiring
↓
Command::Document::Open
The Card does not need to know that it represents a document. It only exposes the interface interaction.
Accessibility
A purely visual Card that only contains content does not automatically become an interactive control simply because it looks like one.
When a Card is interactive, its accessible identity and description should be explicit:
#![allow(unused)]
fn main() {
card()
.label(localize("document.card.label"))
.aria(localize("document.card.description"))
.on("click", Command::Document::Open)
.children([
text(Document::title),
text(Document::summary),
])
}
This is particularly important for Cards that visually resemble buttons, links, selectable records, or other controls.
The visual appearance should not be the only indication of what the Card does. Its semantics should be represented explicitly in the component configuration.
A visual container is not automatically an interactive control.
Data-Driven Cards
Cards become especially useful when their contents come from application data.
#![allow(unused)]
fn main() {
card()
.header(text(User::name))
.body([
text(User::email),
badge(User::status),
])
}
The Card does not own User. It receives a projection of application state and presents it.
This means the same Card structure can represent many records:
#![allow(unused)]
fn main() {
users.iter()
.map(|user| {
card()
.header(text(user.name()))
.body([
text(user.email()),
badge(user.status()),
])
})
}
The Model remains the source of truth. The View turns that state into Cards.
This separation is important for AI-generated applications because the component does not need to understand where its data came from. The same Card can present database records, API responses, local files, streams, or AI-generated data.
Conditional Content
Cards can also compose content based on application state.
#![allow(unused)]
fn main() {
card()
.children([
text(User::name),
if User::is_active {
badge(localize("user.active"))
} else {
badge(localize("user.inactive"))
},
])
}
The Card remains responsible only for presenting the resulting composition.
The decision about whether a user is active belongs to application state or domain logic, not to the Card itself.
Loading and Async State
A Card can present loading state without implementing the operation that is loading.
#![allow(unused)]
fn main() {
card()
.children([
text(localize("document.title")),
text(Document::summary),
])
.submitting_style(|style| style.opacity(0.5))
}
A richer presentation can replace or augment its contents:
#![allow(unused)]
fn main() {
card()
.children([
text(Document::title),
text(Document::summary),
])
.submitting(|| [
spinner(),
text(localize("document.loading")),
])
}
The Card does not perform the asynchronous operation. It presents the state produced by the application.
This follows the same principle used throughout Beverly:
The Model owns the state. The View presents the state. Events describe interaction. Controllers connect interaction to application behavior.
Error and Success Presentation
Cards can present application state such as errors or successful operations.
#![allow(unused)]
fn main() {
card()
.error(|errors| text(errors.join("\n")))
.success(|| [
icon("check"),
text(localize("document.saved")),
])
}
The messages should come from the application’s localization and state mechanisms rather than being hard-coded into the component.
The Card presents the meaning supplied by the application; it does not decide what constitutes an error or success.
Cards Within Cards
Because Cards are ordinary composable components, they can contain other Cards.
#![allow(unused)]
fn main() {
card()
.header(text(localize("dashboard")))
.body([
card()
.children([
text(localize("users")),
text(User::count),
]),
card()
.children([
text(localize("documents")),
text(Document::count),
]),
])
}
This is useful for dashboards and hierarchical interfaces, but it is not a special Card feature. It is simply the result of components being composable.
children() provides the common language.
Card Events
When interaction is required, Card Events should remain about the Card’s UI lifecycle.
#![allow(unused)]
fn main() {
card()
.label(localize("document.card.label"))
.aria(localize("document.card.description"))
.on("click_start", Ui::DocumentCard::Pressed)
.on("click_end", Ui::DocumentCard::Released)
.on("click", Ui::DocumentCard::Clicked)
}
These Events can be used independently of application Commands.
For example, a pressed Event could control a visual effect:
#![allow(unused)]
fn main() {
.on("click_start", Ui::DocumentCard::Pressed)
}
while the completed interaction can be connected to application behavior:
#![allow(unused)]
fn main() {
.on("click", Command::Document::Open)
}
This keeps the component’s vocabulary independent from the application’s domain vocabulary.
The Card reports interaction. The application decides what the interaction means.
The Card Contract
The Card has a deliberately small conceptual contract:
Create. Configure. Compose. Present. Optionally interact.
A Card provides:
- visual structure
- spacing and presentation
- composition through
children() - optional semantic sections such as
header,body, andfooter - state-specific presentation
- accessibility semantics when acting as an interactive control
- UI Events when interaction is required
It does not own:
- application data
- business rules
- domain validation
- application Commands
- database operations
- API operations
- agent behavior
Those concerns belong elsewhere in the architecture.
The result is a component that can be reused from a simple settings page to a high-throughput enterprise dashboard without changing what a Card fundamentally is.
The Card is a container, not an application.