Studio Journal
A deep dive into frontend engineering, minimal design, and the technical craft behind premium digital experiences.
I write my date pickers by hand — no react-day-picker, no date-fns, no dayjs. Radix supplies popover positioning and focus management; the 42-cell month, the week offset and the month arithmetic are about forty lines of pure functions. That held fine for one grid. The moment I rendered two months side by side, the arrow keys started teleporting. Each grid draws six full weeks, so January's trailing cells and February's leading cells are the same fourteen days, and data-date existed twice in the DOM. querySelector returns the first match, so focus kept landing in the wrong grid. The fix is to render the adjacent-month days as inert blanks — but only when a second grid is on screen.
Next renders <link rel="alternate" hrefLang> through React DOM, which writes the prop name verbatim, in camelCase — so grep hreflang on the served HTML returns zero and you conclude the tags were never emitted. They were. The two failures that actually cost indexation are quieter: canonicalizing every locale to /en, which discards the cluster, and a route-level alternates object, which replaces the layout's rather than merging into it. All three are catchable with one pass over the build output, which is the only artifact that tells the truth.
pnpm 11 finished moving its supply-chain settings into pnpm-workspace.yaml — that is now the only place they are read from — and three of them accept a shape that looks right and does nothing. allowBuilds is a map, not a list — write it as a YAML array and pnpm turns your entry into a package literally named '0'. trustPolicyIgnoreAfter counts minutes, so 365 means six hours, not a year, and quietly turns trustPolicy: no-downgrade into a no-op. And pnpm 11 no longer reads the pnpm field of package.json at all — it warns once and exits 0. None of the four fixes a CVE in a version you pinned. That is a different problem with a different fix.
A script rmSync'd a source file I had never committed — no git blob, no recycle bin, no editor backup. Most .js.map files Next.js writes into .next/ carry a sourcesContent array holding the original, pre-transform text of each module they compiled, so a project that has been built once since the file existed still has that file on disk, inside the build output. I got 660 lines back byte-for-byte. The lesson underneath is the one that comes first: back up every file a destructive script can touch, not just the ones git can restore.
shadcn 4.x generates components on top of Base UI instead of Radix. asChild is gone, and Accordion no longer accepts type or collapsible — both are TS2322, so tsc hands them to you in the first run. The one that survives a green build is Select: Base UI's <Select.Value> renders the value, not the selected item's children, unless you pass an items map to <Select.Root>. No error, no warning, and the open popup still looks correct — only the closed trigger reads buy where it should read Buy a template.
next/font/google downloads woff2 files from fonts.gstatic.com at build time. When Google rotated a file hash, my deploy started failing with 404s I could not reproduce locally, because the same fetch had already succeeded on my machine in an earlier second. Fix: vendor the fonts and use next/font/local, so the bytes are an input you own.
For filter and backdrop-filter, Lightning CSS merges a prefixed and unprefixed declaration into one, keeping the prefix flags of whichever you wrote last. So backdrop-filter followed by -webkit-backdrop-filter compiles to WebKit-only. Chrome doesn't support -webkit-backdrop-filter at all, so the blur silently disappears in production while the source looks correct. Most other prefixed properties survive the same treatment unharmed — which is exactly what makes this one easy to miss.
In Tailwind v4, --z-overlay: 200 inside @theme does not create a z-overlay utility. Utilities read from namespaces, and the one for z- is --z-index-. Nothing errors and nothing warns. The class simply does not exist, the element falls back to z-index: auto, and anything with a real positive z-index paints over it — on some routes, not others. The check that actually settles it is grepping the built CSS for the class name, not the source.
If your first render depends on document, matchMedia, location.hash, localStorage or a cookie, you are one differing value away from a hydration mismatch — those APIs are absent during SSR and already real on the client's first render. tsc, ESLint and a Node-environment unit suite all pass anyway, because none of them ever perform a hydration render. Two honest fixes: seed after mount (simple, one frame of flash), or read the value server-side (no flash, needs a cookie round-trip).
params is a Promise, metadata merges per field and not deeply, PageProps/LayoutProps only exist once something generates them, and a pre-paint theme script has to be server-rendered. Three of the four fail silently: green build, green types, wrong HTML. Everything below is from five Next.js 16.2.10 / React 19.2.7 codebases I maintain.
Newsletter
An email when we ship a new template or write something worth your time. No noise.