busy-office-ui
Menu

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

August 2026
MoTuWeThFrSaSu
— 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

Week starts Monday — September 2026
MoTuWeThFrSaSu
— non-working day — non-working day
— non-working day — non-working day
— non-working day — non-working day
— non-working day — non-working day
Week starts Sunday — September 2026
SuMoTuWeThFrSa
— 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

August 2026
MoTuWeThFrSaSu
— 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
September 2026
MoTuWeThFrSaSu
— non-working day — non-working day
— non-working day — non-working day
— non-working day — non-working day
— non-working day — non-working day
October 2026
MoTuWeThFrSaSu
— 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

Delivery date — August 2026
MoTuWeThFrSaSu
2728293031
123456
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

ClassRole
.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)
TextonLightDark
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
↑↓ move Enter open Esc close