Skip to content
How-to guides and worked examplesSee the guides →

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.

ruby
# app/avo/cards/users_metric.rb
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
  self.id = "users_metric"
  self.label = "Users count"
  # options listed below
end

Base 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.

ruby
self.id = "users_metric"
  • Type: String
  • Default: nil
  • Required: yes

self.label

The card's title, rendered at the top of the card.

ruby
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:

ruby
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.

ruby
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.

ruby
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.

ruby
self.cols = 2
  • Type: Integer (or Proc returning one)
  • Default: 1
  • Values: 1 to 6

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.

ruby
self.rows = 2
  • Type: Integer (or Proc returning one)
  • Default: 1
  • Values: 1 to 12

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.

ruby
self.display_header = false

As 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.

ruby
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.

ruby
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.

ruby
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

Since avo-dashboards >= v4.3.4

When 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.

ruby
self.loading = :manual
ValueBehavior
:lazyLoads when the card scrolls into view.
:manualRenders 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 ArgumentError for any other value
  • i18n key: avo.load_item ("Load %{item}"), or avo.cards.load ("Load card") when the card has no label

See Load cards only when they're needed.

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.

ruby
class Avo::Cards::UsersCount < Avo::Cards::MetricCard
  self.cache_for = 5.minutes

  def query
    result User.where(active: true).count
  end
end

Avo 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:

PartWhy
Card class, parent, and positionSeparates cards, and two registrations of the same class
range and the dashboard's global rangeEach range is its own result
Current user and tenantKeeps per-user and per-tenant queries apart
Current localeA localized card, or a query returning translated content, caches per language
The resource's view and recordA 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:

ruby
def cache_key
  super + [Current.account.id]
end

Returning 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.

ruby
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.

ruby
self.initial_range = 30
  • Type: matches a ranges value
  • 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).

ruby
self.prefix = "$"
  • Type: String or Proc
  • Default: nil

self.suffix

Text rendered after the value (e.g. %).

ruby
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.

ruby
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.

ruby
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.

ruby
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.

ruby
self.flush = false
  • Type: Boolean
  • Default: true

self.legend

Shows the chart legend. Takes effect once flush is false.

ruby
self.legend = true
  • Type: Boolean
  • Default: false

self.scale

Shows the chart's axis scales. Takes effect once flush is false.

ruby
self.scale = true
  • Type: Boolean
  • Default: false

self.legend_on_left

Positions the legend on the left. Takes effect once flush is false.

ruby
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.

ruby
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.

ruby
self.partial = "avo/cards/map_card"
  • Type: String
  • Default: nil

HTML card ​

Since avo-dashboards >= v4.1

Avo::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 valueRendered as
ActiveSupport::SafeBuffer (tag helpers, explicit render)passed through untouched
Stringcompiled as an inline ERB template, with card as a local
anything else (a component instance, a {partial:} hash)passed to render
ruby
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 NotImplementedError if 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 avo-dashboards >= v4.1

Avo::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.

ruby
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.

ruby
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.

ruby
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 receiving record)
  • Values: url accepts http, https, mailto, and relative URLs; anything else (e.g. javascript:) is ignored

self.density

Vertical spacing of the rows.

ruby
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.

ruby
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.

ruby
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.

ruby
# 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.

ruby
def cards
  card Avo::Cards::ExampleColumnChart
  divider label: "Custom partials"
  card Avo::Cards::MapCard
end
  • label: String — text shown on the divider. Default nil.
  • invisible: Boolean — when true, renders no border or label. Default false.
  • visible: Boolean or Proc — conditionally show the divider. Default true. As a Proc it has access to context, params, parent, dashboard, and resource.

Standalone card components ​

Since avo-dashboards >= v4.3.2

View 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.

erb
<%= render Avo::Cards::StandaloneCardComponent.new(label: "Recent visitors", ranges: [7, 30], range: params[:range]) do %>
  <%= render Avo::Cards::MetricComponent.new(value: @visitors) %>
<% end %>
ArgumentTypeDefaultBehavior
labelStringnilThe card's title.
descriptionStringnilSubtitle under the title.
discreet_descriptionStringnilText for the info icon tooltip in the bottom-end corner.
rangesArray or Hash[]Options for the ranges dropdown, in the shape options_for_select takes. The dropdown is hidden when empty.
rangeString or IntegernilThe selected range.
range_paramSymbol or String:rangeThe query param a picked range is written to.
display_headerBooleantrueSet to false to drop the header even when label, description, or ranges are set.
colsInteger, 1 to 6nilGrid columns the card spans. Setting cols or rows makes the card a grid item; the other one defaults to 1.
rowsInteger, 1 to 12nilGrid rows the card spans.
  • Slot: with_header_actions renders content in the header, before the ranges dropdown.
  • Header: rendered only when display_header is true and at least one of label, description, or ranges is 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.

erb
<%= render Avo::Cards::MetricComponent.new(value: number_to_social(@revenue), prefix: "$") %>
ArgumentTypeDefault
valueAnyrequired
prefixStringnil
suffixStringnil