Skip to content

v4: prefix BlazorBlueprint's Tailwind utilities to stop cross-build class collisions #501

Description

@mathewtaylor

Plan: docs/plans/2026-08-28-v4-tailwind-prefix.md in the internal devkit repo (not this one).

Fixes #496. Breaking — targets a v4 major.

The fault

BB ships a prebuilt Tailwind stylesheet. A consumer running their own Tailwind build ends up with two stylesheets writing into the same utilities cascade layer. Tailwind gives md:grid-cols-4 priority over sm:grid-cols-2 by sort order within a single build, not by specificity — both are one class. Across two builds that ordering is gone, and priority at the join is decided by which <link> came second.

Both directions break, which is why no ordering fixes it:

  • BB second — a consumer's sm:grid-cols-2 md:grid-cols-4 collapses, because BB re-emits sm:grid-cols-2 later in the document.
  • BB first — BB's own hidden sm:flex pagination collapses, because the consumer emits hidden later.

This reproduces in our own demo today: demos/BlazorBlueprint.Demo.Server/App.razor:9-11 loads app.css then blazorblueprint.css.

The fix

Prefix BB's utilities so the two builds cannot produce the same class name, and give them their own cascade layer. Verified against tailwindcss v4.3.0:

@layer bb-theme, bb-base, bb-components, bb-utilities, utilities, bb;
@import 'tailwindcss/theme.css'     layer(bb-theme)     prefix(bb);
@import 'tailwindcss/preflight.css' layer(bb-base);
@import 'tailwindcss/utilities.css' layer(bb-utilities) prefix(bb);

Emits .bb\:hidden, .bb\:sm\:flex. Consumer Class="..." values stay unprefixed and are unaffected.

Scope

  • 966 class="…" strings in Components .razor, 17 in Primitives, 119 .cs files building class strings.
  • The rewrite is mechanical: the current build emits 933 distinct utility tokens, and that set is the exact allowlist. Verification is a diff of the built CSS — every utility selector gains bb\: and nothing else changes.
  • ClassNames.cn() needs a decision: consumer overrides currently win by string merge, and TailwindMerge keeps both when only one side is prefixed. See step 4 of the plan.
  • The demo's @source on library sources must go; the demo should be a real consumer.

Ordering

#490 must land first. It touches 299 files and would conflict badly with a mechanical class rewrite.

Also worth batching into the same major: #428.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions