rails-datepicker is a Rails 8 datepicker for Ethiopian and Gregorian dates. It uses Stimulus 3, Turbo, vanilla JavaScript, and Tailwind CSS 4. Users can change the displayed calendar without changing the selected day; forms always submit a Gregorian ISO value (YYYY-MM-DD) that maps directly to an Active Record date column.
- Ruby 4.0 or newer
- Rails 8.0
- Stimulus 3 and Turbo
- Tailwind CSS 4
- Importmap, Bun, or another Rails JavaScript bundler
No Bootstrap, DaisyUI, jQuery, React, or Vue dependency is used.
Add the gem to the application:
gem "rails-datepicker"Then install it:
bundle install
bin/rails generate rails_datepicker:installThe generator:
- creates
config/initializers/rails_datepicker.rb; - copies the Stimulus controller and calendar module into
app/javascript; - adds the Importmap pin when
config/importmap.rbexists; - copies and imports the Tailwind CSS 4 component source.
Existing files go through the standard Rails generator collision prompt. Restart the asset watcher after installation. With Bun/esbuild, the installer rewrites the copied controller to a relative ES module import, so no package alias is needed. With Importmap, it retains the pinned module name and stimulus-rails discovers the copied controller automatically.
If the Tailwind entrypoint uses a custom path, add this line after @import "tailwindcss":
@import "./rails_datepicker.css";RailsDatepicker.configure do |config|
config.default_calendar = :ethiopian
config.default_locale = :am
config.date_format = "YYYY-MM-DD"
config.theme = :auto
config.week_start = :sunday
config.dual_dates = false
config.locale_follows_calendar = false
endValid calendars are :ethiopian and :gregorian; locales are :am and :en; themes are :auto, :light, and :dark; week starts are :sunday and :monday.
<%= form_with model: @student do |f| %>
<%= f.label :birth_date %>
<%= f.datepicker_field :birth_date,
calendar: :ethiopian,
locale: :am %>
<% end %>The visible input has no name. A hidden input named student[birth_date] contains the canonical Gregorian date, so standard strong parameters and Active Record date casting work unchanged.
Standalone helper:
<%= rails_datepicker_field(
:student,
:birth_date,
object: @student,
calendar: :ethiopian,
locale: :am
) %>Nested fields_for names and IDs are generated by the Rails builder. Standard HTML options such as required, disabled, class, data, and aria are preserved. Model validation errors are connected to the visible input with aria-describedby.
<%= f.datepicker_field :appointment_date,
calendar: :ethiopian,
locale: :am,
display_calendar: :gregorian,
date_format: "DD/MM/YYYY",
week_start: :monday,
theme: :auto,
ethiopic_numerals: true,
dual_dates: true,
locale_follows_calendar: true,
min: Date.current,
max: 2.years.from_now.to_date,
disabled_dates: holidays,
readonly: false,
inline: false,
years_before: 100,
years_after: 20 %>display_calendar: :gregorian keeps the visible result and manual input Gregorian while the popup can use either calendar. Omit it to display the result in the active calendar. The popup has a calendar dropdown showing the active calendar's flag and, unless locale_follows_calendar is set, an independent English/Amharic switch.
locale_follows_calendar: true ties the language to the calendar: the Ethiopian calendar is shown in Amharic and the Gregorian calendar in English. The separate language switch is hidden. Any locale option is ignored.
dual_dates: true shows the other calendar's day number beneath each day (for example, the Gregorian day under each Ethiopian day). The first day of the other calendar's month is highlighted, and screen readers hear both dates.
min, max, and every disabled_dates entry are interpreted as canonical Gregorian dates. The year dropdown is anchored to today and the selected date (it does not drift as you navigate), and it is clamped to min/max. Months outside the range are disabled in the month dropdown, and the previous/next and Today buttons are disabled when they would lead to unavailable dates. Manual input must exactly match date_format; invalid and unavailable dates use native constraint validation. readonly: true disables manual editing while retaining calendar selection; clicking a read-only field opens the calendar. inline: true keeps the calendar visible.
When Simple Form is installed, the engine loads RailsDatepickerInput:
<%= simple_form_for @student do |f| %>
<%= f.input :birth_date,
as: :rails_datepicker,
calendar: :ethiopian,
locale: :am %>
<% end %>Simple Form is optional and is not a runtime dependency.
Each selection change dispatches a bubbling rails-datepicker:change event on the component with both calendar representations:
element.addEventListener("rails-datepicker:change", ({ detail }) => {
detail.iso // "2023-09-12" ("" when cleared)
detail.gregorian // { year: 2023, month: 9, day: 12 }
detail.ethiopian // { year: 2016, month: 1, day: 1 }
detail.formatted // the visible input value
})In Stimulus, use data-action="rails-datepicker:change->my-controller#update". Standard input/change events are still dispatched on the hidden and visible inputs.
- The input exposes combobox/dialog relationships and validation state.
- Arrow keys move by day or week and skip over unavailable dates; Home/End move within a week; Page Up/Down move by month and Shift+Page Up/Down by year, keeping the day of the month; Escape closes and restores focus.
- Every day is announced with its full date (weekday, month, day, year), plus whether it is today or unavailable. Days are grouped into grid rows, and month changes are announced through a polite live region.
- Focus starts on the selected date, today, or the first enabled date. Reopening the popup returns to the selected month, and tabbing out of the component closes it.
- The popover flips above the field and clamps horizontally to remain inside the viewport.
- Every controller instance owns and removes its document/window listeners in
disconnect(), including Turbo Frame replacement and Turbo cache navigation. - Inline and popup instances are independent and work inside modal dialogs.
Conversion is anchored at 2015-01-01 Ethiopian / 2022-09-11 Gregorian and advances by the Ethiopian calendar's own year lengths, so it remains accurate across Gregorian century boundaries. Ethiopian leap years satisfy year % 4 == 3; Pagume has six days in those years and five otherwise. In the modern 1900–2098 range, Ethiopian New Year falls on September 12 when the following Gregorian year is a leap year, and September 11 otherwise.
The Ruby and JavaScript suites include reference boundaries such as:
| Ethiopian | Gregorian |
|---|---|
| 2015-01-01 | 2022-09-11 |
| 2015-13-06 | 2023-09-11 |
| 2016-01-01 | 2023-09-12 |
Round-trip tests cover representative dates for every year from 1900 through 2100, with explicit reference assertions around the 2099/2100 century boundary.
bundle exec rake test
npm test
RUN_SYSTEM_TESTS=1 bundle exec ruby -Itest test/system/datepicker_test.rbThe test suite includes a Rails 8 dummy application and covers conversion, leap years, Pagume, New Year boundaries, strict input, restrictions, canonical form submission, Simple Form, multiple instances, Turbo Frame reconnection, keyboard navigation, and light/dark/inline rendering.
A matching v*.*.* tag runs the release workflow: it verifies the tag against the gem and npm versions, runs the JavaScript and Ruby suites on Ruby 4.0, builds and validates the gem, publishes it to RubyGems, and creates a GitHub release with the .gem attached.
Required secret in the release environment (Settings, Environments, release):
| Secret | Description |
|---|---|
RUBYGEMS_API_KEY |
Push-only key scoped to rails-datepicker from an account with MFA enabled |
# Release
git tag v0.2.0
git push origin v0.2.0Build locally with gem build rails-datepicker.gemspec and review the contents with gem contents --show-install-dir rails-datepicker after installing the artifact. The gemspec requires MFA on the RubyGems account.
MIT.