Ask
88
@burr_bennet ·

tailwind v4 @theme colors work in apps/web but bg-brand-500 does nothing inside packages/ui Design Tokens

pnpm workspace, apps/web and packages/ui. The @theme block with my brand colours lives in apps/web/src/app.css and everything in the app renders correctly.

A Button in packages/ui/src/Button.tsx uses bg-brand-500. The class is in the DOM. There is no matching rule anywhere in the emitted stylesheet. Nothing errors, it just renders transparent.

packages/ui has its own ui.css with @import "tailwindcss" in it, which I inherited and have not thought about. Diagram of what I think is happening attached, tell me which half is wrong.

9 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @pattern_tracer · 5d ago · 3 replies

    Both halves are right and they are two separate bugs stacked on each other, which is why it is confusing.

    First: v4 finds source files automatically starting from the project root of the stylesheet, and it skips node_modules. In a pnpm workspace your packages/ui is reachable as a symlink inside node_modules, so the scanner never opens Button.tsx and never learns that bg-brand-500 is wanted. Add an explicit source to apps/web/src/app.css:

    @import "tailwindcss";
    @source "../../../packages/ui/src";
    

    That path is relative to the CSS file, not the package root and not the repo root. Get that wrong and it fails silently, which is the second afternoon people lose to this.

    Second: delete the @import "tailwindcss" from packages/ui/ui.css. Right now that produces a completely separate stylesheet that has never seen your @theme, so even when it does emit something, the token is not defined. Libraries inside a monorepo should ship class names and let the app compile. One compile, one token set, one output.

    134
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @lowell_frame · 2d ago

      The symlink part is the bit that costs people the afternoon. ls -l node_modules/@acme and you will see it pointing straight back at ../../packages/ui, and as far as the scanner is concerned that is node_modules and therefore not interesting.

      41
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @notetaking_ivo · 2d ago

      One nuance if that package is also published to npm and consumed by apps outside this repo: those consumers cannot scan your source, so ship a prebuilt stylesheet for them and keep the source-scan path for the monorepo. Two consumers, two strategies, and it is fine to have both.

      16
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @serger_seth · 4d ago

    Once the scanning is fixed, check whether you want @theme or @theme inline. If any of your token values reference another custom property, the non-inline form keeps the reference rather than the resolved value, and it then resolves in whatever context the utility is used in. That is what you want for theming and extremely not what you want if the referenced variable only exists in one place.

    Symptom is a colour that works on one page and is transparent on another, which sounds close enough to your bug to be worth ruling out.

    57
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @pumptest_pat · 3d ago · 2 replies

    You need to add the package path to the content array in tailwind.config.js. That is what it is for.

    21
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @pattern_tracer · 2d ago

      There is no content array in v4 unless you are deliberately loading a legacy JS config with @config. Configuration moved into CSS and the direct equivalent of content is @source. If they still have a tailwind.config.js sitting in the repo that nothing imports, that is itself worth deleting before it confuses the next person.

      18
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @burr_and_edge · 4d ago · 2 replies

    Sanity check before you change anything: grep the built CSS for brand-500.

    Absent means it is a scanning problem. Present but not applying means it is a cascade problem. Those two get diagnosed as each other constantly and the fixes have nothing in common.

    33
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @rawfileruth · 3d ago

      Layers specifically. v4 puts utilities in a layer, and any plain CSS you wrote outside of a layer beats everything inside one regardless of specificity. If someone dropped a global button { background: transparent } in a bare stylesheet, that wins and no amount of ! energy will feel proportionate.

      14
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @thea_mandel · 4d ago

    After you add the @source line, restart the dev server. The CSS plugin does not always notice a new glob on a hot reload and you will spend ten minutes convinced the path is wrong when it was correct the first time.

    12
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report