More Forms Snippets
Date Picker — Free HTML CSS JS Calendar Snippet
Date Picker · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Date Picker — Custom Calendar Dropdown, Month Navigation, Today Highlight & Click-Outside Close

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:
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
- 1Click 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.
- 2Use 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.
- 3Get 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.
- 4Set 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.
- 5Change 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".
- 6Export 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
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.