原生 JS 实现自定义单选日历组件(无 UI 库、纯手写、禁用过去日期)

发布时间:2026/7/12 10:14:02
原生 JS 实现自定义单选日历组件(无 UI 库、纯手写、禁用过去日期) 前言在后台管理、表单页面开发中日期选择是高频需求。项目中经常会遇到两种场景一是不想引入Element UI、Ant Design等重型 UI 框架减少打包体积二是产品要求日历样式高度自定义原生input[type“date”]样式无法修改无法满足设计稿。本文基于原生 JavaScript jQuery仅引入基础库DOM 简化操作 从零手写一套独立悬浮单选日历具备以下特性完全自定义样式弹窗定位跟随输入框支持上 / 下一月、上 / 下一年快速切换自动区分上月 / 当月 / 下月日期灰色置灰非当月天数核心业务逻辑所有过去日期禁用不可点击仅可选择今日及未来日期「今天」快捷按钮一键选中当前日期并关闭弹窗点击空白区域自动关闭日历防止弹窗滞留日期输出统一YYYY-MM-DD标准格式直接适配后端接口低耦合结构单文件开箱即用可快速复用至任意表单。全文附带完整可运行源码、逐行逻辑拆解、关键 API 说明、踩坑点总结新手也能完全看懂可直接落地项目。一、整体结构分层设计整个日历组件分为三层输入框层、日历弹窗容器、日历内部渲染模块结构完全解耦输入框 .date_input只读文本框内置日历图标点击唤起弹窗弹窗容器 .calendar_wrapfixed 悬浮定位默认隐藏点击输入框计算坐标展示日历内部模块头部导航年份 / 月份切换按钮 当前年月标题星期头部周日到周六固定表头日期格子容器动态渲染 42 格日期固定 6 行 ×7 列保证布局稳定底部快捷按钮回到今日。CSS 设计规范说明输入框内置 svg 日历图标无需引入图片资源减少 HTTP 请求弹窗使用fixed定位通过getBoundingClientRect实时获取输入框坐标永远紧贴输入框下方样式分层隔离日历所有样式统一包裹在.calendar_wrap下不会污染页面其他元素状态类区分.other上月 / 下月灰色无效日期.disabled已过期、不可点击日期.active当前选中日期高亮底色交互优化hover 高亮有效日期、弹窗阴影分层提升视觉体验。二、完整可运行源码html预览原生自定义单选日历组件« ‹2026年 Jun› »SuMoTuWeThFrSa今天script // 获取全局DOM元素缓存节点减少重复DOM查询提升性能 const inp document.querySelector(.date_input); const calWrap document.getElementById(calendar); const dayBox document.getElementById(dayBox); const calTitle document.getElementById(calTitle); const todayBtn document.getElementById(todayBtn); // 控制日历当前展示年月的基准日期 let nowDate new Date(); // 月份英文简写映射用于顶部标题渲染 const monthArr [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]; // 1. 输入框点击唤起日历 inp.onclick function (e) { // 阻止冒泡避免触发body关闭弹窗事件 e.stopPropagation(); // getBoundingClientRect获取输入框相对视口的坐标 const rect inp.getBoundingClientRect(); // 弹窗定位左对齐输入框输入框下方8px展示 calWrap.style.left rect.left px; calWrap.style.top rect.bottom 8 px; calWrap.style.display block; // 每次打开重新渲染当月日期防止跨天打开日历日期不刷新 renderCal(); } // 点击页面空白处关闭日历弹窗 document.body.onclick () calWrap.style.display none; // 点击日历内部阻止冒泡不会关闭弹窗 calWrap.onclick e e.stopPropagation(); // 2. 核心渲染函数 renderCal function renderCal() { // 获取当前系统今日清空时分秒用于日期对比仅比较年月日 const today new Date(); today.setHours(0, 0, 0, 0); // 获取当前展示年份、月份 const y nowDate.getFullYear(); const m nowDate.getMonth(); // 渲染顶部年月标题 calTitle.innerText ${y}年 ${monthArr[m]}; // 清空上次渲染的日期格子 dayBox.innerHTML ; // 关键日期计算API说明 const firstDay new Date(y, m, 1).getDay(); // 当月1号是星期几(0周日~6周六) const curMonthDays new Date(y, m 1, 0).getDate(); // 当前月份总天数 const lastMonthDays new Date(y, m, 0).getDate(); // 上个月总天数 // 第一部分渲染上月末尾剩余日期全部置灰 .other for (let i 0; i firstDay; i) { let d lastMonthDays - firstDay i 1; dayBox.innerHTML div classday-item other${d}/div; } // 第二部分渲染当月完整日期核心逻辑判断过期禁用、选中高亮 for (let i 1; i curMonthDays; i) { const currentDate new Date(y, m, i); currentDate.setHours(0, 0, 0, 0); let cls ; // 当前选中日期添加active高亮 if (i nowDate.getDate()) cls active; // 日期小于今日添加disabled禁用类不可点击 if (currentDate today) { cls disabled; } // padStart补零统一输出 YYYY-MM-DD 标准日期格式 const formatMonth String(m 1).padStart(2, 0); const formatDay String(i).padStart(2, 0); dayBox.innerHTML div classday-item ${cls}>