Robert Birming

Photo calendar

Show your photos by date, as a monthly calendar.1

Each month gets its own page, with a photo on every day you took one. The photos get a tinted look that lights up on hover, and a click opens them full size. Inspired by CalenBear.

Preview

← MarchApril 2026May →

MoTuWeThFrSaSu
  1. Östra station restaurant, Stockholm

  2. Statue in Nacka

  3. Saltsjö-Boo

  4. Gnuttarna

  5. Odenplan, Stockholm

  6. Sweden

  7. Sweden

  8. Sweden

  9. Sweden

How to use

Add the markup below to each monthly page, then add the styles to your theme. The lightbox script is optional. Without it, a click opens the photo on its own.

Markup

<div class="photo-cal-nav">

<span>[← March](/march-2026/)</span><span>April 2026</span><span>May →</span>

</div>

<div class="photo-cal" style="--start: 3">
<div class="photo-cal-head"><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span><span>Su</span></div>

1. 
2. [![Alt text](image-url)](image-url)
3. 
...
30. 

</div>

Script

<script>
/* Photo calendar lightbox | robertbirming.com */
(() => {
  const links = [...document.querySelectorAll(".photo-cal li a")].filter((a) => a.querySelector("img"));
  if (!links.length || !window.HTMLDialogElement) return;

  const dialog = document.createElement("dialog");
  dialog.className = "photo-cal-lightbox";
  dialog.setAttribute("aria-label", "Photo");
  const photo = document.createElement("img");
  dialog.append(photo);
  document.body.append(dialog);

  links.forEach((link) => {
    link.addEventListener("click", (event) => {
      // Let modified clicks open the photo in a new tab as usual
      if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
      event.preventDefault();
      const thumb = link.querySelector("img");
      photo.src = link.href;
      photo.alt = thumb.alt;
      dialog.showModal();
    });
  });

  // Click anywhere to close. Escape closes too, built into dialogs.
  dialog.addEventListener("click", () => dialog.close());
  dialog.addEventListener("close", () => photo.removeAttribute("src"));
})();
</script>

Styles

/* Photo calendar | robertbirming.com */
.photo-cal-nav {
  margin-block-end: 0.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  font-family: ui-monospace, monospace;
  color: color-mix(in srgb, var(--text-color), transparent 40%);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.photo-cal-nav p {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  margin: 0;
}

.photo-cal-nav a {
  color: color-mix(in srgb, var(--text-color), transparent 40%);
  text-decoration: none;
}

@media (hover: hover) {
  .photo-cal-nav a:hover {
    color: var(--link-color);
  }
}

.photo-cal-nav span:first-child:not(:has(a)),
.photo-cal-nav span:last-child:not(:has(a)) {
  opacity: 0.4;
}

.photo-cal-head {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  border: 1px solid color-mix(in srgb, var(--text-color), transparent 85%);
  border-radius: 6px 6px 0 0;
  overflow: hidden;
}

.photo-cal-head span {
  padding-block: 0.5em;
  font-size: 0.65rem;
  font-weight: 700;
  font-family: ui-monospace, monospace;
  color: color-mix(in srgb, var(--text-color), transparent 40%);
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background-color: var(--code-background-color);
  border-inline-end: 1px solid color-mix(in srgb, var(--text-color), transparent 85%);
}

.photo-cal-head span:last-child {
  border-inline-end: none;
}

.photo-cal ol {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  padding: 0;
  list-style: none;
  counter-reset: day;
  border: 1px solid color-mix(in srgb, var(--text-color), transparent 85%);
  border-block-start: none;
  border-radius: 0 0 6px 6px;
  overflow: hidden;
}

.photo-cal li {
  position: relative;
  aspect-ratio: 1;
  margin: 0;
  background-color: var(--background-color);
  border-inline-start: 1px solid color-mix(in srgb, var(--text-color), transparent 85%);
  border-block-end: 1px solid color-mix(in srgb, var(--text-color), transparent 85%);
  overflow: hidden;
  counter-increment: day;
}

@media (prefers-color-scheme: dark) {
  .photo-cal li {
    background-color: color-mix(in srgb, var(--link-color) 12%, var(--background-color));
  }
}

.photo-cal li:last-child {
  border-inline-end: 1px solid color-mix(in srgb, var(--text-color), transparent 85%);
}

.photo-cal li:nth-last-child(-n+7):nth-child(7n+1),
.photo-cal li:nth-last-child(-n+7) ~ li,
.photo-cal li:last-child {
  border-block-end: none;
}

.photo-cal li:first-child {
  grid-column-start: var(--start, 1);
}

.photo-cal li::after {
  content: counter(day);
  position: absolute;
  inset-block-start: 0.5em;
  inset-inline-start: 0.5em;
  font-size: 0.6rem;
  font-weight: 700;
  font-family: ui-monospace, monospace;
  color: color-mix(in srgb, var(--text-color), transparent 40%);
  line-height: 1;
  z-index: 1;
  user-select: none;
}

.photo-cal li:has(> p)::after {
  padding: 0.15em 0.3em;
  color: #fff;
  background: rgb(0 0 0 / 0.35);
  border-radius: 2px;
}

.photo-cal li p {
  margin: 0;
  line-height: 0;
}

.photo-cal li a {
  text-decoration: none;
}

.photo-cal li img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  margin: 0;
  border-radius: 0;
  filter: sepia(0.8) hue-rotate(-10deg) saturate(1.4) brightness(0.85) contrast(1.1);
  transition: transform 0.3s ease, filter 0.4s ease;
}

@media (prefers-color-scheme: dark) {
  .photo-cal li img {
    filter: sepia(0.8) hue-rotate(180deg) saturate(1.2) brightness(0.7) contrast(1.15);
  }
}

@media (hover: hover) {
  .photo-cal li:has(img):hover img {
    transform: scale(1.05);
    filter: saturate(1) brightness(1) contrast(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .photo-cal li img {
    transition: none;
  }
}

/* Lightbox */
.photo-cal-lightbox {
  max-width: 90vw;
  max-height: 90vh;
  padding: 0;
  background: none;
  border: 0;
  cursor: zoom-out;
}

.photo-cal-lightbox::backdrop {
  background: rgb(0 0 0 / 0.85);
}

.photo-cal-lightbox img {
  display: block;
  max-width: 90vw;
  max-height: 90vh;
  margin: 0;
  object-fit: contain;
  border-radius: 4px;
  filter: none;
}

Happy blogging, one day at a time.

Want more? Check out the full Bear Blog library.

  1. The lightbox requires JavaScript, available on Bear Blog's premium plan.↩