Skip to content

fix: classes that compiled to nothing, SSR column mismatches, and a CI guard - #6850

Open
tsahimatsliah wants to merge 3 commits into
mainfrom
dead-classes-hydration-dailydotdev
Open

tsahimatsliah wants to merge 3 commits into
mainfrom
dead-classes-hydration-dailydotdev

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Why

The phone shell (#6767) broke desktop feed settings by splitting w-74 into tablet:w-74 w-full. w-74 had never existed in our Tailwind config, so a no-op became a desktop regression (#6840 fixed it). A sweep of the files the shell PRs touched found more classes that compile to nothing, two SSR mismatches, and no guard. This PR fixes what the sweep found and adds the guard.

Changes

Alpha modifiers work on tokens

  • bg-accent-avocado-default/40 and friends compiled to nothing because the token colours were bare var(--theme-…) strings. withAlpha wraps the accent, text and status tokens with Tailwind's <alpha-value> placeholder, so /NN works (for an alpha on the opacity scale) and /[0.35] works for the rest. The hot takes sparks and blobs and the composer's spam warning keep their authored alphas and render for the first time. The plain utilities compile to the same colour as before.

Dead classes, now real or removed

  • Spotlight: animate-spotlight-scrim-in, animate-spotlight-panel-in and animate-spotlight-list-fade were never defined (since feat: spotlight feature #6011). All three fade opacity only, so the both fill leaves no transform on the scrolling list or the panel.
  • opacity-70 → opacity-64 (the scale has no 70), z-10 → z-1, z-20 → z-[20] (keeps its place under the z-[21] overlay).
  • Three classes that never applied are deleted rather than replaced, since the shipped look is the one without them: the post analytics button's hover:bg-action-blueCheese-hober (its pressed Secondary fill is the design), the feedback caption's opacity-80 (contrast), and the squad brand button's hover:opacity-90.
  • shadow-1, line-clamp-10, scale-60 and scale-80 are added to the config, so the three places that already use them render as written.

Hydration

  • MainFeedLayout's --num-cards and the tag page's follow grid rendered the client's column count on the hydration pass while the server rendered one column (dev warned on /search at tablet width and on every tag page). In production React keeps a mismatched attribute, so the layout stayed one column until something re-rendered the component. useIsHydrated reads React's own hydration state through useSyncExternalStore: the server value on every hydration render, including a boundary hydrated after the first effects, and the real one on every later mount, so client-side navigation does not flash. MainLayoutHeader drops its local hasHydrated state for it.

CI

  • scripts/check-dead-classes.mjs extracts the class tokens from the string literals a branch touches (whole files, kept to literals that reach an added line, with interpolated classes skipped), compiles them through packages/webapp/tailwind.config.ts (utilities and components, plus every hand-written stylesheet), and fails on tokens with no selector. npm run check:dead-classes; --all <files> audits whole files. It runs as dead_classes_changed next to the strict typecheck.

Verified

  • check:dead-classes --all on every touched file plus the spotlight and feed settings files: all 307 classes compile. In branch mode: all classes added by this branch compile.
  • Lint clean in shared and webapp; webapp typecheck clean. Shared typecheck has one pre-existing failure in src/lib/transaction.spec.tsx that this branch does not touch.
  • Shared suites for layout, spotlight, hot takes, feedback, squad header and the Tailwind plugins pass, as do the webapp page tests for tags, search, sources and my feed.

Events

No new event names.

Experiment

None.

🤖 Generated with Claude Code

Preview domain

https://dead-classes-hydration-dailydotd.preview.app.daily.dev

…I guard

The phone shell (#6767) broke desktop feed settings by splitting `w-74`
into `tablet:w-74 w-full`: `w-74` had never existed, so the no-op turned
into a desktop regression (#6840 fixed it). This sweeps the rest of the
classes that compile to nothing and guards against new ones.

Dead classes, now real:
- Spotlight: `animate-spotlight-scrim-in`, `-panel-in` and `-list-fade`
  were never defined; keyframes and animations added.
- `bg-<token>/NN` alpha modifiers cannot apply to CSS-variable colours
  (hot takes sparks and blobs, composer spam warning): `color-mix`.
- `opacity-70/80` -> `opacity-64`, `z-10` -> `z-1`, `z-20` -> `z-[20]`,
  `hover:opacity-90` on the brand button -> `hover:brightness-90`,
  `hover:bg-action-blueCheese-hober` -> `hover:bg-accent-blueCheese-flat`.
- `shadow-1`, `line-clamp-10`, `scale-60`, `scale-80` added to the config
  so the three places that already use them render.

Hydration:
- `MainFeedLayout`'s `--num-cards` and the tag page's follow grid rendered
  the client's column count on the hydration pass while the server
  rendered one column. In production React keeps a mismatched attribute,
  so the layout stayed one column until a later re-render. `useIsHydrated`
  renders the server value on that pass and the real one after mount;
  later mounts start hydrated, so client navigation does not flash.

CI:
- `scripts/check-dead-classes.mjs` compiles every class added by a branch
  through the real Tailwind config and fails on tokens with no selector.
  Runs as `dead_classes_changed` next to the strict typecheck.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
daily-webapp Ready Ready Preview Oct 9, 2026 11:59pm UTC

Request Review

- Alpha modifiers work on the accent, text and status tokens
  (`withAlpha` wraps each variable with Tailwind's `<alpha-value>`), so
  the hot takes and composer classes are written as `bg-x/40` or, for an
  alpha outside the opacity scale, `bg-x/[0.35]`, not color-mix literals.
- The three classes that never applied are deleted rather than replaced:
  the analytics button's hover (its Secondary pressed fill was the shipped
  look), the feedback caption's opacity (contrast), and the brand button's
  hover.
- `useIsHydrated` reads React's own hydration state through
  `useSyncExternalStore`, so a boundary hydrated after the first effects
  still renders the server value; `MainLayoutHeader` uses it too.
- The spotlight animations fade opacity only, so `both` leaves no transform
  on the scrolling list or the panel.
- The checker skips tokens that touch an interpolation, scans whole files
  and keeps the literals that reach an added line, and follows the repo's
  Prettier config.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@tsahimatsliah tsahimatsliah left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff, compiled the Tailwind config on main and on this branch, and read the failing CI logs.

Blocking

  1. CI is red. lint_shared and the lint step in test_webapp fail with tailwindcss/classnames-order in files this PR doesn't touch: PostFocusCard.tsx:794, SidebarDesktopV2.tsx:2544, Spotlight.tsx:1016 and :1103, and DevCardStep2.tsx:249. The cause is that registering shadow-1, scale-60/80 and animate-spotlight-* as real utilities changes where the sorter puts them. Running eslint --fix on those five files should fix it.
  2. caret-text-link breaks on every text field (inline comment on withAlpha.ts).

Non-blocking

  • The SSR column fix covers two consumers, but FeedContainer writes the same --num-cards without the gate (inline).
  • 6 existing /10, /20 and /30 usages still compile to nothing, and the diff-only guard won't flag them (inline).
  • eslint-plugin-tailwindcss is already installed, and its no-custom-classname rule covers what the new script does (inline).
  • Classes that never applied before now render, so several surfaces change visually. Before/after screenshots are needed (inline).
  • Scope: this PR combines a change to every text-*, accent-* and status-* utility (each now compiles to color-mix), a new hydration hook, a new CI tool and several visual changes. Splitting the colour-token change and the CI guard into their own PRs would make each one cheaper to review and to revert.

Reviewed by AI.

if (!variable) {
return value;
}
return `color-mix(in srgb, var(${variable}) calc(<alpha-value> * 100%), transparent)`;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this breaks caret-text-link, the caret colour on the shared text inputs (fields/common.tsx, fields/Textarea.tsx).

The custom caret plugin (packages/shared/tailwind/caret.ts) reads theme('colors') and writes each value straight into caret-color, so it never replaces <alpha-value>. Compiling caret-text-link on this branch gives:

.caret-text-link {
  caret-color: color-mix(in srgb, var(--theme-text-link) calc(1 * 100%), transparent);
  caret-color: color-mix(in srgb, var(--theme-text-link) calc(<alpha-value> * 100%), transparent)
}

On main it compiles to caret-color: var(--theme-text-link). The plugin's declaration comes last, so it wins. Because it contains var(), the browser accepts it when parsing and only rejects it at computed-value time. The caret then falls back to auto (the text colour), and nothing reports an error.

Suggested fix: drop the custom caret plugin, since core Tailwind's caretColor already generates the same caret-* classes from theme.colors. Alternatively, have the plugin replace <alpha-value> with 1. Also check for other plugins or JS that read these colour objects directly and would pass the placeholder through.

Reviewed by AI.

const isHydrated = useIsHydrated();
const feedWidthStyle = {
'--num-cards': feedSpacinessCards.eco,
'--num-cards': isHydrated ? feedSpacinessCards.eco : ssrNumCards.eco,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the same mismatch is still there in FeedContainer.

packages/shared/src/components/feeds/FeedContainer.tsx:175 writes '--num-cards' from currentSettings.numCards.eco with no hydration gate. useFeedHeroAd, Feed and useFeed also read numCards during the first render. All of them get the client's useMedia value on the hydration pass. If the main feed container is server-rendered, its grid has the problem this PR describes.

Rather than patching each consumer, could the gate go in FeedLayoutProvider? It owns numCards, so it could serve ssrNumCards while !useIsHydrated() and fix every consumer at once. ssrNumCards and the per-call-site gates would no longer be needed.

Reviewed by AI.

};

export default statusColors;
export default withAlpha(statusColors);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: 6 more alpha modifiers still compile to nothing.

The opacity scale has no 10, 20 or 30, so these existing classes are still dead after this change (checked by compiling on this branch):

  • bg-status-success/10: profile/SocialLinksInput.tsx, organizations/components/LinksInput.tsx
  • bg-status-warning/10: webapp/pages/recruiter/[opportunityId]/plans.tsx
  • bg-accent-cabbage-default/10: shortcuts/components/AddShortcutTile.tsx
  • ring-accent-cabbage-default/30: shortcuts/components/ShortcutEditForm.tsx
  • bg-accent-onion-default/20: brand/HighlightedWord.tsx

The new guard only checks lines a branch adds, so it won't catch these. Either convert them to /[0.1]-style values here, as the hot takes classes were, or list them as a follow-up. The description currently says alpha modifiers on tokens work.

Reviewed by AI.

@@ -0,0 +1,216 @@
#!/usr/bin/env node
// Fails on Tailwind classes that compile to nothing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking question: why a custom script instead of the lint plugin we already have?

eslint-plugin-tailwindcss is already a dependency and configured in packages/eslint-config (callees: classnames/clsx/ctl/classNames). Its tailwindcss/no-custom-classname rule flags classes the Tailwind config doesn't generate. It runs in the editor and in the existing lint jobs, and accepts a whitelist for the hand-written stylesheet classes (shell-*, no-scrollbar, …).

This script adds 216 lines of regex heuristics (UTILITY, AMBIGUOUS, VARIANT prefix lists that must be kept in sync by hand), a separate CircleCI job, and a diff-only mode that can't see existing dead classes. It also counts every class in every *.module.css as globally defined.

Could we enable no-custom-classname (warn, or error with a whitelist) instead? If the plugin can't handle something here, it would help to note the reason in the PR description.

Reviewed by AI.

);

const baseShadows = {
1: (color) => `0 0.125rem 0.25rem -0.0625rem ${color}66`,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: please add before/after screenshots (desktop and mobile). These classes never applied before, so making them real changes what users see. Checks so far confirm the classes compile, not how they look:

  • shadow-1 (a new value, not taken from a design token) now appears on PostFocusCard and SidebarDesktopV2.
  • scale-60 and scale-80 now apply in DevCardStep2 at mobileL and mobileXXL.
  • In the hot takes onboarding card, the aura blobs and sparks were invisible because their background classes were dead. They now render in colour, and the wrapper drops to opacity-64.
  • The spotlight scrim, panel and list now animate (gated on motion-safe).
  • The comment thread toggle and the GIF star button now get a real z-index.

Some of these probably need design sign-off rather than "render as written", since the design that shipped is the one without them.

Reviewed by AI.

This branch was successfully deployed

1 active deployment
Preview — d98d0ae3 Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant