Calendar

New event
@use 'sprucecss/scss/spruce' as *;

.calendar {
  --calendar-event-color: #{color('primary')};
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  overflow: hidden;

  &__header {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('s');
    padding: spacer('s') spacer('m');
  }

  &__title {
    font-size: font-size('h4');
    margin: 0 auto 0 0;
  }

  &__navigation {
    display: flex;
    gap: spacer('xxs');
  }

  &__weekdays,
  &__days {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));

    // The items sit side by side: clear the stack margins of the base list style.
    > li {
      margin-block: 0;
    }
  }

  &__weekdays {
    border-block-start: config('border-width-base', $display) solid color('border');
    color: color('muted');
    font-size: 0.75rem;
    font-weight: 700;
    padding-block: spacer('xs');
    text-align: center;
    text-transform: uppercase;
  }

  &__days {
    @include clear-list;
    background-color: color('border');
    border-block-start: config('border-width-base', $display) solid color('border');
    gap: config('border-width-base', $display);
  }

  &__day {
    background-color: color('surface');
    display: flex;
    flex-direction: column;
    gap: spacer('xxs');
    min-block-size: 6.5rem;
    min-inline-size: 0;
    padding: spacer('xxs');

    &--outside {
      background-color: color('surface-muted');

      .calendar__date {
        color: color('muted');
      }
    }
  }

  &__date {
    align-items: center;
    aspect-ratio: 1;
    border-radius: radius('pill');
    color: color('heading');
    display: inline-flex;
    font-size: config('font-size-sm', $typography);
    font-weight: 600;
    inline-size: 1.75rem;
    justify-content: center;

    &[aria-current='date'] {
      background-color: color('primary');
      color: color('primary-foreground', 'btn');
    }
  }

  &__events {
    @include clear-list;
    display: grid;
    gap: 2px;

    li {
      margin: 0;
      min-inline-size: 0;
    }

    // The items sit side by side: clear the stack margins of the base list style.
    > li {
      margin-block: 0;
      padding-block: 0;
    }
  }

  &__event {
    background-color: color-alpha('primary', 0.1);
    border-inline-start: 3px solid var(--calendar-event-color);
    border-radius: radius('sm');
    color: color('heading');
    display: block;
    font-size: 0.75rem;
    line-height: 1.4;
    overflow: hidden;
    padding: 0.15rem 0.35rem;
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
    @include transition;

    time {
      color: color('muted');
    }

    &:hover {
      background-color: color-alpha('primary', 0.18);
    }

    &:focus-visible {
      outline: 2px solid color('focus-ring');
      outline-offset: 1px;
    }

    &--all-day {
      background-color: var(--calendar-event-color);
      border-inline-start: 0;
      color: hsl(0deg 0% 100%);
      font-weight: 600;

      &:hover {
        background-color: var(--calendar-event-color);
        opacity: 0.85;
      }
    }

    &--success {
      --calendar-event-color: #{color('success', 'alert')};
    }

    &--warning {
      --calendar-event-color: #{color('warning', 'alert')};
      color: hsl(0deg 0% 0%);
    }

    &--info {
      --calendar-event-color: #{color('info', 'alert')};
    }
  }

  // Below the sm breakpoint, the month grid becomes an agenda: only the days with events, in a list.
  @include breakpoint-down('sm') {
    &__weekdays {
      display: none;
    }

    &__days {
      grid-template-columns: 1fr;
    }

    &__day {
      align-items: flex-start;
      flex-direction: row;
      gap: spacer('s');
      min-block-size: 0;
      padding: spacer('xs') spacer('m');

      &:not([data-has-events], .calendar__day--today) {
        display: none;
      }
    }

    &__events {
      flex-grow: 1;
      gap: spacer('xxs');
    }

    &__event {
      font-size: config('font-size-sm', $typography);
      padding: spacer('xxs') spacer('xs');
      white-space: normal;
    }
  }
}

[dir='rtl'] {
  .calendar__title {
    margin: 0 0 0 auto;
  }

  .calendar__chevron {
    scale: -1 1;
  }
}
<section class="calendar" aria-labelledby="calendar-title">
    <header class="calendar__header">
        <h3 class="calendar__title" id="calendar-title"><time datetime="2026-10">October 2026</time></h3>
        <nav class="calendar__navigation" aria-label="Months">
            <a class="btn btn--outline-secondary btn--sm btn--icon" href="?month=2026-09">
                <svg class="btn__icon calendar__chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m15 18-6-6 6-6"/></svg>
                <span class="sr-only">Previous month</span>
            </a>
            <a class="btn btn--outline-secondary btn--sm" href="?month=2026-10">Today</a>
            <a class="btn btn--outline-secondary btn--sm btn--icon" href="?month=2026-11">
                <svg class="btn__icon calendar__chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m9 18 6-6-6-6"/></svg>
                <span class="sr-only">Next month</span>
            </a>
        </nav>
        <a class="btn btn--primary btn--sm" href="#">New event</a>
    </header>
    <div class="calendar__weekdays" aria-hidden="true">
        <span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span><span>Sun</span>
    </div>
    <ol class="calendar__days" role="list">
        <li class="calendar__day calendar__day--outside">
            <time class="calendar__date" datetime="2026-09-28"><span class="sr-only">Monday, September </span>28</time>
        </li>
        <li class="calendar__day calendar__day--outside">
            <time class="calendar__date" datetime="2026-09-29"><span class="sr-only">Tuesday, September </span>29</time>
        </li>
        <li class="calendar__day calendar__day--outside">
            <time class="calendar__date" datetime="2026-09-30"><span class="sr-only">Wednesday, September </span>30</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-01"><span class="sr-only">Thursday, October </span>1</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-01T14:00">2 PM</time> Q4 planning</a></li>
            </ul>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-02"><span class="sr-only">Friday, October </span>2</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--success calendar__event--all-day" href="#"><span class="sr-only">All day: </span>Payroll</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-03"><span class="sr-only">Saturday, October </span>3</time>
        </li>
        <li class="calendar__day calendar__day--today" data-has-events>
            <time class="calendar__date" datetime="2026-10-04" aria-current="date"><span class="sr-only">Sunday, October </span>4<span class="sr-only">, today</span></time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-04T10:00">10 AM</time> Product photo shoot</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-05"><span class="sr-only">Monday, October </span>5</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-06"><span class="sr-only">Tuesday, October </span>6</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-06T09:30">9:30 AM</time> Sprint review</a></li>
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-06T15:00">3 PM</time> 1:1 with Ben</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-07"><span class="sr-only">Wednesday, October </span>7</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-08"><span class="sr-only">Thursday, October </span>8</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-09"><span class="sr-only">Friday, October </span>9</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--warning calendar__event--all-day" href="#"><span class="sr-only">All day: </span>Black Friday prep</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-10"><span class="sr-only">Saturday, October </span>10</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-11"><span class="sr-only">Sunday, October </span>11</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-12"><span class="sr-only">Monday, October </span>12</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-13"><span class="sr-only">Tuesday, October </span>13</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-13T11:00">11 AM</time> Supplier call</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-14"><span class="sr-only">Wednesday, October </span>14</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-15"><span class="sr-only">Thursday, October </span>15</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--info calendar__event--all-day" href="#"><span class="sr-only">All day: </span>Newsletter goes out</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-16"><span class="sr-only">Friday, October </span>16</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-17"><span class="sr-only">Saturday, October </span>17</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-18"><span class="sr-only">Sunday, October </span>18</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-19"><span class="sr-only">Monday, October </span>19</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-20"><span class="sr-only">Tuesday, October </span>20</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-20T10:00">10 AM</time> Sprint review</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-21"><span class="sr-only">Wednesday, October </span>21</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-22"><span class="sr-only">Thursday, October </span>22</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--warning calendar__event--all-day" href="#"><span class="sr-only">All day: </span>Inventory count</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-23"><span class="sr-only">Friday, October </span>23</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-24"><span class="sr-only">Saturday, October </span>24</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-25"><span class="sr-only">Sunday, October </span>25</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-26"><span class="sr-only">Monday, October </span>26</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-27"><span class="sr-only">Tuesday, October </span>27</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-27T13:00">1 PM</time> Marketing sync</a></li>
                <li><a class="calendar__event calendar__event--primary" href="#"><time datetime="2026-10-27T16:00">4 PM</time> Interview: designer</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-28"><span class="sr-only">Wednesday, October </span>28</time>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-29"><span class="sr-only">Thursday, October </span>29</time>
        </li>
        <li class="calendar__day" data-has-events>
            <time class="calendar__date" datetime="2026-10-30"><span class="sr-only">Friday, October </span>30</time>
            <ul class="calendar__events" role="list">
                <li><a class="calendar__event calendar__event--success calendar__event--all-day" href="#"><span class="sr-only">All day: </span>Month-end close</a></li>
            </ul>
        </li>
        <li class="calendar__day">
            <time class="calendar__date" datetime="2026-10-31"><span class="sr-only">Saturday, October </span>31</time>
        </li>
        <li class="calendar__day calendar__day--outside">
            <time class="calendar__date" datetime="2026-11-01"><span class="sr-only">Sunday, November </span>1</time>
        </li>
    </ol>
</section>

Calendar

A month calendar with events. On wide screens, it's a seven-column grid; on phones, it turns into an agenda list of the days with events. No JavaScript.

Anatomy

  • .calendar__header: the month title (a time element), the previous/today/next links and a primary action.
  • .calendar__weekdays: the decorative weekday row.
  • .calendar__day: one li per day (--outside for the days of the neighboring months, data-has-events when it has events), with the __date and a list of __event links.
  • .calendar__event: an event with an optional time; --all-day for full-day events and --success, --warning, --info for the categories.

Accessibility and Usability

  • The days are an ordered list, and every date has hidden context ("Sunday, October 4, today"), so the visual weekday columns aren't needed to understand it. The weekday row is aria-hidden.
  • Today is marked with aria-current="date", not only with the highlight.
  • All-day events have hidden "All day:" text; the other events start with their time.
  • The month navigation uses links (?month=2026-11), so it works without JavaScript and every month has a URL.

Technical Details

  • Render the days on the server: start on the Monday (or Sunday) before the 1st and fill complete weeks. The grid uses repeat(7, minmax(0, 1fr)), and the 1px gap on a border-colored background draws the cell borders.
  • Long event titles are truncated with an ellipsis in the grid; the full title is in the link, and it wraps in the agenda layout.
  • The agenda view hides the days without events with :not([data-has-events]) below the sm breakpoint (today is always shown).
  • The chevrons are mirrored in right-to-left languages.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.