Open demo

CSS code

HTML
<section class="yr" data-yr aria-labelledby="yr-title">
  <h2 class="yr-title" id="yr-title">Plan the whole <span></span></h2>
  <div class="yr-grid"></div>
</section>
CSS
/* Year overview: twelve small month tables, generated for the current year. Today has aria-current="date". */
.yr{--yr-accent:#f59e0b;--yr-muted:#7b879f;position:relative;max-width:1040px;margin:0 auto;color:#e8ebf1}
.yr-title{text-align:center;font-size:1.4rem;letter-spacing:-.01em;margin-bottom:30px}
.yr-title span{color:var(--yr-accent)}
.yr-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:18px}
.yr-month{background:#171c25;border:1px solid #242c3a;border-radius:14px;padding:14px}
.yr-month h3{font-size:.82rem;margin-bottom:10px;color:var(--yr-accent)}
.yr-month table{width:100%;border-collapse:separate;border-spacing:1px;table-layout:fixed;text-align:center;font-size:.62rem}
.yr-month th{color:var(--yr-muted);font-weight:800;padding:0 0 4px}
.yr-month td{padding:3px 0;color:#aab3c5;border-radius:5px}
.yr-month td[aria-current="date"]{background:var(--yr-accent);color:#101318;font-weight:900}
.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
/* Builds a table per month (Monday first). Each day keeps its full date as hidden text for screen readers. */
(() => {
  const root = document.querySelector('[data-yr]'); if (!root) return;
  const now = new Date(), Y = now.getFullYear();
  const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
  const head = '<thead><tr>' + DAYS.map(d => `<th scope="col"><span aria-hidden="true">${d[0]}</span><span class="sr-only">${d}</span></th>`).join('') + '</tr></thead>';
  const full = d => d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
  root.querySelector('.yr-title span').textContent = Y;
  let out = '';
  for (let m = 0; m < 12; m++) {
    const name = new Date(Y, m, 1).toLocaleDateString('en-US', { month: 'long' });
    const first = (new Date(Y, m, 1).getDay() + 6) % 7, days = new Date(Y, m + 1, 0).getDate();
    let rows = '';
    for (let i = 0; i < 42; i++) {
      const d = i - first + 1;
      if (i % 7 === 0) rows += '<tr>';
      if (d < 1 || d > days) rows += '<td></td>';
      else {
        const dt = new Date(Y, m, d), today = dt.toDateString() === now.toDateString();
        rows += `<td${today ? ' aria-current="date"' : ''}><span aria-hidden="true">${d}</span><span class="sr-only">${full(dt)}</span></td>`;
      }
      if (i % 7 === 6) rows += '</tr>';
    }
    out += `<div class="yr-month"><h3 id="yr-m${m}">${name}</h3><table aria-labelledby="yr-m${m}">${head}<tbody>${rows}</tbody></table></div>`;
  }
  root.querySelector('.yr-grid').innerHTML = out;
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<section class="yr mx-auto" data-yr aria-labelledby="yr-title">
  <h2 class="yr-title fw-bold text-center mb-4 pb-1" id="yr-title">Plan the whole <span></span></h2>
  <div class="yr-grid"></div>
</section>
CSS
/* Bootstrap dark colour mode: a .row of .card months, each with a borderless .table-sm. Recoloured through CSS variables. */
.yr{--yr-accent:#f59e0b;--yr-muted:#7b879f;max-width:1040px;color:#e8ebf1}
.yr-title{font-size:1.4rem;letter-spacing:-.01em;line-height:normal}
.yr-title span{color:var(--yr-accent)}
.yr-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:18px}
.yr .card{--bs-card-bg:#171c25;--bs-card-border-color:#242c3a;--bs-card-border-radius:14px;--bs-card-spacer-x:14px;--bs-card-spacer-y:14px}
.yr .card h3{font-size:.82rem;color:var(--yr-accent);line-height:normal}
.yr .table{--bs-table-bg:transparent;--bs-table-color:#aab3c5;border-collapse:separate;border-spacing:1px;table-layout:fixed;font-size:.62rem}
.yr .table>:not(caption)>*>*{padding:3px 0;border-radius:5px}
.yr .table th{color:var(--yr-muted);font-weight:800;padding:0 0 4px}
.yr .table td[aria-current="date"]{--bs-table-bg:var(--yr-accent);--bs-table-color:#101318;font-weight:900}
JavaScript
/* Builds a card and table per month (Monday first). Each day keeps its full date as hidden text for screen readers. */
(() => {
  const root = document.querySelector('[data-yr]'); if (!root) return;
  const now = new Date(), Y = now.getFullYear();
  const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
  const head = '<thead><tr>' + DAYS.map(d => `<th scope="col"><span aria-hidden="true">${d[0]}</span><span class="visually-hidden">${d}</span></th>`).join('') + '</tr></thead>';
  const full = d => d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
  root.querySelector('.yr-title span').textContent = Y;
  let out = '';
  for (let m = 0; m < 12; m++) {
    const name = new Date(Y, m, 1).toLocaleDateString('en-US', { month: 'long' });
    const first = (new Date(Y, m, 1).getDay() + 6) % 7, days = new Date(Y, m + 1, 0).getDate();
    let rows = '';
    for (let i = 0; i < 42; i++) {
      const d = i - first + 1;
      if (i % 7 === 0) rows += '<tr>';
      if (d < 1 || d > days) rows += '<td></td>';
      else {
        const dt = new Date(Y, m, d), today = dt.toDateString() === now.toDateString();
        rows += `<td${today ? ' aria-current="date"' : ''}><span aria-hidden="true">${d}</span><span class="visually-hidden">${full(dt)}</span></td>`;
      }
      if (i % 7 === 6) rows += '</tr>';
    }
    out += `<div class="card"><div class="card-body"><h3 class="fw-bold mb-2 pb-1" id="yr-m${m}">${name}</h3>` +
      `<table class="table table-borderless text-center mb-0" aria-labelledby="yr-m${m}">${head}<tbody>${rows}</tbody></table></div></div>`;
  }
  root.querySelector('.yr-grid').innerHTML = out;
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<section class="relative mx-auto max-w-[1040px] text-[#e8ebf1]" data-yr aria-labelledby="yr-title">
  <h2 class="mb-[30px] text-center text-[1.4rem] font-bold tracking-[-.01em]" id="yr-title">Plan the whole <span class="text-yr-accent"></span></h2>
  <div class="grid grid-cols-[repeat(auto-fill,minmax(210px,1fr))] gap-[18px]"></div>
</section>
CSS
@theme {
  --color-yr-accent: #f59e0b;
  --color-yr-muted: #7b879f;
}
JavaScript
/* Builds a table per month (Monday first). Each day keeps its full date as hidden text for screen readers. */
(() => {
  const root = document.querySelector('[data-yr]'); if (!root) return;
  const now = new Date(), Y = now.getFullYear();
  const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
  const head = '<thead class="font-extrabold text-yr-muted"><tr>' + DAYS.map(d => `<th scope="col" class="pb-1"><span aria-hidden="true">${d[0]}</span><span class="sr-only">${d}</span></th>`).join('') + '</tr></thead>';
  const TD = 'rounded-[5px] py-[3px] text-[#aab3c5] aria-[current=date]:bg-yr-accent aria-[current=date]:font-black aria-[current=date]:text-[#101318]';
  const full = d => d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
  root.querySelector('#yr-title span').textContent = Y;
  let out = '';
  for (let m = 0; m < 12; m++) {
    const name = new Date(Y, m, 1).toLocaleDateString('en-US', { month: 'long' });
    const first = (new Date(Y, m, 1).getDay() + 6) % 7, days = new Date(Y, m + 1, 0).getDate();
    let rows = '';
    for (let i = 0; i < 42; i++) {
      const d = i - first + 1;
      if (i % 7 === 0) rows += '<tr>';
      if (d < 1 || d > days) rows += `<td class="${TD}"></td>`;
      else {
        const dt = new Date(Y, m, d), today = dt.toDateString() === now.toDateString();
        rows += `<td class="${TD}"${today ? ' aria-current="date"' : ''}><span aria-hidden="true">${d}</span><span class="sr-only">${full(dt)}</span></td>`;
      }
      if (i % 7 === 6) rows += '</tr>';
    }
    out += `<div class="rounded-[14px] border border-[#242c3a] bg-[#171c25] p-3.5"><h3 class="mb-2.5 text-[.82rem] font-bold text-yr-accent" id="yr-m${m}">${name}</h3>` +
      `<table class="w-full table-fixed border-separate border-spacing-px text-center text-[.62rem]" aria-labelledby="yr-m${m}">${head}<tbody>${rows}</tbody></table></div>`;
  }
  root.querySelector('.grid').innerHTML = out;
})();

About this set

A dark, full-year planner that shows all twelve months at once, useful for holiday planning, content calendars, academic years and annual reports. A heading reads “Plan the whole” followed by the year in amber, and below it twelve small month cards flow into a responsive grid: four across on a wide screen, fewer on tablets and a single column on phones, using repeat(auto-fill, minmax(210px, 1fr)) so no media queries are needed. Each card has the month name in amber and a seven-column table with single-letter weekday headers, Monday first. Today is picked out with an amber fill in its month. A short script builds the whole year for the current date, so the page never goes stale; change one line to render a different year. The output is plain tables, so you can extend the script to mark holidays, school terms or booked weeks by adding a class or a data attribute to particular cells. The Bootstrap version runs in Bootstrap’s dark colour mode and places each month in a .card with a borderless .table, and the Tailwind version uses an arbitrary grid template with the same auto-fill rule.

What’s included

  • Twelve month tables generated for the current year
  • Responsive auto-fill grid without media queries
  • Today filled in amber with aria-current="date"
  • Weekday headers with full names for screen readers
  • Dark theme with labels lifted to 4.5:1

Accessibility

Each month is a table labelled by its month heading, with full weekday names as hidden header text and the full date of every day as hidden cell text, so a screen reader reads "Thursday, 15 October 2026" rather than a bare number. Today has aria-current="date". The weekday letters were lightened to pass 4.5:1 on the dark cards. There are no interactive controls.

Customise it

Change --yr-accent and --yr-muted on .yr, and the 210px minimum card width in .yr-grid. In Bootstrap the cards are restyled through --bs-card-* variables and the tables through --bs-table-*; in Tailwind edit --color-yr-accent and --color-yr-muted.