← All posts

Building a Date Picker Component with Flatpickr and Stimulus

June 5, 2024 rails 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 call destroy() 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.css or similar to match your app.
  • Localization: import { French } from "flatpickr/dist/l10n/fr.js" and pass locale: French in the config.
Let’s work together! Tell Me About Your Project