Calendar
A month grid that says which days are special — public holidays, a
plant shutdown, period-end, the dates a shipment cannot land on. Use it to
show a calendar, and — with each day as a real submit button — to
pick from one when which days are special is part of the choice.
Not for a plain date field — when the reader just needs
to state a date and no day is special, the
native
<input type="date"> wins on every axis that matters:
localized for free, one tab stop instead of a grid of them, and the
platform's own picker on a phone. Reach for this grid only when
which days are special is part of the decision — a shutdown, a
blackout window, period-end — because that is the one thing the native
control cannot show.
One month, with the days that matter marked
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| — non-working day | — non-working day | |||||
| — Public holiday | — non-working day | — non-working day | ||||
| — non-working day | — non-working day | |||||
| — today | — non-working day | — non-working day | ||||
| — selected | — non-working day | — non-working day | ||||
View code
<div class="bo-calendar">
<table class="bo-calendar__month">
<caption>August 2026</caption>
<thead><tr><th scope="col"><abbr title="Monday">Mo</abbr></th><th scope="col"><abbr title="Tuesday">Tu</abbr></th><th scope="col"><abbr title="Wednesday">We</abbr></th><th scope="col"><abbr title="Thursday">Th</abbr></th><th scope="col"><abbr title="Friday">Fr</abbr></th><th scope="col"><abbr title="Saturday">Sa</abbr></th><th scope="col"><abbr title="Sunday">Su</abbr></th></tr></thead>
<tbody>
<tr><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-27">27</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-28">28</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-29">29</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-30">30</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-31">31</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-01">1</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-02">2</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day" data-day="holiday"><time datetime="2026-08-03">3</time><span class="bo-visually-hidden"> — Public holiday</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-04">4</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-05">5</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-06">6</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-07">7</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-08">8</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-09">9</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-10">10</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-11">11</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-12">12</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-13">13</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-14">14</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-15">15</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-16">16</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-17">17</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-18">18</time></span></td><td><span class="bo-calendar__day" data-day="today"><time datetime="2026-08-19">19</time><span class="bo-visually-hidden"> — today</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-20">20</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-21">21</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-22">22</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-23">23</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day" data-day="selected"><time datetime="2026-08-24">24</time><span class="bo-visually-hidden"> — selected</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-25">25</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-26">26</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-27">27</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-28">28</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-29">29</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-30">30</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-31">31</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-01">1</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-02">2</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-03">3</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-04">4</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-05">5</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-06">6</time></span></td></tr>
</tbody>
</table>
</div> Weekends are hatched, the holiday carries a dot, today is outlined and the chosen day is filled. Every one of those is a shape as well as a colour, and each writes its meaning into the cell as visually-hidden text — so the grid reads the same in greyscale, under forced colours, and to a screen reader.
Where the week starts is your data, not a setting here
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| — non-working day | ||||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day |
Monday-first is ISO 8601 and usual across Europe and much of Asia; Sunday-first is standard in the US, Canada, Japan and much of Latin America. An ERP shipped to a US buyer with a Monday-first delivery calendar is simply wrong for them, so both must be possible.
The framework deliberately ships no
data-week-start. There is nothing for it to do: this
CSS contains no positional rule — no nth-child, and
“closed” is a per-cell attribute you set, not a column. Week
start changes which date sits in which cell, which is decided where you
generate the month. An attribute that styled nothing would be API we could
never remove.
Generate the grid, do not hand-write it: a month
is 35+ cells whose date must agree with its column, and an error there is
invisible — the grid still looks like a calendar. Both demos above come from
one function, and a build gate checks every calendar in these docs by
reading each cell's datetime back against its column heading.
Three months — repetition, not a setting
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| — non-working day | — non-working day | |||||
| — Public holiday | — non-working day | — non-working day | ||||
| — non-working day | — non-working day | |||||
| — today | — non-working day | — non-working day | ||||
| — selected | — non-working day | — non-working day | ||||
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day | — non-working day | |||||
| — non-working day |
View code
<div class="bo-calendar">
<table class="bo-calendar__month">
<caption>August 2026</caption>
<thead><tr><th scope="col"><abbr title="Monday">Mo</abbr></th><th scope="col"><abbr title="Tuesday">Tu</abbr></th><th scope="col"><abbr title="Wednesday">We</abbr></th><th scope="col"><abbr title="Thursday">Th</abbr></th><th scope="col"><abbr title="Friday">Fr</abbr></th><th scope="col"><abbr title="Saturday">Sa</abbr></th><th scope="col"><abbr title="Sunday">Su</abbr></th></tr></thead>
<tbody>
<tr><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-27">27</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-28">28</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-29">29</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-30">30</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-31">31</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-01">1</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-02">2</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day" data-day="holiday"><time datetime="2026-08-03">3</time><span class="bo-visually-hidden"> — Public holiday</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-04">4</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-05">5</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-06">6</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-07">7</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-08">8</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-09">9</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-10">10</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-11">11</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-12">12</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-13">13</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-14">14</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-15">15</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-16">16</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-17">17</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-18">18</time></span></td><td><span class="bo-calendar__day" data-day="today"><time datetime="2026-08-19">19</time><span class="bo-visually-hidden"> — today</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-20">20</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-21">21</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-22">22</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-23">23</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day" data-day="selected"><time datetime="2026-08-24">24</time><span class="bo-visually-hidden"> — selected</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-25">25</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-26">26</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-27">27</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-28">28</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-29">29</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-30">30</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-31">31</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-01">1</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-02">2</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-03">3</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-04">4</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-05">5</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-06">6</time></span></td></tr>
</tbody>
</table>
<table class="bo-calendar__month">
<caption>September 2026</caption>
<thead><tr><th scope="col"><abbr title="Monday">Mo</abbr></th><th scope="col"><abbr title="Tuesday">Tu</abbr></th><th scope="col"><abbr title="Wednesday">We</abbr></th><th scope="col"><abbr title="Thursday">Th</abbr></th><th scope="col"><abbr title="Friday">Fr</abbr></th><th scope="col"><abbr title="Saturday">Sa</abbr></th><th scope="col"><abbr title="Sunday">Su</abbr></th></tr></thead>
<tbody>
<tr><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-08-31">31</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-01">1</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-02">2</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-03">3</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-04">4</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-05">5</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-06">6</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-09-07">7</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-08">8</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-09">9</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-10">10</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-11">11</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-12">12</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-13">13</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-09-14">14</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-15">15</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-16">16</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-17">17</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-18">18</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-19">19</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-20">20</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-09-21">21</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-22">22</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-23">23</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-24">24</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-25">25</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-26">26</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-09-27">27</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-09-28">28</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-29">29</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-09-30">30</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-10-01">1</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-10-02">2</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-10-03">3</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-10-04">4</time></span></td></tr>
</tbody>
</table>
<table class="bo-calendar__month">
<caption>October 2026</caption>
<thead><tr><th scope="col"><abbr title="Monday">Mo</abbr></th><th scope="col"><abbr title="Tuesday">Tu</abbr></th><th scope="col"><abbr title="Wednesday">We</abbr></th><th scope="col"><abbr title="Thursday">Th</abbr></th><th scope="col"><abbr title="Friday">Fr</abbr></th><th scope="col"><abbr title="Saturday">Sa</abbr></th><th scope="col"><abbr title="Sunday">Su</abbr></th></tr></thead>
<tbody>
<tr><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-28">28</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-29">29</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-30">30</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-01">1</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-02">2</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-03">3</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-04">4</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-10-05">5</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-06">6</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-07">7</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-08">8</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-09">9</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-10">10</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-11">11</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-10-12">12</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-13">13</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-14">14</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-15">15</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-16">16</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-17">17</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-18">18</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-10-19">19</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-20">20</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-21">21</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-22">22</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-23">23</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-24">24</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-25">25</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-10-26">26</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-27">27</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-28">28</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-29">29</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-10-30">30</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-10-31">31</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-11-01">1</time></span></td></tr>
</tbody>
</table>
</div> Put three .bo-calendar__month tables in one
.bo-calendar. There is no months option to learn, and they
wrap to one column when the space runs out, so the same markup serves a wide planning
screen and a phone.
Picking a date from it — no JavaScript
View code
<form method="get" class="bo-calendar">
<table class="bo-calendar__month">
<caption>Delivery date — August 2026</caption>
<thead><tr><th scope="col"><abbr title="Monday">Mo</abbr></th><th scope="col"><abbr title="Tuesday">Tu</abbr></th><th scope="col"><abbr title="Wednesday">We</abbr></th><th scope="col"><abbr title="Thursday">Th</abbr></th><th scope="col"><abbr title="Friday">Fr</abbr></th><th scope="col"><abbr title="Saturday">Sa</abbr></th><th scope="col"><abbr title="Sunday">Su</abbr></th></tr></thead>
<tbody>
<tr><td><span class="bo-calendar__day bo-calendar__day--outside">27</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">28</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">29</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">30</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">31</span></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-01" data-day="closed" disabled>1<span class="bo-visually-hidden"> — non-working day</span></button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-02" data-day="closed" disabled>2<span class="bo-visually-hidden"> — non-working day</span></button></td></tr>
<tr><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-03" data-day="holiday" disabled>3<span class="bo-visually-hidden"> — public holiday, no delivery</span></button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-04">4</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-05">5</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-06">6</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-07">7</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-08" data-day="closed" disabled>8<span class="bo-visually-hidden"> — non-working day</span></button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-09" data-day="closed" disabled>9<span class="bo-visually-hidden"> — non-working day</span></button></td></tr>
<tr><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-10">10</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-11">11</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-12">12</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-13">13</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-14">14</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-15" data-day="closed" disabled>15<span class="bo-visually-hidden"> — non-working day</span></button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-16" data-day="closed" disabled>16<span class="bo-visually-hidden"> — non-working day</span></button></td></tr>
<tr><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-17">17</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-18">18</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-19">19</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-20">20</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-21">21</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-22" data-day="closed" disabled>22<span class="bo-visually-hidden"> — non-working day</span></button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-23" data-day="closed" disabled>23<span class="bo-visually-hidden"> — non-working day</span></button></td></tr>
<tr><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-24" data-day="selected">24<span class="bo-visually-hidden"> — currently chosen</span></button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-25">25</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-26">26</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-27">27</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-28">28</button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-29" data-day="closed" disabled>29<span class="bo-visually-hidden"> — non-working day</span></button></td><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-30" data-day="closed" disabled>30<span class="bo-visually-hidden"> — non-working day</span></button></td></tr>
<tr><td><button class="bo-calendar__day" type="submit" name="date" value="2026-08-31">31</button></td><td><span class="bo-calendar__day bo-calendar__day--outside">1</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">2</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">3</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">4</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">5</span></td><td><span class="bo-calendar__day bo-calendar__day--outside">6</span></td></tr>
</tbody>
</table>
</form> Each day is a real <button type="submit">
carrying its own ISO date, inside a plain <form method="get">. Click
one and the date is in the URL. The server decides what is selectable
— weekends and the public holiday are rendered disabled, so the rule that
blocks them lives where the works calendar already lives, not in a client-side config
you have to keep in sync.
No roving tabindex, no aria-* from
us, no widget: real buttons in a real table already give keyboard operation, disabled
semantics and a screen-reader announcement of the column and row. This works with
scripting switched off.
The honest cost: it is a tab stop per selectable day — twenty in
the month above, once the weekends and the holiday are out. A native
<input type="date"> is one. So use the native input for a plain
date, and reach for this only when seeing which days are special is part of
the decision — a delivery date around a plant shutdown, a period-end close. That is
a narrow case, and it is the whole case.
A RANGE — "from this day to that one"
— across two or three months. For an ordinary from/to, the answer
is not this grid: it is two
native date fields with the
second's min set from the first, which costs two tab stops
against one per selectable day. Reach for the grid only when the marks
matter here too — picking a shutdown window around the holidays you can
see. Then it is the same mechanism twice and still no JavaScript: the
first submit puts the start date in the URL, the server re-renders with
that day selected and everything before it disabled, and the
second submit closes the range. The rule about which spans are legal stays
where the works calendar lives. What is refused is the scripted
drag-select: it needs pointer tracking, a hover-preview state and
a keyboard equivalent to invent, and it would replace a server round trip
that an ERP is making anyway.
Why the framework does not ship a date-picker widget
The native date input is good and this framework keeps recommending it. What it
cannot do is the whole reason for this component: its only constraints are
min, max and step — which describe a
range. A works calendar is a set: eight public holidays, a two-week
shutdown, the last working day of each period. There is no attribute for that, and no
API to mark a day.
So the division of labour is: the browser owns the ordinary case, this owns the case where the marks matter. Most date entry is ordinary — use the native input. When the marks are part of the decision, the section above picks from this grid with real submit buttons and no JavaScript.
What is still refused is the widget: a scripted
popup with roving tabindex, its own ARIA and a month-navigation model to
re-implement. That is a large surface to own badly, and nothing above needs it —
which is the test for whether refusing was right.
It is a real <table> because a month is
tabular data. That buys row and column semantics, scope headers and a
caption for free: a screen reader announces "Tuesday, 12" without a line of ARIA.
Each day carries a <time datetime>, so the machine-readable date is
in the markup rather than inferred from the number.
The assembled screen lives in Patterns
Month grid + a day-detail list, swapped in beside it without re-rendering the month — the same swap discipline master-detail uses for a list and a record — is the schedule pattern. This page documents the calendar itself; the pattern documents the screen.
Markup
<div class="bo-calendar">
<table class="bo-calendar__month">
<caption>August 2026</caption>
<thead><tr><th scope="col"><abbr title="Monday">Mo</abbr></th><th scope="col"><abbr title="Tuesday">Tu</abbr></th><th scope="col"><abbr title="Wednesday">We</abbr></th><th scope="col"><abbr title="Thursday">Th</abbr></th><th scope="col"><abbr title="Friday">Fr</abbr></th><th scope="col"><abbr title="Saturday">Sa</abbr></th><th scope="col"><abbr title="Sunday">Su</abbr></th></tr></thead>
<tbody>
<tr><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-27">27</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-28">28</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-29">29</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-30">30</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-07-31">31</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-01">1</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-02">2</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day" data-day="holiday"><time datetime="2026-08-03">3</time><span class="bo-visually-hidden"> — Public holiday</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-04">4</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-05">5</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-06">6</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-07">7</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-08">8</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-09">9</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-10">10</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-11">11</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-12">12</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-13">13</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-14">14</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-15">15</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-16">16</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-17">17</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-18">18</time></span></td><td><span class="bo-calendar__day" data-day="today"><time datetime="2026-08-19">19</time><span class="bo-visually-hidden"> — today</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-20">20</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-21">21</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-22">22</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-23">23</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day" data-day="selected"><time datetime="2026-08-24">24</time><span class="bo-visually-hidden"> — selected</span></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-25">25</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-26">26</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-27">27</time></span></td><td><span class="bo-calendar__day"><time datetime="2026-08-28">28</time></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-29">29</time><span class="bo-visually-hidden"> — non-working day</span></span></td><td><span class="bo-calendar__day" data-day="closed"><time datetime="2026-08-30">30</time><span class="bo-visually-hidden"> — non-working day</span></span></td></tr>
<tr><td><span class="bo-calendar__day"><time datetime="2026-08-31">31</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-01">1</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-02">2</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-03">3</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-04">4</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-05">5</time></span></td><td><span class="bo-calendar__day bo-calendar__day--outside"><time datetime="2026-09-06">6</time></span></td></tr>
</tbody>
</table>
</div> Quick reference generated
| Class | Role |
|---|---|
| .bo-calendar | block (root) |
| .bo-calendar__day | part: day |
| .bo-calendar__day--outside | variant: outside |
| .bo-calendar__month | part: month |
State attributes: data-day
API generated from CSS
| Blocks | bo-calendar |
|---|---|
| Parts | bo-calendar__daybo-calendar__month |
| Variants | bo-calendar__day--outside |
| data-* hooks | data-day="closed|holiday|selected|today" |
| JS required | None — CSS-only. |
- data-day is written by your app per render, like data-row-state — it is app state, not a variant.
- Every marked day needs its meaning in text; the colour and shape are the visible half only.
- A day may be a plain span, a link or a button. Only the last two get hover and pointer affordances.
Contrast — 20 AA pairs this component uses (generated, CI-gated)
| Text | on | Light | Dark |
|---|---|---|---|
| text-primary | bg-surface | 17.74 | 16.15 |
| text-primary | bg-canvas | 16.98 | 18.08 |
| text-muted | bg-surface | 7.56 | 6.65 |
| text-muted | bg-muted | 6.87 | 5.66 |
| bg-surface | text-primary | 17.74 | 16.15 |
| accent-text | accent-subtle | 7.27 | 8.38 |
| danger-text | danger-subtle | 5.91 | 8.06 |
| danger-text | bg-surface | 6.47 | 8.89 |
| danger-text | bg-canvas | 6.19 | 9.96 |
| accent | bg-surface | 5.47 | 9.07 |
| text-primary | bg-hover | 16.12 | 13.75 |
| text-primary | bg-selected | 17.01 | 11.86 |
| text-secondary | bg-selected | 9.88 | 8.41 |
| accent-text | bg-selected | 7.27 | 8.38 |
| text-primary | bg-surface-raised | 17.74 | 14.51 |
| accent | bg-selected | 5.25 | 6.66 |
| accent | accent-subtle | 5.25 | 6.66 |
| accent | bg-muted | 4.97 | 7.72 |
| text-primary | bg-muted | 16.12 | 13.75 |
| danger-text | bg-muted | 5.88 | 7.57 |
Maturity generated
- Introduced
- v0.3.0
- CSS floor
- Chrome/Edge 99 · Firefox 97 · Safari 15.4
- Alignment scored
- 2026-08-23
- Assistive-tech evidence
- None recorded
Pin this component's first release with npm i @busy-office/ui@0.3.0.
The floor is derived from this component's stylesheet alone — set by
@layer — so it sits at or below the
whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5.
No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour.
What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see
accessibility.
Design-system alignment generated
100% (18/18) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-23.
| Rule | Score | Evidence |
|---|---|---|
| Typography | 3 / 3 | no raw font-size; the em literals are dot and cell geometry, not type |
| Colour | 3 / 3 | zero raw colour, and every marked day is required to carry its meaning in text — colour and shape are the visible half only |
| Spacing | 3 / 3 | the holiday dot geometry now states that it is the NON-COLOUR half of the signal and why all three numbers are em (94.6b); the 2.25em cell and 3px/4px stripe already carried theirs |
| Interaction | 3 / 3 | days can be real submit buttons, so selection posts without JS; data-day is app state written per render, like data-row-state |
| Content | 3 / 3 | opener carries the wrong-choice clause: "Not for a plain date field" — the native input type=date is localized, one tab stop, and gets the platform picker on a phone; the grid earns its place only when WHICH days are special is part of the decision. Blind re-scored 2026-08-23 (Polish round 2), judged the strongest of its batch. |
| Fit | 3 / 3 | a month grid that says which days are special — holidays, shutdowns, period-end, blocked delivery dates |