用 css grid 实现30天打卡日历:设 grid-template-columns: repeat(7, 1fr),用 和 data-checked="true" 属性控制状态,配合 aria-pressed 提升可访问性,移动端用 media 查询调列数并确保点击区域≥44×44px。

怎么用纯 HTML + CSS 实现 30 天连续打卡日历
不需要 JavaScript 就能渲染出 30 个格子的打卡日历,核心是用 <div> 或 <code><span></span> 配合 CSS Grid 布局。关键不是“能不能”,而是“怎么让格子对齐、可标记、不换行错位”。
常见错误是直接写 30 个 <div> 然后用 <code>float 或 inline-block 排列——容易因空格、换行、字体大小导致最后一行只有 4 个格子;更稳妥的方式是强制网格化。
- 用
display: grid,设grid-template-columns: repeat(7, 1fr)(固定 7 列),再通过grid-auto-rows控制行高 - 30 天最多占 5 行(4 周 + 2 天),所以容器高度不用硬设,靠
min-height预留视觉空间即可 - 每个格子用
<button></button>而非<div>,语义正确,也方便后续加点击态(比如 <code>:active或[data-checked])如何标记「已打卡」和「未打卡」状态
状态不靠 JS 动态插入 class,而是提前在 HTML 中用属性控制,比如
data-checked="true"或data-day="15"。这样 CSS 可以直接响应:button[data-checked="true"] { background: #4caf50; color: white; } button:not([data-checked="true"]) { background: #f5f5f5; color: #999; }注意:不要用
class="checked"再写一堆重复样式——属性选择器更轻量,也避免 class 名冲突;若需区分「今日」「历史」「不可打」,可用多个 data 属性组合,例如:data-status="today"、data-status="locked"。-
data-checked布尔值建议统一用"true"/"false"字符串,避免data-checked存在即为 true 的歧义(某些框架会自动转换) - 如果服务端返回的是 ISO 日期字符串(如
"2024-06-12"),前端渲染时建议转成 day-of-month 数字再写进data-day,方便 CSS 伪类或 JS 定位 - 别忘了给已打卡按钮加
aria-pressed="true",提升无障碍体验
为什么 grid-column-start 不适合手动排布 30 天
有人想用
grid-column-start给每个格子指定位置(比如第 1 天放第 3 列、第 2 天放第 4 列),这在「日历要对齐星期几」时看似合理,但实际会让 HTML 结构和语义脱节——第 1 个<button></button>对应的是星期三,而 DOM 顺序仍是线性 1~30,屏幕阅读器会按顺序读,用户听到的是“打卡第 1 天、打卡第 2 天…”,而非“星期三、星期四…”。- 真正需要首日对齐星期几,应该由服务端/模板引擎计算当月 1 号是周几,然后前置插入空
<button disabled hidden></button>占位(数量 = 周几索引,周一为 0) - 纯静态 30 天打卡(不关联真实日历),就老老实实从左到右、从上到下填满 30 个格子,无需模拟星期偏移
- 手动设
grid-row/grid-column会让 CSS 文件膨胀,且无法用:nth-child()批量控制样式
移动端适配和点击反馈的关键细节
在手机上点小格子容易误触,尤其 30 个挤在一起时。不能只靠增大
font-size,得从交互层加固:- 每个
<button></button>必须设touch-action: manipulation,减少 iOS Safari 的 300ms 延迟 - 最小点击区域保证 ≥ 44×44px(iOS)或 48×48dp(Android),可通过
min-width/min-height+padding实现,别只靠width/height - 用
@media (max-width: 480px)把 grid 列数从 7 改成 5,避免单格过窄;同时把字体从14px提到16px,文字才不会糊 - 禁用
user-select: none—— 看似防误选,但会阻止长按复制日期、分享等合理操作
最常被忽略的是:没设
button { border: none; outline: none; },导致点击后残留焦点环,在深色背景上极难看,又影响视觉连续性。 -











