Skip to content

Calendar view ​

Display a resource's records on a month or week calendar, right on the Index view. Hour-based events show their start time behind a dot of their color — and stretch to their duration in the week view — all-day events render as filled bars (and land in the week view's all-day lane), and multi-day events span every day they cover as filled pills. Clicking an event opens a preview popover with the fields marked show_on: :preview.

The month calendar — timed event chips, a month-long all-day bar, a multi-day event spanning three days, and the Month/Week switcher.

Requirements ​

  • Avo 4
  • avo-calendar_view gem (paid add-on)

Installation ​

1. Install the gem ​

ruby
# Gemfile
gem "avo-calendar_view", source: "https://packager.dev/avo-hq/"

Then run bundle install.

2. Enable the view on a resource ​

Add :calendar to the resource's view_types:

ruby
class Avo::Resources::Event < Avo::BaseResource
  self.view_types = [:table, :calendar] 
  self.default_view_type = :calendar # optional 

  def fields
    field :id, as: :id
    field :name, as: :text
    field :starts_at, as: :date_time
    field :ends_at, as: :date_time
  end
end

That adds the calendar to the view switcher next to table, grid, and map.

The Events resource in calendar view — the month grid with timed chips, a multi-day event spanning four days, and the Month/Week switcher.

Configuration ​

Configure which attributes drive the calendar with calendar_view:

ruby
class Avo::Resources::Event < Avo::BaseResource
  self.view_types = [:table, :calendar]
  self.calendar_view = {
    starts_at: :starts_at,       # attribute holding the event date/time
    ends_at: :ends_at,           # optional; enables multi-day events
    title: -> { record.name },   # optional; defaults to the record's title
    week_start: :monday,         # or :sunday (default :monday)
    default_period: :month,        # or :week — which grid opens first
    hide_weekends: false,        # true renders Monday–Friday only
    color: -> { record.status == "urgent" ? :red : :blue }, # optional chip color
    on_click: :preview           # or :show / :edit — what clicking an event opens
  }
end

starts_at

The attribute holding the event's date or time. This must be an attribute name, not a proc — the calendar queries the visible month's date range directly, so every record in the month is shown regardless of index pagination.

  • Type: Symbol
  • Default: the resource's first date or date-time field, falling back to created_at

ends_at

The attribute holding the event's end. Setting it enables multi-day events; records whose end is nil render as single-day events.

  • Type: Symbol
  • Default: nil (all events are single-day)

title

A proc returning the chip label, evaluated with record (and resource) in scope.

  • Type: Proc
  • Default: the record's title (the resource's title attribute)

week_start

Which day the week starts on.

  • Type: Symbol (:monday or :sunday)
  • Default: :monday

default_period

Which grid the calendar opens on. The user can always switch with the Month/Week toggle in the header.

  • Type: Symbol (:month or :week)
  • Default: :month

hide_weekends

Renders Monday–Friday only. Events falling on a weekend are not displayed.

  • Type: Boolean
  • Default: false

color

A proc returning the chip's color token, evaluated with record in scope. Valid tokens: :blue, :green, :red, :orange, :purple, :pink, :teal, :gray — anything else renders the default chip. The color tints filled pills and sets the dot on single-day hour-based chips.

  • Type: Proc
  • Default: nil (neutral chips)

on_click

What clicking an event chip opens:

  • :preview — a popover with the record's preview fields (see Event previews)

  • :show — the record's Show page

  • :edit — the record's Edit page

  • Type: Symbol (:preview, :show, or :edit)

  • Default: :preview

All-day and hour-based events ​

The event kind is inferred from the column type:

  • a date column renders all-day chips
  • a datetime column renders hour-based chips with an HH:MM label

On the month grid, an hour-based event that fits one day renders unfilled with a dot of its color — the way calendar apps mark timed events — while all-day and multi-day events keep the filled bar.

ruby
class Avo::Resources::Holiday < Avo::BaseResource
  self.view_types = [:table, :calendar]
  # No calendar_view needed — the first date field (`date`) is picked up
  # automatically and renders as all-day events.

  def fields
    field :name, as: :text
    field :date, as: :date
  end
end

Month and week views ​

The header's Month/Week toggle switches between a month grid and a week time grid (carried in the calendar_period query param). In the week view, all-day and multi-day events sit in the all-day lane at the top, and hour-based events render as blocks stretched to their duration — an event without an end renders as one hour. Overlapping events share the day column side by side, each block showing its title under the start time. The grid scrolls under the pinned day-header row and all-day lane, and opens with the current time centred. When the week on screen is the current one, a red line runs across the day columns at the current time — a dot marking today's — and keeps itself up to date.

The week view — event blocks stretched to their duration, three overlapping events sharing a day column side by side.

Read a crowded day ​

A month cell shows as many event lanes as fit its row and collapses the rest behind a +N more toggle. Activate it and the week row expands in place to reveal every event it holds — the other rows keep their height and the page scrolls — then Show less fits it back. Expansion is view state only: navigating or reloading resets it.

Event previews ​

Clicking an event chip opens a popover with the record's preview — the fields marked show_on: :preview — and a link to the record. Cmd/Ctrl-click still opens the record directly.

If you'd rather skip the popover, set on_click: :show or on_click: :edit to navigate straight to the record's Show or Edit page. Without the popover, hovering a chip shows a tooltip with the full title, so truncated chips stay readable.

  • The header's arrows navigate by month or week; the anchor date is carried in the calendar_date query param. The Today button jumps back to the current date, keeping the period you pressed it in.
  • Filters and scopes applied on the index also apply to the calendar. Pagination does not — the calendar always shows the whole visible range (capped at 500 records).

Pressing Today on today's own range ​

A month grid bleeds a few days of its neighbours, so today can be on screen in a month it doesn't belong to — Today still takes you to its own month rather than pointing at the leaked cell.

When the grid already shows today's own month or week, though, there is nothing to fetch, and the press is answered in the browser instead of by a reload. If today has scrolled out of sight it comes back to the middle of the viewport, and then it pops: the day number and the circle behind it scale up twice while a wash of the accent drains out of the cell. A press with today already in sight skips the scroll and pops on the spot.

In the week view today's column is a pinned header that never leaves the screen, so what gets centred there is the current hour instead. Navigating is never accompanied by a pop — the arriving grid speaks for itself — and Cmd/Ctrl-click (or Shift/Alt) still opens today's range in a new tab or window, leaving the current page where it was.

Two cases have nothing to reveal, and both are quiet rather than broken: hide_weekends can drop today from the grid entirely, and visitors who ask for reduced motion get the same jumps without the animation.

Customize the calendar index ​

See the Slots guide for shared registrations, wrapping defaults, and all refresh and replacement caveats. The boundaries below are specific to Calendar.

With the updated add-on and Avo's index_slot API, add content around the calendar or wrap its built-in markup without copying its component:

ruby
# app/avo/resources/event.rb
class Avo::Resources::Event < Avo::BaseResource
  self.view_types = [:table, :calendar]

  index_slot :before_collection do
    tag.p("All times use your selected time zone.", class: "mb-4") if view_type.to_sym == :calendar
  end

  index_slot :records, replace: true do
    tag.section(default_content.call, class: "w-full event-calendar")
  end
end

Collection slots surround the whole calendar card, including its month/week controls. Records slots surround the month or week grid inside the card, leaving date navigation intact. Both support before/after additions and lazy default replacements, and refresh inside the calendar's Turbo Frame on date navigation and live search. Returning nil hides a replacement region; omitting default_content.call skips its built-in rendering.

Slots add no wrapper or styles. Use w-full on your wrapper to preserve the grid's full width inside the card's flex layout.

The resource shell already renders cards, header, title, controls, and toolbar slots. The calendar's internal date heading does not fire those hooks again. Calendar has no paginator, so pagination slots do not run. With no index records, core's empty-state branch supplies collection and records slots instead.

Calendar summaries

Slot context is the standard index context: resources contains hydrated resources from the index page, pagy describes that page, and query is the authorized index query. The calendar independently queries the visible date range, ignoring index pagination. Do not use resources.size or pagy.count as the number of events on screen. A range summary must apply its own date-range conditions to the authorized query.

INFO

There is no drag-to-reschedule yet.