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.

Requirements
- Avo 4
avo-calendar_viewgem (paid add-on)
Installation
1. Install the gem
# 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:
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
endThat adds the calendar to the view switcher next to table, grid, and map.

Configuration
Configure which attributes drive the calendar with calendar_view:
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
titleattribute)
-> week_start
Which day the week starts on.
- Type: Symbol (
:mondayor: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 (
:monthor: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'sShowpage:edit— the record'sEditpageType: Symbol (
:preview,:show, or:edit)Default:
:preview
All-day and hour-based events
The event kind is inferred from the column type:
- a
datecolumn renders all-day chips - a
datetimecolumn renders hour-based chips with anHH:MMlabel
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.
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
endMonth 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.

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.
Navigation and filtering
- The header's arrows navigate by month or week; the anchor date is carried in the
calendar_datequery 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:
# 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
endCollection 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.