Date Picker — Free HTML CSS JS Calendar Snippet

Date Picker · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Calendar grid: firstDay padding, daysInMonth cells, trailing padding — repeat(7,1fr)
Today highlight: .today class with accent colour text, no background fill
Selected state: .selected with filled accent background and white text
Scale+opacity dropdown animation: scale(0.97)→1 + opacity 0→1 on .open
Click-outside close: document listener + e.target.closest() check
Today shortcut: navigates to current month and selects today in one click
Clear button: resets selected state and trigger display text
Accessible: aria-haspopup, aria-expanded, role=dialog on calendar panel

About this UI Snippet

Date Picker — Custom Calendar Dropdown, Month Navigation, Today Highlight & Click-Outside Close

Screenshot of the Date Picker snippet rendered live

A date picker is one of the most commonly needed but hardest-to-build form components. The native HTML date input looks wildly different across browsers and operating systems, cannot be styled to match your design system, and lacks features like Today/Clear shortcuts. This snippet provides a complete custom date picker: a styled trigger button that shows the selected date, a calendar dropdown with month navigation, today highlighting, selected date state, and Today/Clear footer buttons — all in plain HTML, CSS, and vanilla JavaScript. For a start/end range use the date range picker; for time-of-day, the time picker.

The calendar grid generation

The render() function generates the calendar grid the same way as the Calendar Widget snippet: compute firstDay (the weekday the month starts on), daysInMonth, and prevDays (for padding). The grid uses CSS grid with repeat(7,1fr) columns. Each cell is a button element — keyboard-accessible by default. The .today class highlights today's date without filling the cell; .selected applies the filled accent background.

The dropdown animation

The calendar panel uses opacity: 0 + transform: scale(0.97) translateY(-4px) in the closed state. Adding .open switches to opacity: 1 + scale(1) + translateY(0). CSS transition: opacity 0.18s, transform 0.18s creates the smooth pop-in effect. The transform-origin defaults to top left, which matches the trigger button position.

Click-outside close

A document click listener checks e.target.closest('#picker-wrap'). If the click was outside the picker wrapper, toggle() is called to close the calendar. The listener is attached once to the document and fires for all clicks — efficient for pages with multiple date pickers.

Selected date display

When a date is selected, pick(date) calls date.toLocaleDateString() with a format showing weekday, month, day, and year ("Mon, Jun 2, 2026"). The trigger span text updates and the .selected class changes its colour from placeholder grey to dark text. The helper text below the trigger confirms the selection.

Accessibility

The trigger button has aria-haspopup="true" and aria-expanded toggling with open state. The calendar panel has role="dialog" and aria-modal="true". All day cells are button elements so they are keyboard-focusable. Arrow key navigation between days can be added by handling keydown on the grid and moving focus to adjacent cells.

Connecting to form submission

To include the date in a form POST, add a hidden input: hiddenInput.value = selected ? selected.toISOString().split('T')[0] : ''. Update this in pick() alongside the display. The YYYY-MM-DD ISO format is the standard for backend date handling.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to work out the grid math by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how firstDay, daysInMonth, and prevDays combine inside render() to fill the leading and trailing padding cells correctly for every month length, including February in leap years. The same assistant can help you optimize it, for example checking whether rebuilding the entire days grid with innerHTML on every shift() call is wasteful compared to patching only the changed cells. It is just as useful for extending the picker: ask it to add a minDate/maxDate range restriction, keyboard arrow-key navigation between day buttons, or a small preset-range dropdown like "next 7 days". Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a custom date picker in plain HTML, CSS, and JavaScript with no external date library and no native input type="date" — just a trigger button and a popover calendar built from scratch.

Requirements:
- A trigger button showing a placeholder or the selected date, toggling an absolutely-positioned calendar panel with a scale plus opacity transition (not display none/block, so the transition actually plays).
- Compute the calendar grid purely from the native Date object: use getDay() on the first of the month to find the starting weekday, new Date(year, month + 1, 0).getDate() for the number of days in the month, and new Date(year, month, 0).getDate() for the previous month's length used only to number the leading padding cells.
- Render every day cell as a real button element (not a div with a click handler) so it is keyboard-focusable by default, and give today's cell and the selected cell distinct CSS classes rather than inline styles.
- Include Previous/Next month navigation that mutates a single cursor Date and re-renders the grid, plus Today and Clear footer buttons.
- Close the calendar when a day is clicked and also when the user clicks anywhere outside the picker, using a single document-level click listener with closest() to detect outside clicks.
- Format the selected date for display with toLocaleDateString and also expose it in ISO YYYY-MM-DD form so it could be written into a hidden form input.

Step by step

How to Use

  1. 1
    Click the date trigger button to open the calendarThe calendar slides open below the trigger with a scale+opacity animation. Click ‹ and › to navigate months. Click any day to select it and close the calendar.
  2. 2
    Use the Today and Clear footer buttonsClick "Today" to navigate to the current month and select today's date. Click "Clear" to deselect the current date and reset the trigger to placeholder state.
  3. 3
    Get the selected date in JavaScriptRead the selected variable directly: if (selected) { const iso = selected.toISOString().split("T")[0]; }. Add a hidden input and update its value in pick() to include the date in a standard HTML form submission.
  4. 4
    Set minimum and maximum datesIn addDay(), add a disabled condition: const isPast = date < minDate; btn.disabled = isPast. Add the .other class to past dates for a dimmed appearance. This prevents users from selecting dates outside your allowed range.
  5. 5
    Change the date display formatUpdate the toLocaleDateString() format in pick(): date.toLocaleDateString("en-GB") for DD/MM/YYYY, or date.toLocaleDateString("default",{month:"long",day:"numeric",year:"numeric"}) for "June 2, 2026".
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component with useState for selected and cur dates, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

Booking and reservation date selection forms
Replace native date inputs in booking flows, appointment scheduling, and reservation systems. Set a minimum date of today to prevent selecting past dates. The Today button is especially useful for same-day bookings.
Project management task deadline pickers
Use in task creation forms where users set due dates. The month navigation makes it easy to set deadlines weeks ahead. The clear button lets users remove a deadline without deleting the task.
Date range start and end date selection
Adapt two date pickers side by side for check-in/check-out or start/end date selection. Enforce constraints between them: the end date picker sets minDate to the selected start date so end cannot be before start.
Filter and search interface date constraints
Use in dashboard filter panels for date-range filtering of data. The compact calendar fits naturally inside a filter sidebar or popover. Connect the selected date to an API call that fetches records for the chosen date.
Study JavaScript Date arithmetic for calendar generation
The render() function teaches how to use new Date(year, month, 0).getDate() for the last day of the previous month, new Date(year, month+1, 0).getDate() for daysInMonth, and getDay() for the starting weekday — the three calculations needed for any calendar grid.
Replace native date inputs for consistent cross-browser styling
The native <input type="date"> renders differently on Chrome (calendar icon), Firefox (dropdown), iOS Safari (spinner wheel), and Edge. This custom date picker provides identical appearance and behaviour across all browsers with full CSS control.

Got questions?

Frequently Asked Questions

In the addDay() function, add a disabled check: const isPast = date < new Date(today.getFullYear(), today.getMonth(), today.getDate()); btn.disabled = isPast; if (isPast) btn.classList.add("other"). This greys out past dates and makes them unclickable. For a range restriction, replace today with your minDate and maxDate constants: btn.disabled = date < minDate || date > maxDate.

Add a hidden input inside the form: <input type="hidden" name="date" id="date-value">. In the pick() function, add: document.getElementById("date-value").value = date.toISOString().split("T")[0]. This gives the date in YYYY-MM-DD ISO format which works with all server-side date parsers. For a date range form, use two hidden inputs — date-start and date-end — and update each from its respective date picker.

Add two .picker-wrap divs side by side in a flex row. Each has its own calendar and selected variable. In the second picker, set minDate = firstSelected to prevent the end date being before the start date. Add range highlighting: after selecting start, in render() add a .in-range class to days between start and the currently hovered day. Use CSS background on .in-range to highlight the range area.

Click "JSX" to download. Manage selected (Date|null) and cur (Date — first of displayed month) with useState. Compute the calendar grid with useMemo([cur]). Handle day clicks with setSelected(date) and toggle the calendar with useState(false). For keyboard accessibility, add onKeyDown to the grid to handle ArrowLeft/ArrowRight/ArrowUp/ArrowDown for navigating days and Enter to select.