Cards API
Per-option reference for cards. For task-oriented documentation and worked examples, see the Cards guide.
Options are class attributes set on the card class. Unless noted, any option can also be a Proc — it is evaluated through Avo::ExecutionContext, where you gain access to all its attributes plus parent, resource, dashboard, card, arguments, and params.
# app/avo/cards/users_metric.rb
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = "users_metric"
self.label = "Users count"
# options listed below
endBase settings
Available on every card type.
-> self.id
The card's unique identifier. Used to build the card's Turbo frame and paths, so it must be unique across the parent.
self.id = "users_metric"- Type: String
- Default:
nil - Required: yes
-> self.label
The card's title, rendered at the top of the card.
self.label = "Users count"- Type: String or Proc
- Default:
nil
To localize this, prefer the avo.card_translations.<class_path>.label key — see Localization. A Proc remains the fallback and is resolved on every render in the requesting user's locale:
self.label = -> { I18n.t("avo.cards.users_metric.label", default: "Users count") }The String default: matters — without one, a locale you have not translated renders Translation missing: … as the title. See Localization.
-> self.description
Renders directly under the title as a subtitle. Use it for context that should always be visible at a glance.
self.description = "Across all teams and workspaces"- Type: String or Proc
- Default:
nil
Like self.label, a Proc is resolved per request, so -> { I18n.t("avo.cards.users_metric.description", default: "Across all teams and workspaces") } follows the user's locale. See Localization.
-> self.discreet_description
Renders a small info icon in the bottom-right corner of the card; hovering it shows the text as a tooltip. Use it for secondary context — methodology notes, data-source disclaimers, definitions.
self.discreet_description = "Counts only active, non-deleted users."- Type: String or Proc
- Default:
nil
Like self.label, a Proc is resolved per request, so -> { I18n.t("avo.cards.users_metric.discreet_description", default: "Counts only active, non-deleted users.") } follows the user's locale. See Localization.
-> self.cols
How many columns of the parent's grid the card spans.
self.cols = 2- Type: Integer (or Proc returning one)
- Default:
1 - Values:
1to6
-> self.rows
How many rows of the parent's grid the card spans, which is how tall a partial, html, metric, or chartkick card stands.
On table and list cards it is a ceiling instead: the card is as tall as the records it holds and grows no further than the step, with anything past it scrolling inside the card. A card set to rows = 6 that returns three records is three records tall, not six rows of grid.
self.rows = 2- Type: Integer (or Proc returning one)
- Default:
1 - Values:
1to12
-> self.display_header
Whether the card header (label and ranges dropdown) is rendered. Set it to false to let embedded content — a map, an iframe — fill the whole card flush to its edges.
self.display_header = falseAs a Proc it has access to card, parent, dashboard (nil when the parent is a resource), and resource (nil when the parent is a dashboard).
- Type: Boolean (or Proc returning one)
- Default:
true
-> self.visible
Controls whether the card renders. As a Proc it has access to context, params, parent, dashboard (nil when the parent is a resource), resource (nil when the parent is a dashboard), and card.
self.visible = -> { current_user.admin? }- Type: Boolean or Proc
- Default:
true
-> self.refresh_every
Auto-refreshes the card on an interval. Pass a duration; Avo reloads the card in the background.
self.refresh_every = 10.minutes- Type:
ActiveSupport::Duration(or seconds as an Integer) - Default:
nil
WARNING
On table/list cards a refresh reloads the whole card, resetting the scroll position of a tall table. Prefer it on short cards.
INFO
This controls the interval only. A card can also carry a manual refresh control — see self.refresh_button. Refreshing by hand restarts this countdown.
-> self.refresh_button
Renders a manual refresh control on the card, so a viewer can bring that one card up to date without reloading the page.
self.refresh_button = true- Type: Boolean
- Default:
false
The control sits at the end of the card header, or in the card's top corner when it renders no header. See Refresh a card on demand for how it behaves.
INFO
Same name and same default as the dashboard's refresh_button, which refreshes every card at once. Setting it on a dashboard does not turn it on for that dashboard's cards — the two are independent.
-> self.loading
avo-dashboards >= v4.3.4When the card runs its query. Takes the :lazy and :manual values of the loading option on tabs and association fields, without its Hash form or auto_load_for.
self.loading = :manual| Value | Behavior |
|---|---|
:lazy | Loads when the card scrolls into view. |
:manual | Renders the card's title, description, and a Load button in place of its content. The query runs only when the button is clicked. |
- Type: Symbol or String
- Default:
:lazy - Validation: raises
ArgumentErrorfor any other value - i18n key:
avo.load_item("Load %{item}"), oravo.cards.load("Load card") when the card has no label
-> self.cache_for
Caches the result of the card's query for that duration. Within the window the query is skipped entirely and the stored result is replayed — useful for cards whose query is expensive.
class Avo::Cards::UsersCount < Avo::Cards::MetricCard
self.cache_for = 5.minutes
def query
result User.where(active: true).count
end
endAvo caches through Avo.configuration.cache_store. The key is scoped to the current user, tenant and locale, so a card querying current_user never serves one user's data to another, and a card whose result depends on the language never serves one locale's text to another. In full, it covers:
| Part | Why |
|---|---|
| Card class, parent, and position | Separates cards, and two registrations of the same class |
range and the dashboard's global range | Each range is its own result |
| Current user and tenant | Keeps per-user and per-tenant queries apart |
| Current locale | A localized card, or a query returning translated content, caches per language |
| The resource's view and record | A resource card caches per record and per view |
- Type:
ActiveSupport::Duration(or seconds as an Integer), or a Proc returning one - Default:
nil(no caching)
INFO
The card's refresh control bypasses the cache: clicking it re-runs the query and rewrites the entry, so the card never animates over a stale value. Because the key is per user, that only busts the clicker's own entry. self.refresh_every polling does not bypass it — pair the two to poll a card often while querying rarely.
WARNING
Cards with no query — HTML and partial cards — build their content at render time, so cache_for does nothing for them. Wrap the markup in Rails' own cache block instead.
INFO
arguments is deliberately not part of the key. It's fixed at registration time, so a card's position already separates two registrations of the same class.
If your query reads something the key doesn't cover, override cache_key:
def cache_key
super + [Current.account.id]
endReturning a narrower key is how you opt into sharing one entry across users.
WARNING
Outside production Avo.configuration.cache_store defaults to a file store under tmp/cache, which isn't shared between machines — on a multi-server staging environment each server caches on its own. Set config.cache_store in the Avo initializer to share it.
Ranges
Let the user query data across different time ranges via a dropdown in the card header.
-> self.ranges
The options shown in the range dropdown. The value is passed straight to Rails' options_for_select, so it behaves like a select_tag. Integers are treated as a number of days; other strings ("TODAY", "MTD", "ALL", …) are passed through as-is and it's up to your query to interpret them.
self.ranges = {
"7 days": 7,
"30 days": 30,
"Year to date": "YTD",
All: "ALL"
}- Type: Array or Hash
- Default:
[]
-> self.initial_range
The range selected by default when the card first loads. Falls back to the first entry of ranges.
self.initial_range = 30- Type: matches a
rangesvalue - Default:
nil
Metric card
Avo::Cards::MetricCard displays a single big number returned from query/result.
-> self.prefix
Text rendered before the value (e.g. a currency symbol).
self.prefix = "$"- Type: String or Proc
- Default:
nil
-> self.suffix
Text rendered after the value (e.g. %).
self.suffix = "%"- Type: String or Proc
- Default:
nil
-> self.format
Formats the value returned by result before display. The block runs through Avo::ExecutionContext with value (the raw result) in scope and Rails' NumberHelper plus Avo::ApplicationHelper mixed in, so helpers like number_to_currency and number_to_social are available directly.
self.format = -> { number_to_social value, start_at: 1_000 }- Type: Proc
- Default:
-> { number_to_social value.to_i, start_at: 10_000 }
Chartkick card
Avo::Cards::ChartkickCard renders a chart via the chartkick gem, which you must add to your Gemfile.
-> self.chart_type
The kind of chart to render.
self.chart_type = :area_chart- Type: Symbol
- Default:
nil - Values:
:line_chart,:pie_chart,:column_chart,:bar_chart,:area_chart,:scatter_chart
-> self.chart_options
Extra chartkick options merged on top of Avo's defaults — use it for anything not covered by flush, legend, and friends. As a Proc it has access to parent, arguments, and result_data.
self.chart_options = {
library: { plugins: { legend: { display: true } } }
}- Type: Hash or Proc returning a Hash
- Default:
{}
INFO
The class attribute is chart_options. chartkick_options is the internal, read-only method that merges your chart_options into Avo's defaults — you don't set it.
-> self.flush
Offsets chartkick's built-in padding so the chart sits flush inside the card. Set it to false to render the chart with its default padding and unlock scale, legend, legend_on_left, and legend_on_right.
self.flush = false- Type: Boolean
- Default:
true
-> self.legend
Shows the chart legend. Takes effect once flush is false.
self.legend = true- Type: Boolean
- Default:
false
-> self.scale
Shows the chart's axis scales. Takes effect once flush is false.
self.scale = true- Type: Boolean
- Default:
false
-> self.legend_on_left
Positions the legend on the left. Takes effect once flush is false.
self.legend_on_left = true- Type: Boolean
- Default:
false
-> self.legend_on_right
Positions the legend on the right. Takes effect once flush is false.
self.legend_on_right = true- Type: Boolean
- Default:
false
Partial card
Avo::Cards::PartialCard renders a custom partial.
-> self.partial
Path to the partial that renders the card's body.
self.partial = "avo/cards/map_card"- Type: String
- Default:
nil
HTML card
Sinceavo-dashboards >= v4.1Avo::Cards::HtmlCard builds its body from Ruby instead of a partial file. It has no extra class attributes — you implement a body method.
-> body
Instance method returning the card's content. Every view helper (tag, safe_join, link_to, number_to_currency, render, main_app, …) is available directly on the card. The return value is resolved by shape:
| Return value | Rendered as |
|---|---|
ActiveSupport::SafeBuffer (tag helpers, explicit render) | passed through untouched |
String | compiled as an inline ERB template, with card as a local |
anything else (a component instance, a {partial:} hash) | passed to render |
def body
tag.div class: "px-4 py-2" do
tag.strong(number_to_currency(149.99))
end
end- Type: instance method
- Required: yes — raises
NotImplementedErrorif undefined
WARNING
Inline template strings go through ActionView, so interpolated ERB values are HTML-escaped as usual. Never build the template string itself from user input — pass dynamic values through ERB tags or locals:, not Ruby string interpolation.
Data cards (table & list)
Sinceavo-dashboards >= v4.1Avo::Cards::TableCard and Avo::Cards::ListCard both inherit from Avo::Cards::DataCard: you declare columns with fields and return records from query. The table card renders a <table> with column headers; the list card renders a <ul> with no headers, the first field as each row's primary content and the rest trailing at the end edge.
The records you return must have an Avo resource registered for their model — cells render through it using the same components as the resource <Index /> view.
-> self.fields
Instance method declaring the card's columns, using the same field DSL as resources. Each field becomes one cell per row; every field type and option (format_using, link_to_record, badge options:, computed blocks, …) behaves as it does on an index table.
def fields
field :name, as: :text, name: "User", link_to_record: true
field :active, as: :badge, options: {success: "Active"} do
record.active? ? "Active" : "Inactive"
end
end- Type: instance method
- Note: an invalid field configuration raises
ArgumentError
-> self.query
Instance method returning the records, wrapped in result. There is no pagination or sorting — cap the row count with limit. The selected range is available as range.
def query
result User.order(created_at: :desc).limit(10)
end- Type: instance method
-> self.row_url
When set, every row becomes a link. The block runs per row with record in scope and returns either a URL string or a Hash — following the same semantics as discreet information.
self.row_url = -> {
{url: record_path(record), target: :_blank, tooltip: "View #{record.name}"}
}- Type: Proc
- Default:
nil - Returns: a URL String, or
{url:, target:, tooltip:}(each value may itself be a Proc receivingrecord) - Values:
urlacceptshttp,https,mailto, and relative URLs; anything else (e.g.javascript:) is ignored
-> self.density
Vertical spacing of the rows.
self.density = :tight- Type: Symbol (or Proc returning one)
- Default: the global
config.density, or:normal - Values:
:tight,:normal,:relaxed
-> self.empty_message
Message shown when query returns no rows.
self.empty_message = "No sign-ups this week"- Type: String or Proc
- Default: the translated default
- i18n key:
avo.no_item_found("No record found")
Registration overrides
When you register a card on a parent you can override its settings inline, without editing the card class. This is how you reuse one card class with different labels, ranges, or queries.
-> card
Registers a card on a dashboard or resource. Every keyword overrides the card's own attribute for that registration.
def cards
card Avo::Cards::UsersCount,
label: "Active users",
description: "Active users count",
cols: 2,
rows: 2,
visible: -> { true },
refresh_every: 2.minutes,
cache_for: 5.minutes,
chart_options: {library: {plugins: {legend: {display: true}}}},
arguments: {active_users: true}
end- Overridable keys:
label,description,discreet_description,cols,rows,refresh_every,cache_for,visible,chart_options,arguments
-> arguments
Arbitrary data forwarded from the registration to the card, readable as arguments inside the card's methods. Use it to parameterize one card class instead of duplicating it.
# on the parent
card Avo::Cards::UsersCount, arguments: {active_users: true}
# in the card
def query
scope = User
scope = scope.active if arguments[:active_users].present?
result scope.count
end- Type: Hash
- Default:
{}
Dividers
Separate cards with a divider, declared in the cards method.
-> divider
Adds a divider between cards. With a label it shows text; with invisible: true it adds spacing but draws no line or label.
def cards
card Avo::Cards::ExampleColumnChart
divider label: "Custom partials"
card Avo::Cards::MapCard
endlabel: String — text shown on the divider. Defaultnil.invisible: Boolean — whentrue, renders no border or label. Defaultfalse.visible: Boolean or Proc — conditionally show the divider. Defaulttrue. As a Proc it has access tocontext,params,parent,dashboard, andresource.
Standalone card components
Sinceavo-dashboards >= v4.3.2View components that draw a card from plain values, for pages with no dashboard or resource behind them, such as a custom tool. See Show a card in a custom tool.
-> Avo::Cards::StandaloneCardComponent
The card's chrome: header, ranges dropdown, discreet description, and grid sizing. The block renders as the card's body.
<%= render Avo::Cards::StandaloneCardComponent.new(label: "Recent visitors", ranges: [7, 30], range: params[:range]) do %>
<%= render Avo::Cards::MetricComponent.new(value: @visitors) %>
<% end %>| Argument | Type | Default | Behavior |
|---|---|---|---|
label | String | nil | The card's title. |
description | String | nil | Subtitle under the title. |
discreet_description | String | nil | Text for the info icon tooltip in the bottom-end corner. |
ranges | Array or Hash | [] | Options for the ranges dropdown, in the shape options_for_select takes. The dropdown is hidden when empty. |
range | String or Integer | nil | The selected range. |
range_param | Symbol or String | :range | The query param a picked range is written to. |
display_header | Boolean | true | Set to false to drop the header even when label, description, or ranges are set. |
cols | Integer, 1 to 6 | nil | Grid columns the card spans. Setting cols or rows makes the card a grid item; the other one defaults to 1. |
rows | Integer, 1 to 12 | nil | Grid rows the card spans. |
- Slot:
with_header_actionsrenders content in the header, before the ranges dropdown. - Header: rendered only when
display_headeristrueand at least one oflabel,description, orrangesis present.
-> Avo::Cards::MetricComponent
A metric card's value with its prefix and suffix. The value is printed as given, so format it before passing it in.
<%= render Avo::Cards::MetricComponent.new(value: number_to_social(@revenue), prefix: "$") %>| Argument | Type | Default |
|---|---|---|
value | Any | required |
prefix | String | nil |
suffix | String | nil |