Skip to content

Merge classes like tailwind-merge and add a cva-style variants helper - #2505

Merged
pournasserian merged 2 commits into
v2from
2434-tailwind-merge
Oct 18, 2026
Merged

pournasserian merged 2 commits into
v2from
2434-tailwind-merge

Conversation

@pournasserian

Copy link
Copy Markdown
Member

Refs #2434

What

Class merging and variants, so a shadcn example translates into Razor with the same class names and behaves the same when a caller passes its own classes.

  • TailwindMerge: a C# port of tailwind-merge (MIT) for the Tailwind v4 utilities. When two classes set the same CSS property under the same variants, the later one wins and the earlier one is dropped. It keeps tailwind-merge's algorithm:
    • Variants are compared regardless of order, except arbitrary variants and pseudo-elements, whose order matters.
    • Important classes (!p-4 and p-4!) and arbitrary properties ([mask-type:luminance]) have their own scope.
    • A postfix such as /50 is read as an opacity, and text-lg/7 also replaces leading-*.
    • Shorthands replace what they cover: p replaces px and pe, inset-x replaces start and end, rounded replaces rounded-ss, size replaces w and h, and so on.
    • Prefixes shared by several properties are told apart by their value: text-sm, text-start and text-muted-foreground; border-2, border-dashed and border-input; ring-[3px] and ring-ring/50; shadow-xs and shadow-black/5; bg-cover, bg-none and bg-red-500.
    • A class it does not recognise is always kept, and spacing and sizing only take their own values, so a custom class such as my-card is never read as a margin.
    • Results are cached.
  • ClassNames.Cn now merges as well as joins, as shadcn's cn() does. Every component already puts its Class parameter last, so a caller's class overrides the component's own: <Input Class="h-8" /> drops h-9.
  • Variants: shadcn's cva(), with the base classes, the classes for each value of each variant, and the defaults. Classes(chosen, className) merges them with the caller's classes last. An unknown value adds nothing and a missing one takes the default, as in cva. ButtonVariants and Badge now use it, so the size of a button replaces its base gap-2, as it does in shadcn.
  • ButtonTests: Button had no bUnit tests. Each component in Components/Ui now has its own.
  • CONTRIBUTING.md: a paragraph on Cn and Variants.

Boxes on #2434

To tick when this merges:

  • "A C# port of tailwind-merge for class merging and a small variants helper in the CVA style, so shadcn examples translate one-to-one into Razor."
  • "bUnit tests per component for rendering and keyboard behaviour, in tests/FluentCMS.Tests": with ButtonTests, every component in the set has its own test class.

Why

  • Ported, not wrapped. tailwind-merge is JavaScript, and the admin runs no Node (ADR-0011). The algorithm is small and its class map is data, so a port in C# is a single file with no dependency.
  • The groups the admin uses, with a safe fallback. tailwind-merge's full map follows every Tailwind utility and plugin. This port covers the core utilities, which include everything shadcn's classes and the admin's components use. An unknown class is kept, so a missing group means two classes stay side by side, as they did before this pull request, never that a class disappears. A test-only scan of every class string in Components found nothing that the merge drops.
  • Logical utilities conflict as Tailwind v4 compiles them. px and inset-x are padding-inline and inset-inline in v4, so they also replace ps, pe, start and end. tailwind-merge still pairs them with the physical sides only.
  • No compound variants. None of the components needs them yet, and adding them later does not change the API.

No new dependency.

Tests

  • TailwindMergeTests: cases from tailwind-merge's documentation and test suite, rewritten for v4. They cover layout and spacing shorthands, logical utilities, the text, background, border, ring, outline and shadow families, variants and their order, important, arbitrary values, variables, negative values, postfixes, unknown classes and whitespace. One case is a shadcn button with a Class override.
  • VariantsTests: defaults, a chosen value replacing the base classes it conflicts with, null and unknown values, and the class parameter winning.
  • ButtonTests: native button, each variant and size, a Class override, a click and the disabled state, and form attributes passed through.
  • The existing component tests pass unchanged with merging on.

dotnet build -warnaserror, the full suite and markdownlint pass locally.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 18, 2026
@pournasserian pournasserian added v2 FluentCMS v2 (v2 branch) type:chore Repository, tooling or housekeeping area:infra Persistence, host, CI and build labels Oct 18, 2026
@pournasserian
pournasserian merged commit efbbe7b into v2 Oct 18, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2434-tailwind-merge branch October 18, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:infra Persistence, host, CI and build type:chore Repository, tooling or housekeeping v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant