必须用 table 而非 div + css grid,因日历是二维时间网格,需原生行列语义;thead 用 th 写星期头,tbody 每行严格 7 个 td,含前置/尾部空单元格;每个 td 必须含 data-date 属性,移动端需 min-height 和 padding 保障点击热区。

为什么必须用 table 而不是 div + CSS Grid
日历是二维时间网格,不是视觉布局问题——table 提供原生行列语义,屏幕阅读器能正确识别“第3行第5列是2026年7月10日”,而 div 套 grid 只是视觉对齐,辅助技术会把它读作一堆无关联的块。你写的是结构,不是样式。如果跳过 thead、用 td 写星期头、或靠 margin-left 模拟空格,那它就只是个“看起来像日历”的 div 容器。
tr 和 td 怎么填才不崩
常见错误是只渲染当月日期,导致第一行从周三开始却没留周一、周二两个空格,整行向左偏移。表格列数必须严格一致:每周固定 7 列,thead 里用 th 写死 日一二三四五六;tbody 每行也必须有且仅有 7 个 td。
- 用
new Date(2026, 6, 1).getDay()得到 7 月 1 日是星期几(返回4,即周四)→ 前置空格数 =4(周日=0,所以周日、一、二、三共 4 格) - 当月共
new Date(2026, 7, 0).getDate()=31天 → 需要填充 31 个有效日期,加上前置 4 个空td,再补尾部空格使总单元格数为 42(6 行 × 7 列)→ 尾部需42 - 4 - 31 = 7个空td - 所有空
td必须写成<td></td>,不能省略、不能用、不能靠 CSS 模拟——否则表格结构失真,打印或缩放时错位
td 里只写数字?别这么干
用户点“1”时,JS 无法知道这是 7 月 1 日还是 8 月 1 日。纯靠文本内容解析(比如正则提取数字再拼年月)在跨月、闰年、不同长度月份下必然翻车。
- 每个
td必须带data-date="2026-07-01"属性,ISO 格式,方便后续直接传给new Date() - 上月尾/下月头的日期也要写全,比如 6 月 29 日就写
data-date="2026-06-29",别只存day或month - 不要用
dataset.day+dataset.month拆开存——少了年份,跨年场景直接失效 - 点击事件必须委托给
tbody,用e.target.closest('td[data-date]')获取目标单元格,再读.dataset.date
移动端点击总点不中?td 缺少这两样
桌面端看着正常的单元格,在手机上经常触发不了 click——因为默认 td 高度由字号撑开,实际可点击区域远小于视觉范围。
- 给
td加min-height: 48px(符合触摸最小尺寸标准) - 加
padding: 8px扩展内边距,避免文字贴边 - 别依赖
line-height撑高——它不增加点击热区,只影响文字垂直居中 - 空
td也要设aria-hidden="true",防止读屏器误读空白单元格
最易被忽略的不是怎么画格子,而是空单元格的语义完整性、data-date 的强制绑定、以及移动端热区的显式声明——这三处任一缺失,日历就只是个好看但不可靠的栅格。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











