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
-> 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
-> 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
-> 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. On table and list cards it also caps the card's height β rows past the cap scroll inside the card.
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.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 and tenant, so a card querying current_user never serves one user's data 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 |
| 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 β
Since 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) β
Since 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.