Date Picker
Calendar interface for selecting and formatting dates.
TailwindCSS
Alpine.js
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerDefault() {
<div class="w-full max-w-sm">
@components.DatePicker()
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
var (
DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
x-data="date-picker"
data-input-id={ p.ID }
@resize.window="updatePosition"
{ p.Attributes... }
>
<div class="relative">
@Input(
InputProps{
ID: p.ID,
Name: p.Name,
Value: p.Value.Format(p.Config.getGoFormat()),
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Class: utils.TwMerge(p.Class, "peer"),
HasError: p.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
p.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ p.Disabled }
class={
utils.TwMerge(
"absolute top-0 right-0 px-3 py-2",
"cursor-pointer text-muted-foreground",
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar()
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft()
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight()
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
templ DatePickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('date-picker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const monthNames = JSON.parse(this.$el?.dataset?.monthnames);
if (monthNames) {
this.months = monthNames;
}
const dayNames = JSON.parse(this.$el?.dataset?.daynames);
if (dayNames) {
this.days = dayNames;
}
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
Usage
1. Add the script to your page/layout:
// Option A: All components (recommended)
@utils.ComponentScripts()
// Option B: Just DatePicker
@components.DatePickerScript()
2. Use the component:
@components.DatePicker(components.DatePickerProps{...})
Examples
With Label
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerWithLabel() {
<div class="w-full max-w-sm space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-with-label",
}) {
Pick a date
}
@components.DatePicker(components.DatePickerProps{
ID: "date-picker-with-label",
})
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
var (
DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
x-data="date-picker"
data-input-id={ p.ID }
@resize.window="updatePosition"
{ p.Attributes... }
>
<div class="relative">
@Input(
InputProps{
ID: p.ID,
Name: p.Name,
Value: p.Value.Format(p.Config.getGoFormat()),
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Class: utils.TwMerge(p.Class, "peer"),
HasError: p.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
p.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ p.Disabled }
class={
utils.TwMerge(
"absolute top-0 right-0 px-3 py-2",
"cursor-pointer text-muted-foreground",
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar()
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft()
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight()
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
templ DatePickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('date-picker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const monthNames = JSON.parse(this.$el?.dataset?.monthnames);
if (monthNames) {
this.months = monthNames;
}
const dayNames = JSON.parse(this.$el?.dataset?.daynames);
if (dayNames) {
this.days = dayNames;
}
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
Custom Placeholder
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerCustomPlaceholder() {
<div class="w-full max-w-sm">
@components.DatePicker(components.DatePickerProps{
Placeholder: "When is your birthday?",
})
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
var (
DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
x-data="date-picker"
data-input-id={ p.ID }
@resize.window="updatePosition"
{ p.Attributes... }
>
<div class="relative">
@Input(
InputProps{
ID: p.ID,
Name: p.Name,
Value: p.Value.Format(p.Config.getGoFormat()),
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Class: utils.TwMerge(p.Class, "peer"),
HasError: p.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
p.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ p.Disabled }
class={
utils.TwMerge(
"absolute top-0 right-0 px-3 py-2",
"cursor-pointer text-muted-foreground",
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar()
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft()
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight()
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
templ DatePickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('date-picker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const monthNames = JSON.parse(this.$el?.dataset?.monthnames);
if (monthNames) {
this.months = monthNames;
}
const dayNames = JSON.parse(this.$el?.dataset?.daynames);
if (dayNames) {
this.days = dayNames;
}
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
Selected Date
package showcase
import (
"github.com/axzilla/templui/components"
"time"
)
templ DatePickerSelectedDate() {
<div class="w-full max-w-sm">
@components.DatePicker(components.DatePickerProps{
Value: time.Now(),
})
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
var (
DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
x-data="date-picker"
data-input-id={ p.ID }
@resize.window="updatePosition"
{ p.Attributes... }
>
<div class="relative">
@Input(
InputProps{
ID: p.ID,
Name: p.Name,
Value: p.Value.Format(p.Config.getGoFormat()),
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Class: utils.TwMerge(p.Class, "peer"),
HasError: p.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
p.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ p.Disabled }
class={
utils.TwMerge(
"absolute top-0 right-0 px-3 py-2",
"cursor-pointer text-muted-foreground",
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar()
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft()
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight()
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
templ DatePickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('date-picker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const monthNames = JSON.parse(this.$el?.dataset?.monthnames);
if (monthNames) {
this.months = monthNames;
}
const dayNames = JSON.parse(this.$el?.dataset?.daynames);
if (dayNames) {
this.days = dayNames;
}
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
Disabled
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerDisabled() {
<div class="w-full max-w-sm">
@components.DatePicker(components.DatePickerProps{
Disabled: true,
})
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
var (
DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
x-data="date-picker"
data-input-id={ p.ID }
@resize.window="updatePosition"
{ p.Attributes... }
>
<div class="relative">
@Input(
InputProps{
ID: p.ID,
Name: p.Name,
Value: p.Value.Format(p.Config.getGoFormat()),
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Class: utils.TwMerge(p.Class, "peer"),
HasError: p.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
p.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ p.Disabled }
class={
utils.TwMerge(
"absolute top-0 right-0 px-3 py-2",
"cursor-pointer text-muted-foreground",
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar()
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft()
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight()
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
templ DatePickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('date-picker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const monthNames = JSON.parse(this.$el?.dataset?.monthnames);
if (monthNames) {
this.months = monthNames;
}
const dayNames = JSON.parse(this.$el?.dataset?.daynames);
if (dayNames) {
this.days = dayNames;
}
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
Formats
package showcase
import (
"github.com/axzilla/templui/components"
"time"
)
templ DatePickerFormats() {
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-iso-format",
}) {
Default ISO format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerISO,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-eu-format",
}) {
Default EU format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerEU,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-uk-format",
}) {
UK Format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerUK,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-us-format",
}) {
US Format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerUS,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-long-format",
}) {
LONG Format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerLONG,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-es-long-format",
}) {
LONG Format (Spanish)
}
@components.DatePicker(components.DatePickerProps{
Config: components.NewDatePickerConfig(
components.DateFormatLONG,
components.DateLocaleSpanish,
),
Value: time.Now().AddDate(0, 0, -30), // 30 days ago
Placeholder: "Seleccionar fecha",
})
</div>
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
var (
DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
x-data="date-picker"
data-input-id={ p.ID }
@resize.window="updatePosition"
{ p.Attributes... }
>
<div class="relative">
@Input(
InputProps{
ID: p.ID,
Name: p.Name,
Value: p.Value.Format(p.Config.getGoFormat()),
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Class: utils.TwMerge(p.Class, "peer"),
HasError: p.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
p.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ p.Disabled }
class={
utils.TwMerge(
"absolute top-0 right-0 px-3 py-2",
"cursor-pointer text-muted-foreground",
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar()
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft()
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight()
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
templ DatePickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('date-picker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const monthNames = JSON.parse(this.$el?.dataset?.monthnames);
if (monthNames) {
this.months = monthNames;
}
const dayNames = JSON.parse(this.$el?.dataset?.daynames);
if (dayNames) {
this.days = dayNames;
}
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
Form
Select a date from the calendar.
Select a valid date
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerForm() {
<div class="w-full max-w-sm">
@components.FormItem() {
@components.FormLabel(components.FormLabelProps{
For: "date-picker-form",
}) {
Select a date
}
@components.DatePicker(components.DatePickerProps{
ID: "date-picker-form",
HasError: true,
})
@components.FormDescription() {
Select a date from the calendar.
}
@components.FormMessage(components.FormMessageProps{
Variant: components.FormMessageVariantError,
}) {
Select a valid date
}
}
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
var (
DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
x-data="date-picker"
data-input-id={ p.ID }
@resize.window="updatePosition"
{ p.Attributes... }
>
<div class="relative">
@Input(
InputProps{
ID: p.ID,
Name: p.Name,
Value: p.Value.Format(p.Config.getGoFormat()),
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Class: utils.TwMerge(p.Class, "peer"),
HasError: p.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
p.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ p.Disabled }
class={
utils.TwMerge(
"absolute top-0 right-0 px-3 py-2",
"cursor-pointer text-muted-foreground",
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar()
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft()
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight()
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
templ DatePickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('date-picker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
const monthNames = JSON.parse(this.$el?.dataset?.monthnames);
if (monthNames) {
this.months = monthNames;
}
const dayNames = JSON.parse(this.$el?.dataset?.daynames);
if (dayNames) {
this.days = dayNames;
}
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}