◂ writeups // michaelz.dev

The Calendar Saga: a war story against FullCalendar

Some homelab tasks are a five-minute config edit. Others are a multi-hour campaign against a JavaScript library that does not want what you want. This is one of the second kind, and I'm writing it down so the next person — probably future me — doesn't have to fight it again.

The deceptively simple goal

I wanted a calendar on my Home Assistant dashboard. Not a list of upcoming events — an actual month grid, Google-style, where a holiday or a week-long trip shows up as a bar that spans the days it covers. That's it. That's the whole ask.

Home Assistant has a built-in calendar card. It uses FullCalendar under the hood, which absolutely can draw spanning event bars. So this should be easy.

It was not easy.

Round 1: the custom card

My first instinct was a popular community card, atomic-calendar-revive. It's beautiful and configurable — but it renders events as a per-day list, not as bars that stretch across cells. A three-day trip showed up as the same event printed three times. Not the look I was after.

Round 2: the native card, and the word "+more"

So back to the native card with dayGridMonth. It draws real spanning bars — victory! — except the cells were too short. The moment a day had more than one or two events, FullCalendar collapsed the rest into a dreaded "+more" link, and the whole card was a cramped little box that clipped half the month.

I needed the cells taller. Easy, right? Set a height. So I tried setting the height on the card. The weeks clipped. I tried setting it on the calendar container. It overflowed and bled into the card below it. I tried every height property I could find on every element I could reach, and each attempt broke it in a new and creative way.

I was sizing the wrong thing. I kept reaching for the card, when the thing that actually controls the layout lives much deeper.

The shadow DOM problem

Here's the wrinkle. Home Assistant cards live inside the shadow DOM — an encapsulated little world that your normal CSS can't reach. FullCalendar's internal grid is buried inside ha-full-calendar's shadow root. You can't style it from outside. Not with a normal stylesheet, anyway.

The tool for the job is card-mod, which can pierce into a card's shadow DOM and inject styles. With that, I could finally talk to FullCalendar's actual grid elements instead of yelling at the card from the outside.

The one line that won

The breakthrough was realizing which element to size. Not the card. Not the calendar. The day cell itself — FullCalendar's .fc-daygrid-day-frame. Give the day frame a minimum height, and the whole calendar grows to fit the month naturally, no clipping, no bleed:

ha-full-calendar $: |
  .fc-daygrid-day-frame { min-height: 86px !important; }

That was it. That was the war. Eighty-six pixels of minimum height on the right element, injected through a shadow boundary, and suddenly the month breathed. I recoloured the event bars in my theme's mauve, tinted today's cell, and trimmed the calendar down to my three personal calendars (dropping the holiday and birthday feeds that were crowding the cells and triggering "+more" in the first place).

The takeaway

The lesson generalizes beyond calendars: when a layout won't cooperate, make sure you're sizing the element that actually owns the layout — not the convenient wrapper you happen to have a handle on. With grid-based components, that's almost always the cell, not the container.

It's a small thing. It took hours. And every time I open my dashboard now and see the month laid out cleanly with a holiday stretched across its days, I feel a completely disproportionate amount of pride about min-height: 86px.

Comments

◂ all writeups michaelz.dev ▸