Open demo

CSS code

HTML
<div class="dcal" data-dcal>
  <div class="dcal-head">
    <h2 class="dcal-title" id="dcal-title"></h2>
    <ul class="dcal-legend" aria-label="Event types">
      <li class="c1">Releases</li><li class="c2">Meetings</li><li class="c3">Deadlines</li>
    </ul>
  </div>
  <table class="dcal-grid" aria-labelledby="dcal-title">
    <thead><tr>
      <th scope="col"><span aria-hidden="true">Mon</span><span class="sr-only">Monday</span></th>
      <th scope="col"><span aria-hidden="true">Tue</span><span class="sr-only">Tuesday</span></th>
      <th scope="col"><span aria-hidden="true">Wed</span><span class="sr-only">Wednesday</span></th>
      <th scope="col"><span aria-hidden="true">Thu</span><span class="sr-only">Thursday</span></th>
      <th scope="col"><span aria-hidden="true">Fri</span><span class="sr-only">Friday</span></th>
      <th scope="col"><span aria-hidden="true">Sat</span><span class="sr-only">Saturday</span></th>
      <th scope="col"><span aria-hidden="true">Sun</span><span class="sr-only">Sunday</span></th>
    </tr></thead>
    <tbody></tbody>
  </table>
</div>
CSS
/* Dark month grid: a <table> with weekday headers. Coloured pips mark event types; each pip row also has
   visually hidden text naming the types, so the colour is never the only signal. */
.dcal{--dcal-line:#1c2433;--dcal-muted:#7c89a3;--dcal-today:#22d3ee;
  position:relative;width:100%;max-width:720px;background:#121722;border:1px solid #212a3a;border-radius:18px;overflow:hidden;color:#e7ebf3}
.dcal-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:18px 22px}
.dcal-title{font-size:1.05rem}
.dcal-title span{color:var(--dcal-muted);font-weight:500}
.dcal-legend{display:flex;gap:14px;list-style:none;font-size:.72rem}
.dcal-legend li{display:inline-flex;align-items:center;gap:5px}
.dcal-legend li::before{content:"";width:8px;height:8px;border-radius:50%;background:currentColor}
.c1{color:#22d3ee}.c2{color:#a78bfa}.c3{color:#fb7185}
.dcal-grid{width:100%;border-collapse:collapse;table-layout:fixed;border-top:1px solid var(--dcal-line)}
.dcal-grid th{padding:9px 0;text-align:center;font-size:.66rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
  color:var(--dcal-muted);border-bottom:1px solid var(--dcal-line)}
.dcal-grid td{height:74px;padding:7px 8px;vertical-align:top;border-bottom:1px solid var(--dcal-line);
  border-right:1px solid var(--dcal-line);font-size:.78rem;color:#aab5c9}
.dcal-grid td:last-child{border-right:0}
.dcal-grid tr:last-child td{border-bottom:0}
.dcal-grid td.dim{color:#727e95;background:#0e131d}
.dcal-grid td[aria-current="date"]{background:#16202f}
.dcal-grid td[aria-current="date"] .dcal-n{color:var(--dcal-today);font-weight:800}
.dcal-pips{display:flex;gap:3px;margin-top:6px}
.dcal-pips i{width:7px;height:7px;border-radius:50%}
.p1{background:#22d3ee}.p2{background:#a78bfa}.p3{background:#fb7185}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
JavaScript
/* Renders the current month, Monday first. Day cells hold the full date as hidden text and today has aria-current="date". */
(() => {
  const root = document.querySelector('[data-dcal]'); if (!root) return;
  const TYPES = { p1: 'Release', p2: 'Meeting', p3: 'Deadline' };
  // Event types keyed by day of the month
  const PIPS = { 2: ['p2'], 5: ['p1', 'p2'], 9: ['p3'], 12: ['p2'], 16: ['p1'], 19: ['p2', 'p3'], 24: ['p1', 'p2', 'p3'], 29: ['p2'] };
  const now = new Date(), y = now.getFullYear(), m = now.getMonth();
  const full = d => d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
  root.querySelector('#dcal-title').innerHTML = `${now.toLocaleDateString('en-US', { month: 'long' })} <span>${y}</span>`;
  const start = new Date(y, m, 1 - (new Date(y, m, 1).getDay() + 6) % 7);
  let h = '';
  for (let i = 0; i < 42; i++) {
    const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i), n = d.getDate();
    if (i % 7 === 0) h += '<tr>';
    if (d.getMonth() !== m) h += `<td class="dim"><span class="dcal-n" aria-hidden="true">${n}</span></td>`;
    else {
      const p = PIPS[n] || [];
      const pips = p.length ? `<span class="dcal-pips">${p.map(c => `<i class="${c}"></i>`).join('')}` +
        `<span class="sr-only">${p.map(c => TYPES[c]).join(', ')}</span></span>` : '';
      h += `<td${n === now.getDate() ? ' aria-current="date"' : ''}><span class="sr-only">${full(d)}</span>` +
        `<span class="dcal-n" aria-hidden="true">${n}</span>${pips}</td>`;
    }
    if (i % 7 === 6) h += '</tr>';
  }
  root.querySelector('tbody').innerHTML = h;
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="card dcal w-100" data-dcal>
  <div class="d-flex align-items-center justify-content-between flex-wrap gap-3 px-4 py-3">
    <h2 class="dcal-title fw-bold mb-0" id="dcal-title"></h2>
    <ul class="dcal-legend list-inline mb-0" aria-label="Event types">
      <li class="list-inline-item c1">Releases</li><li class="list-inline-item c2">Meetings</li><li class="list-inline-item c3">Deadlines</li>
    </ul>
  </div>
  <table class="table dcal-grid mb-0" aria-labelledby="dcal-title">
    <thead><tr>
      <th scope="col"><span aria-hidden="true">Mon</span><span class="visually-hidden">Monday</span></th>
      <th scope="col"><span aria-hidden="true">Tue</span><span class="visually-hidden">Tuesday</span></th>
      <th scope="col"><span aria-hidden="true">Wed</span><span class="visually-hidden">Wednesday</span></th>
      <th scope="col"><span aria-hidden="true">Thu</span><span class="visually-hidden">Thursday</span></th>
      <th scope="col"><span aria-hidden="true">Fri</span><span class="visually-hidden">Friday</span></th>
      <th scope="col"><span aria-hidden="true">Sat</span><span class="visually-hidden">Saturday</span></th>
      <th scope="col"><span aria-hidden="true">Sun</span><span class="visually-hidden">Sunday</span></th>
    </tr></thead>
    <tbody></tbody>
  </table>
</div>
CSS
/* Bootstrap dark colour mode (data-bs-theme="dark") with .card and .table, recoloured through CSS variables. */
.dcal{--dcal-line:#1c2433;--dcal-muted:#7c89a3;--dcal-today:#22d3ee;--bs-card-bg:#121722;--bs-card-border-color:#212a3a;
  --bs-card-border-radius:18px;--bs-card-inner-border-radius:17px;--bs-card-color:#e7ebf3;max-width:720px;overflow:hidden}
.dcal-title{font-size:1.05rem;line-height:normal}
.dcal-title span{color:var(--dcal-muted);font-weight:500}
.dcal-legend{font-size:.72rem}
.dcal-legend li::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor;margin-right:5px}
.dcal-legend .list-inline-item:not(:last-child){margin-right:14px}
.c1{color:#22d3ee}.c2{color:#a78bfa}.c3{color:#fb7185}
.dcal-grid{--bs-table-bg:#121722;--bs-table-color:#aab5c9;--bs-table-border-color:#1c2433;table-layout:fixed;border-top:1px solid var(--dcal-line)}
.dcal-grid th{padding:9px 0;text-align:center;font-size:.66rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--dcal-muted)}
.dcal-grid td{height:74px;padding:7px 8px;vertical-align:top;font-size:.78rem;border-right:1px solid var(--dcal-line)}
.dcal-grid td:last-child{border-right:0}
.dcal-grid tr:last-child td{border-bottom:0}
.dcal-grid td.dim{--bs-table-bg:#0e131d;--bs-table-color:#727e95}
.dcal-grid td[aria-current="date"]{--bs-table-bg:#16202f}
.dcal-grid td[aria-current="date"] .dcal-n{color:var(--dcal-today);font-weight:800}
.dcal-pips{display:flex;gap:3px;margin-top:6px}
.dcal-pips i{width:7px;height:7px;border-radius:50%}
.p1{background:#22d3ee}.p2{background:#a78bfa}.p3{background:#fb7185}
JavaScript
/* Renders the current month, Monday first. Day cells hold the full date as hidden text and today has aria-current="date". */
(() => {
  const root = document.querySelector('[data-dcal]'); if (!root) return;
  const TYPES = { p1: 'Release', p2: 'Meeting', p3: 'Deadline' };
  // Event types keyed by day of the month
  const PIPS = { 2: ['p2'], 5: ['p1', 'p2'], 9: ['p3'], 12: ['p2'], 16: ['p1'], 19: ['p2', 'p3'], 24: ['p1', 'p2', 'p3'], 29: ['p2'] };
  const now = new Date(), y = now.getFullYear(), m = now.getMonth();
  const full = d => d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
  root.querySelector('#dcal-title').innerHTML = `${now.toLocaleDateString('en-US', { month: 'long' })} <span>${y}</span>`;
  const start = new Date(y, m, 1 - (new Date(y, m, 1).getDay() + 6) % 7);
  let h = '';
  for (let i = 0; i < 42; i++) {
    const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i), n = d.getDate();
    if (i % 7 === 0) h += '<tr>';
    if (d.getMonth() !== m) h += `<td class="dim"><span class="dcal-n" aria-hidden="true">${n}</span></td>`;
    else {
      const p = PIPS[n] || [];
      const pips = p.length ? `<span class="dcal-pips">${p.map(c => `<i class="${c}"></i>`).join('')}` +
        `<span class="visually-hidden">${p.map(c => TYPES[c]).join(', ')}</span></span>` : '';
      h += `<td${n === now.getDate() ? ' aria-current="date"' : ''}><span class="visually-hidden">${full(d)}</span>` +
        `<span class="dcal-n" aria-hidden="true">${n}</span>${pips}</td>`;
    }
    if (i % 7 === 6) h += '</tr>';
  }
  root.querySelector('tbody').innerHTML = h;
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="relative w-full max-w-[720px] overflow-hidden rounded-[18px] border border-[#212a3a] bg-[#121722] text-[#e7ebf3]" data-dcal>
  <div class="flex flex-wrap items-center justify-between gap-3 px-[22px] py-[18px]">
    <h2 class="text-[1.05rem] font-bold" id="dcal-title"></h2>
    <ul class="flex gap-3.5 text-[.72rem]" aria-label="Event types">
      <li class="inline-flex items-center gap-[5px] text-dcal-release before:size-2 before:rounded-full before:bg-current">Releases</li>
      <li class="inline-flex items-center gap-[5px] text-dcal-meeting before:size-2 before:rounded-full before:bg-current">Meetings</li>
      <li class="inline-flex items-center gap-[5px] text-dcal-deadline before:size-2 before:rounded-full before:bg-current">Deadlines</li>
    </ul>
  </div>
  <table class="w-full table-fixed border-collapse border-t border-dcal-line" aria-labelledby="dcal-title">
    <thead class="text-[.66rem] font-extrabold tracking-[.08em] text-dcal-muted uppercase"><tr>
      <th scope="col" class="border-b border-dcal-line py-[9px]"><span aria-hidden="true">Mon</span><span class="sr-only">Monday</span></th>
      <th scope="col" class="border-b border-dcal-line py-[9px]"><span aria-hidden="true">Tue</span><span class="sr-only">Tuesday</span></th>
      <th scope="col" class="border-b border-dcal-line py-[9px]"><span aria-hidden="true">Wed</span><span class="sr-only">Wednesday</span></th>
      <th scope="col" class="border-b border-dcal-line py-[9px]"><span aria-hidden="true">Thu</span><span class="sr-only">Thursday</span></th>
      <th scope="col" class="border-b border-dcal-line py-[9px]"><span aria-hidden="true">Fri</span><span class="sr-only">Friday</span></th>
      <th scope="col" class="border-b border-dcal-line py-[9px]"><span aria-hidden="true">Sat</span><span class="sr-only">Saturday</span></th>
      <th scope="col" class="border-b border-dcal-line py-[9px]"><span aria-hidden="true">Sun</span><span class="sr-only">Sunday</span></th>
    </tr></thead>
    <tbody class="[&>tr:last-child>td]:border-b-0"></tbody>
  </table>
</div>
CSS
@theme {
  --color-dcal-line: #1c2433;
  --color-dcal-muted: #7c89a3;
  --color-dcal-release: #22d3ee;
  --color-dcal-meeting: #a78bfa;
  --color-dcal-deadline: #fb7185;
}
JavaScript
/* Renders the current month, Monday first. Day cells hold the full date as hidden text and today has aria-current="date". */
(() => {
  const root = document.querySelector('[data-dcal]'); if (!root) return;
  const CELL = 'group h-[74px] border-r border-b border-dcal-line px-2 py-[7px] align-top text-[.78rem] last:border-r-0';
  const TYPES = { release: 'Release', meeting: 'Meeting', deadline: 'Deadline' };
  const PIP = { release: 'bg-dcal-release', meeting: 'bg-dcal-meeting', deadline: 'bg-dcal-deadline' };
  // Event types keyed by day of the month
  const PIPS = { 2: ['meeting'], 5: ['release', 'meeting'], 9: ['deadline'], 12: ['meeting'], 16: ['release'],
    19: ['meeting', 'deadline'], 24: ['release', 'meeting', 'deadline'], 29: ['meeting'] };
  const now = new Date(), y = now.getFullYear(), m = now.getMonth();
  const full = d => d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
  root.querySelector('#dcal-title').innerHTML = `${now.toLocaleDateString('en-US', { month: 'long' })} <span class="font-medium text-dcal-muted">${y}</span>`;
  const start = new Date(y, m, 1 - (new Date(y, m, 1).getDay() + 6) % 7);
  let h = '';
  for (let i = 0; i < 42; i++) {
    const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i), n = d.getDate();
    if (i % 7 === 0) h += '<tr>';
    if (d.getMonth() !== m) h += `<td class="${CELL} bg-[#0e131d] text-[#727e95]"><span aria-hidden="true">${n}</span></td>`;
    else {
      const p = PIPS[n] || [];
      const pips = p.length ? `<span class="mt-1.5 flex gap-[3px]">${p.map(c => `<i class="size-[7px] rounded-full ${PIP[c]}"></i>`).join('')}` +
        `<span class="sr-only">${p.map(c => TYPES[c]).join(', ')}</span></span>` : '';
      h += `<td class="${CELL} text-[#aab5c9] aria-[current=date]:bg-[#16202f]"${n === now.getDate() ? ' aria-current="date"' : ''}>` +
        `<span class="sr-only">${full(d)}</span><span class="group-aria-[current=date]:font-extrabold group-aria-[current=date]:text-dcal-release" aria-hidden="true">${n}</span>${pips}</td>`;
    }
    if (i % 7 === 6) h += '</tr>';
  }
  root.querySelector('tbody').innerHTML = h;
})();

About this set

A dark month calendar for status pages, release dashboards and night-mode apps. The header pairs the month name with a muted year and a legend of three event types: Releases in cyan, Meetings in violet and Deadlines in rose. Instead of text chips, each day shows a row of small coloured pips, one per event type, which keeps the grid calm when many days are busy and lets the eye scan for clusters. Today gets a lifted background and a bold cyan number; days outside the month sit on a darker shade. The calendar renders the current month automatically from a PIPS object keyed by day, so you can feed it counts from an API without touching the markup. Because colour alone should never carry meaning, every pip row also contains visually hidden text naming the event types, and each cell carries its full date for screen readers. The grid is a real table with weekday column headers. Use it where you want an at-a-glance pattern rather than event titles; for titles in the cells, pick the month grid with events. The Bootstrap version runs in Bootstrap’s dark colour mode with .card, .table and .list-inline, and the Tailwind version uses theme colours for the three types.

What’s included

  • Dark month table generated for the current month
  • Three event types shown as coloured pips with a legend
  • Pips backed by hidden text so colour is not the only signal
  • Today highlighted with a lifted cell and aria-current="date"
  • Muted text lifted to at least 4.5:1 on the dark surface

Accessibility

The month is a table labelled by its heading, with full weekday names in the headers. Each day cell reads its full date followed by the event types, for example "Monday, 5 October 2026, Release, Meeting", and today has aria-current="date". The legend is a list labelled "Event types". Weekday labels and out-of-month numbers were lightened to pass 4.5:1 against the dark background. There are no interactive controls.

Customise it

Edit --dcal-line, --dcal-muted and --dcal-today on .dcal and the .c1 to .c3 and .p1 to .p3 colours. In Bootstrap the table colours come from --bs-table-* variables on .dcal-grid; in Tailwind change the --color-dcal-release, -meeting and -deadline theme colours.