Content Security Policy
The calendar sets geometry through inline style attributes. Two ways to run it under a strict style-src, both verified in a test.
The hooks place every event, lane and label with CSS custom properties in inline style
attributes (--event-top: 37.5%, --calendar-columns: …), so that positions need no measuring
and no per-instance CSS. A Content-Security-Policy whose style-src has no 'unsafe-inline'
blocks style attributes in server-rendered markup, and hydration does not apply them again: the
grid collapses. The calendar's own code inserts no <style> elements and no inline scripts, so
style attributes are the part of a policy it concerns, apart from one image in its theme
(below).
There are two ways to run it under a strict policy. Both are checked on every change by an end-to-end test against pages served with these policies (a strict page and one with style attributes), each an editable calendar: no violation is reported, every event sits at its time, and an event's "More actions" button shows its icon and opens its menu.
Option 1: allow style attributes
Keep style-src strict for stylesheets and <style> elements, and allow inline style
attributes only:
style-src 'self'; style-src-attr 'unsafe-inline'Server rendering works unchanged. Style attributes cannot run script or load anything that
img-src and font-src do not allow, which is why many strict policies allow them; with this
option your style-src stays nonce- or hash-free.
Option 2: render the calendar in the browser
Without any inline-style allowance, render the calendar on the client only. React then sets every
style through the CSSOM (element.style.setProperty), which style-src does not govern:
'use client'
import type { CalendarEventInput } from '@hapio/calendar-react'
import { useSyncExternalStore, type ReactNode } from 'react'
import { HapioCalendar } from '@/components/hapio-calendar/hapio-calendar'
function subscribe(): () => void {
return () => undefined
}
/** `children` in the browser only: the server render and hydration show `fallback`. */
function ClientOnly({ children, fallback }: { children: ReactNode; fallback: ReactNode }) {
const client = useSyncExternalStore(
subscribe,
() => true,
() => false,
)
return client ? children : fallback
}
export function StrictCspCalendar({ events }: { events: readonly CalendarEventInput[] }) {
return (
<ClientOnly fallback={<div className="h-[36rem] rounded-xl border" />}>
<HapioCalendar events={events} timeZone="Europe/Stockholm" className="h-[36rem]" />
</ClientOnly>
)
}The server renders the fallback (give it the calendar's height), and the calendar appears after
hydration. next/dynamic with ssr: false in a client component does the same.
What does not work
-
ssrEvents={false}alone. It leaves the event layers out of the server render, but the grids' own layout uses style attributes too, all of them in server-rendered markup:Element Inline styles the time grid's scroller overflow,scrollbar-gutter,--calendar-columns,--calendar-slotscolumn headers --calendar-spanthe all-day row, week rows --calendar-levels,positionslot labels --calendar-slot,--calendar-slot-toplanes positionthe now indicator --calendar-now-topbackground events their geometry and color Most of these values depend on the data (the number of days, slots and rows), so they cannot be classes.
-
Nonces and hashes apply to
<style>and<script>elements, not to style attributes;'unsafe-hashes'would need a hash per attribute value, which changes with every event.
The theme's icon
calendar-theme draws the "More actions" button's "⋯" with a CSS mask whose image is a data:
SVG, and img-src governs CSS images. Keep data: in img-src (img-src 'self' data:, as the
test pages do), or the buttons show no icon.
Your framework's part
The rest of a policy is your app's, and none of it changes with the calendar:
- Next.js inlines its own scripts, so a static page needs
script-src 'unsafe-inline', or a nonce from a proxy with dynamic rendering. - Your shadcn components are yours. The calendar uses your
Popover,DropdownMenuandDialog; Radix's modal dialog (the "Change time…" form and the scope dialog) inserts a<style>element to lock the page's scrolling while it is open, which needs a nonce (Radix reads it through theget-noncepackage) or astyle-srcthat allows it. - Theme switchers such as next-themes insert a
<style>element while they switch themes; allow it by its hash, as the test pages do.