Open demo

CSS code

HTML
<div class="combo" data-combo>
  <div class="combo-cal">
    <h2 id="combo-title"></h2>
    <table class="combo-grid" aria-labelledby="combo-title">
      <thead><tr>
        <th scope="col"><span aria-hidden="true">M</span><span class="sr-only">Monday</span></th>
        <th scope="col"><span aria-hidden="true">T</span><span class="sr-only">Tuesday</span></th>
        <th scope="col"><span aria-hidden="true">W</span><span class="sr-only">Wednesday</span></th>
        <th scope="col"><span aria-hidden="true">T</span><span class="sr-only">Thursday</span></th>
        <th scope="col"><span aria-hidden="true">F</span><span class="sr-only">Friday</span></th>
        <th scope="col"><span aria-hidden="true">S</span><span class="sr-only">Saturday</span></th>
        <th scope="col"><span aria-hidden="true">S</span><span class="sr-only">Sunday</span></th>
      </tr></thead>
      <tbody></tbody>
    </table>
  </div>
  <aside class="combo-side" aria-labelledby="combo-side-title">
    <h3 id="combo-side-title">This month</h3>
    <ul>
      <li class="combo-card"><time data-day="7"></time><b>Quarterly planning</b><span>All-day workshop · Studio 2</span></li>
      <li class="combo-card"><time data-day="15"></time><b>Client showcase</b><span>Live demo for Arcadia and Fieldnote</span></li>
      <li class="combo-card"><time data-day="24"></time><b>Release freeze</b><span>Code freeze ahead of v5 launch</span></li>
    </ul>
  </aside>
</div>
CSS
/* Month table and an events sidebar in one panel. The panel stacks below 760px. */
.combo{--combo-accent:#0f766e;position:relative;display:grid;grid-template-columns:1fr 280px;width:100%;max-width:880px;background:#fff;
  border:1px solid #e5e9f1;border-radius:18px;overflow:hidden;color:#1a2233;box-shadow:0 18px 42px -22px rgba(18,28,52,.2)}
.combo-cal{padding:24px}
.combo-cal h2{font-size:1.02rem;margin-bottom:16px}
.combo-grid{width:100%;border-collapse:separate;border-spacing:3px;table-layout:fixed;text-align:center}
.combo-grid th{font-size:.64rem;font-weight:800;text-transform:uppercase;color:#667489;padding:6px 0}
.combo-grid td{padding:0}
.combo-d{aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.82rem;font-weight:600;border-radius:11px;
  color:#2d3a52;position:relative}
.combo-d.dim{color:#6b7489;font-weight:500}
.combo-d[aria-current="date"]{background:var(--combo-accent);color:#fff;font-weight:900}
.combo-d.ev::after{content:"";position:absolute;bottom:6px;width:5px;height:5px;border-radius:50%;background:var(--combo-accent)}
.combo-d.ev[aria-current="date"]::after{background:#fff}
.combo-side{background:#0f172a;color:#dbe4f2;padding:24px;display:flex;flex-direction:column;gap:14px}
.combo-side h3{font-size:.78rem;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:#7d8aa5}
.combo-side ul{display:flex;flex-direction:column;gap:14px;list-style:none}
.combo-card{background:#16213a;border:1px solid #232f4d;border-radius:12px;padding:13px 15px}
.combo-card time{font-size:.7rem;font-weight:800;color:#5eead4;text-transform:uppercase;letter-spacing:.05em}
.combo-card b{display:block;font-size:.88rem;margin:3px 0 2px;color:#fff}
.combo-card span{font-size:.76rem;color:#8c99b4}
.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}
@media (max-width:760px){.combo{grid-template-columns:1fr}}
JavaScript
/* Renders the current month, Monday first, and fills the sidebar dates from the same EVENTS list.
   Event days get a dot plus the event name as hidden text; today has aria-current="date". */
(() => {
  const root = document.querySelector('[data-combo]'); if (!root) return;
  const CLS = { day: 'combo-d', dim: 'combo-d dim', ev: 'ev', sr: 'sr-only' };
  const EVENTS = { 7: 'Quarterly planning', 15: 'Client showcase', 24: 'Release freeze' }; // day of month: title
  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' });
  const iso = d => `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
  root.querySelector('#combo-title').textContent = now.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
  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><span class="${CLS.dim}" aria-hidden="true">${n}</span></td>`;
    else h += `<td><span class="${CLS.day}${EVENTS[n] ? ' ' + CLS.ev : ''}"${n === now.getDate() ? ' aria-current="date"' : ''}>` +
      `<span aria-hidden="true">${n}</span><span class="${CLS.sr}">${full(d)}${EVENTS[n] ? ': ' + EVENTS[n] : ''}</span></span></td>`;
    if (i % 7 === 6) h += '</tr>';
  }
  root.querySelector('tbody').innerHTML = h;
  root.querySelectorAll('time[data-day]').forEach(t => {
    const d = Number(t.dataset.day);
    t.dateTime = iso(d);
    t.textContent = new Date(y, m, d).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="card combo w-100" data-combo>
  <div class="row g-0">
    <div class="col-md combo-cal p-4">
      <h2 class="fw-bold mb-3" id="combo-title"></h2>
      <table class="table table-borderless text-center combo-grid mb-0" aria-labelledby="combo-title">
        <thead><tr>
          <th scope="col"><span aria-hidden="true">M</span><span class="visually-hidden">Monday</span></th>
          <th scope="col"><span aria-hidden="true">T</span><span class="visually-hidden">Tuesday</span></th>
          <th scope="col"><span aria-hidden="true">W</span><span class="visually-hidden">Wednesday</span></th>
          <th scope="col"><span aria-hidden="true">T</span><span class="visually-hidden">Thursday</span></th>
          <th scope="col"><span aria-hidden="true">F</span><span class="visually-hidden">Friday</span></th>
          <th scope="col"><span aria-hidden="true">S</span><span class="visually-hidden">Saturday</span></th>
          <th scope="col"><span aria-hidden="true">S</span><span class="visually-hidden">Sunday</span></th>
        </tr></thead>
        <tbody></tbody>
      </table>
    </div>
    <aside class="col-md combo-side p-4 d-flex flex-column gap-3" aria-labelledby="combo-side-title">
      <h3 class="text-uppercase mb-0" id="combo-side-title">This month</h3>
      <ul class="list-group">
        <li class="list-group-item"><time class="text-uppercase" data-day="7"></time><b class="d-block">Quarterly planning</b><span>All-day workshop · Studio 2</span></li>
        <li class="list-group-item"><time class="text-uppercase" data-day="15"></time><b class="d-block">Client showcase</b><span>Live demo for Arcadia and Fieldnote</span></li>
        <li class="list-group-item"><time class="text-uppercase" data-day="24"></time><b class="d-block">Release freeze</b><span>Code freeze ahead of v5 launch</span></li>
      </ul>
    </aside>
  </div>
</div>
CSS
/* Bootstrap .card with a .row: the month .table in .col-md, the sidebar .list-group in a fixed 280px column (stacks below 768px). */
.combo{--combo-accent:#0f766e;--bs-card-border-color:#e5e9f1;--bs-card-border-radius:18px;--bs-card-color:#1a2233;max-width:880px;
  overflow:hidden;box-shadow:0 18px 42px -22px rgba(18,28,52,.2)}
.combo-cal h2{font-size:1.02rem;line-height:normal}
.combo-grid{--bs-table-bg:transparent;border-collapse:separate;border-spacing:3px;table-layout:fixed}
.combo-grid>:not(caption)>*>*{padding:0}
.combo-grid th{font-size:.64rem;font-weight:800;text-transform:uppercase;color:#667489;padding:6px 0}
.combo-d{aspect-ratio:1;font-size:.82rem;font-weight:600;border-radius:11px;color:#2d3a52;position:relative}
.combo-d.dim{color:#6b7489;font-weight:500}
.combo-d[aria-current="date"]{background:var(--combo-accent);color:#fff;font-weight:900}
.combo-d.ev::after{content:"";position:absolute;bottom:6px;width:5px;height:5px;border-radius:50%;background:var(--combo-accent)}
.combo-d.ev[aria-current="date"]::after{background:#fff}
.combo-side{background:#0f172a;color:#dbe4f2}
@media (min-width:768px){.combo-side{flex:0 0 280px;max-width:280px}}
.combo-side h3{font-size:.78rem;font-weight:800;letter-spacing:.09em;color:#7d8aa5;line-height:normal}
.combo-side .list-group{--bs-list-group-bg:#16213a;--bs-list-group-border-color:#232f4d;--bs-list-group-border-radius:12px;
  --bs-list-group-color:#dbe4f2;--bs-list-group-item-padding-x:15px;--bs-list-group-item-padding-y:13px;gap:14px}
.combo-side .list-group-item{border-width:1px;border-radius:var(--bs-list-group-border-radius)}
.combo-side time{font-size:.7rem;font-weight:800;color:#5eead4;letter-spacing:.05em}
.combo-side b{font-size:.88rem;margin:3px 0 2px;color:#fff}
.combo-side span{font-size:.76rem;color:#8c99b4}
JavaScript
/* Renders the current month, Monday first, and fills the sidebar dates from the same EVENTS list.
   Event days get a dot plus the event name as hidden text; today has aria-current="date". */
(() => {
  const root = document.querySelector('[data-combo]'); if (!root) return;
  const CLS = { day: 'combo-d d-flex align-items-center justify-content-center', dim: 'combo-d dim d-flex align-items-center justify-content-center', ev: 'ev', sr: 'visually-hidden' };
  const EVENTS = { 7: 'Quarterly planning', 15: 'Client showcase', 24: 'Release freeze' }; // day of month: title
  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' });
  const iso = d => `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
  root.querySelector('#combo-title').textContent = now.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
  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><span class="${CLS.dim}" aria-hidden="true">${n}</span></td>`;
    else h += `<td><span class="${CLS.day}${EVENTS[n] ? ' ' + CLS.ev : ''}"${n === now.getDate() ? ' aria-current="date"' : ''}>` +
      `<span aria-hidden="true">${n}</span><span class="${CLS.sr}">${full(d)}${EVENTS[n] ? ': ' + EVENTS[n] : ''}</span></span></td>`;
    if (i % 7 === 6) h += '</tr>';
  }
  root.querySelector('tbody').innerHTML = h;
  root.querySelectorAll('time[data-day]').forEach(t => {
    const d = Number(t.dataset.day);
    t.dateTime = iso(d);
    t.textContent = new Date(y, m, d).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="relative grid w-full max-w-[880px] grid-cols-[1fr_280px] overflow-hidden rounded-[18px] border border-[#e5e9f1] bg-white text-[#1a2233] shadow-[0_18px_42px_-22px_rgba(18,28,52,.2)] max-[760px]:grid-cols-1" data-combo>
  <div class="p-6">
    <h2 class="mb-4 text-[1.02rem] font-bold" id="combo-title"></h2>
    <table class="w-full table-fixed border-separate border-spacing-[3px] text-center" aria-labelledby="combo-title">
      <thead class="text-[.64rem] font-extrabold text-[#667489] uppercase"><tr>
        <th scope="col" class="py-1.5"><span aria-hidden="true">M</span><span class="sr-only">Monday</span></th>
        <th scope="col" class="py-1.5"><span aria-hidden="true">T</span><span class="sr-only">Tuesday</span></th>
        <th scope="col" class="py-1.5"><span aria-hidden="true">W</span><span class="sr-only">Wednesday</span></th>
        <th scope="col" class="py-1.5"><span aria-hidden="true">T</span><span class="sr-only">Thursday</span></th>
        <th scope="col" class="py-1.5"><span aria-hidden="true">F</span><span class="sr-only">Friday</span></th>
        <th scope="col" class="py-1.5"><span aria-hidden="true">S</span><span class="sr-only">Saturday</span></th>
        <th scope="col" class="py-1.5"><span aria-hidden="true">S</span><span class="sr-only">Sunday</span></th>
      </tr></thead>
      <tbody></tbody>
    </table>
  </div>
  <aside class="flex flex-col gap-3.5 bg-combo-side p-6 text-[#dbe4f2]" aria-labelledby="combo-side-title">
    <h3 class="text-[.78rem] font-extrabold tracking-[.09em] text-[#7d8aa5] uppercase" id="combo-side-title">This month</h3>
    <ul class="flex flex-col gap-3.5">
      <li class="rounded-xl border border-[#232f4d] bg-combo-card px-[15px] py-[13px]"><time class="text-[.7rem] font-extrabold tracking-[.05em] text-[#5eead4] uppercase" data-day="7"></time><b class="mt-[3px] mb-0.5 block text-[.88rem] text-white">Quarterly planning</b><span class="text-[.76rem] text-[#8c99b4]">All-day workshop · Studio 2</span></li>
      <li class="rounded-xl border border-[#232f4d] bg-combo-card px-[15px] py-[13px]"><time class="text-[.7rem] font-extrabold tracking-[.05em] text-[#5eead4] uppercase" data-day="15"></time><b class="mt-[3px] mb-0.5 block text-[.88rem] text-white">Client showcase</b><span class="text-[.76rem] text-[#8c99b4]">Live demo for Arcadia and Fieldnote</span></li>
      <li class="rounded-xl border border-[#232f4d] bg-combo-card px-[15px] py-[13px]"><time class="text-[.7rem] font-extrabold tracking-[.05em] text-[#5eead4] uppercase" data-day="24"></time><b class="mt-[3px] mb-0.5 block text-[.88rem] text-white">Release freeze</b><span class="text-[.76rem] text-[#8c99b4]">Code freeze ahead of v5 launch</span></li>
    </ul>
  </aside>
</div>
CSS
@theme {
  --color-combo-accent: #0f766e;
  --color-combo-side: #0f172a;
  --color-combo-card: #16213a;
}
JavaScript
/* Renders the current month, Monday first, and fills the sidebar dates from the same EVENTS list.
   Event days get a dot plus the event name as hidden text; today has aria-current="date". */
(() => {
  const root = document.querySelector('[data-combo]'); if (!root) return;
  const CLS = { day: 'relative flex aspect-square items-center justify-center rounded-[11px] text-[.82rem] font-semibold text-[#2d3a52] aria-[current=date]:bg-combo-accent aria-[current=date]:font-black aria-[current=date]:text-white', dim: 'relative flex aspect-square items-center justify-center rounded-[11px] text-[.82rem] font-medium text-[#6b7489]', ev: 'after:absolute after:bottom-1.5 after:size-[5px] after:rounded-full after:bg-combo-accent aria-[current=date]:after:bg-white', sr: 'sr-only' };
  const EVENTS = { 7: 'Quarterly planning', 15: 'Client showcase', 24: 'Release freeze' }; // day of month: title
  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' });
  const iso = d => `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
  root.querySelector('#combo-title').textContent = now.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
  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><span class="${CLS.dim}" aria-hidden="true">${n}</span></td>`;
    else h += `<td><span class="${CLS.day}${EVENTS[n] ? ' ' + CLS.ev : ''}"${n === now.getDate() ? ' aria-current="date"' : ''}>` +
      `<span aria-hidden="true">${n}</span><span class="${CLS.sr}">${full(d)}${EVENTS[n] ? ': ' + EVENTS[n] : ''}</span></span></td>`;
    if (i % 7 === 6) h += '</tr>';
  }
  root.querySelector('tbody').innerHTML = h;
  root.querySelectorAll('time[data-day]').forEach(t => {
    const d = Number(t.dataset.day);
    t.dateTime = iso(d);
    t.textContent = new Date(y, m, d).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
  });
})();

About this set

A month calendar and an events list combined in one panel, for dashboards, community sites, studios and course pages that want both the overview and the details. On the left, a clean month grid with rounded day cells; on the right, a dark navy sidebar headed “This month” with a card for each event showing its date in teal capitals, a title and a short description. Days that have an event get a teal dot under the number, and today is a solid teal tile; when today also has an event the dot turns white so it stays visible. Both the dots and the sidebar dates come from one EVENTS object in the script, so they can never disagree. Below 760 pixels the sidebar drops under the calendar and the panel becomes a single column. Event days carry the event title as hidden text, so a screen reader hears “Thursday, 15 October 2026: Client showcase” in the grid and the same event again in the list. The Bootstrap version uses a .card with a .row, a borderless .table and a recoloured .list-group, and the Tailwind version uses a two-column grid that collapses with a max-width variant.

What’s included

  • Month table and events sidebar in one panel
  • Event dots and sidebar dates driven by one EVENTS object
  • Today tile with a white dot when it has an event
  • Sidebar stacks under the calendar below 760px
  • Event titles available as hidden text in the grid

Accessibility

The calendar is a table labelled by the month heading, with full weekday names and full dates as hidden text, and the event title appended on event days. Today has aria-current="date". The sidebar is an aside labelled "This month" containing a list, and its dates are time elements with datetime attributes. Weekday letters and out-of-month numbers were darkened to pass 4.5:1. There are no interactive controls.

Customise it

Change --combo-accent on .combo for the teal, and the 280px sidebar column. In Bootstrap the sidebar cards are restyled through --bs-list-group-* variables; in Tailwind edit --color-combo-accent, --color-combo-side and --color-combo-card.