Building a Date Picker Component with Flatpickr and Stimulus
This is a follow-up to my post on pairing Rails partials with Stimulus controllers as reusable components. Here we’ll wrap Flatpickr in the same pattern so you get a consistent date picker that submits dates in the format Rails expects.
Install Flatpickr
bin/importmap pin flatpickr
Or if you’re using a bundler:
yarn add flatpickr
You’ll also need the CSS. The simplest way is a CDN link in your layout head:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
The Controller
app/javascript/controllers/datepicker_controller.js:
import { Controller } from "@hotwired/stimulus"
import flatpickr from "flatpickr"
export default class extends Controller {
static targets = ["input"]
static values = {
enableTime: { type: Boolean, default: false },
minDate: String,
maxDate: String
}
connect() {
this.picker = flatpickr(this.inputTarget, {
dateFormat: "Y-m-d",
enableTime: this.enableTimeValue,
altInput: true,
altFormat: "F j, Y",
minDate: this.minDateValue || null,
maxDate: this.maxDateValue || null
})
}
disconnect() {
this.picker.destroy()
}
}
The key is the two format options. dateFormat: "Y-m-d" sets what gets submitted to Rails — a standard 2026-02-08 string that Date.parse handles without any fuss. altInput: true with altFormat: "F j, Y" creates a second visible input showing a human-friendly format like “February 8, 2026”. The user sees one thing, the server receives another.
If enableTime is on, adjust the formats:
dateFormat: this.enableTimeValue ? "Y-m-d H:i" : "Y-m-d",
altFormat: this.enableTimeValue ? "F j, Y h:i K" : "F j, Y",
The Partial
app/views/components/_datepicker.html.erb:
<div data-controller="datepicker"
data-datepicker-enable-time-value="<%= enable_time || false %>"
<%= "data-datepicker-min-date-value=#{min_date}" if local_assigns[:min_date] %>
<%= "data-datepicker-max-date-value=#{max_date}" if local_assigns[:max_date] %>>
<%= label_tag name, label if local_assigns[:label] %>
<%= text_field_tag name, value,
data: { datepicker_target: "input" },
class: "form-control",
autocomplete: "off" %>
</div>
Usage
Basic date field:
<%= render "components/datepicker", name: "event[start_date]", value: @event.start_date %>
With a label and date constraints:
<%= render "components/datepicker",
name: "booking[check_in]",
value: @booking.check_in,
label: "Check-in date",
min_date: Date.today %>
DateTime field:
<%= render "components/datepicker",
name: "event[starts_at]",
value: @event.starts_at,
enable_time: true %>
Date Range with Two Pickers
For a start/end date pair, render two pickers and let the form handle the rest. No extra JavaScript needed — Rails receives both values as separate params.
<div class="date-range">
<%= render "components/datepicker",
name: "report[start_date]",
value: @report.start_date,
label: "From",
max_date: @report.end_date %>
<%= render "components/datepicker",
name: "report[end_date]",
value: @report.end_date,
label: "To",
min_date: @report.start_date %>
</div>
If you want the pickers to constrain each other dynamically (selecting a start date updates the end picker’s min date), that’s where you’d introduce a parent date-range Stimulus controller that coordinates between the two. But for most forms, static constraints are enough.
Tips
autocomplete: "off"prevents the browser’s native date suggestions from fighting with Flatpickr.disconnect()is important. Flatpickr creates extra DOM elements — if you don’t calldestroy()on disconnect, navigating with Turbo will leave orphaned elements behind.- Theming: Flatpickr has several built-in themes. Swap the CSS import to
flatpickr/dist/themes/dark.cssor similar to match your app. - Localization:
import { French } from "flatpickr/dist/l10n/fr.js"and passlocale: Frenchin the config.