Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conflicting className precedence rules #1010

Closed
jasonlimantoro opened this issue Jul 7, 2019 · 14 comments
Closed

Conflicting className precedence rules #1010

jasonlimantoro opened this issue Jul 7, 2019 · 14 comments

Comments

@jasonlimantoro
Copy link

Given

<button class="rounded-lg p-4 inline-block p-0 default ripple">No?</button>

which has a conflicting className: p-4 and p-0

Expected

The button should have a padding of 0, following the CSS rules, i.e. the one that's defined later takes precedence.

Actual

The button has a padding of 4 (p-4 is taking effect).

Side notes

Interestingly, if I swap the class name position from Chrome devtools, i.e.

<button class="rounded-lg p-0 inline-block p-4 default ripple">No?</button>

The button still has a padding of 4.

Is this the expected behavior?

@tlgreg
Copy link

tlgreg commented Jul 7, 2019

It's expected behavior. This is how html and css works, you can't change the precedence in html with the order of class-names.

What matters is the specificity of css selectors and the order of declarations in the source code if the specificity is the same. Because these declarations have the same specificity (single class-names) and the .p-4 declaration is defined later than .p-0 it will be the one applied to the element.
Read more about specificity: MDN Article | Specifics on CSS Specificity (CSS-Tricks)

If you are trying to dynamically toggle between p-0 and p-4 you should always swap them out, toggle both, you shouldn't rely on css source order.

@jasonlimantoro
Copy link
Author

@tlgreg got it, thanks for the in depth answer

@tim-phillips
Copy link

tim-phillips commented Mar 14, 2020

The order of class names defined on an HTML element have no effect on styling. CSS Specificity only cares about what is defined in stylesheets.

Most specificity articles, including the ones referenced above, don't talk about the order of HTML class names. This helped:

https://stackoverflow.com/questions/12258596/class-overrule-when-two-classes-assigned-to-one-div/12258654

As a workaround, I've defined some base styles in order to increase specificity in my tailwind.css:

@tailwind base;

body {
  @apply text-sm;
}

nav a {
  @apply px-3;
  @apply py-2;
  @apply text-gray-800;
}

nav a.active {
  @apply rounded-sm;
  @apply bg-blue-gray-dark;
  @apply text-gray-200;
}

@tailwind components;

@tailwind utilities;

@DylanVann
Copy link
Contributor

It would be interesting to make something like https://www.npmjs.com/package/classnames, but that removes p-4 from the output if it sees p-0 applied after. This would allow making React components that can have Tailwind styles overridden, even if they're being overridden with classes of lower specificity.

@mracette
Copy link

mracette commented Jan 29, 2021

I agree with @DylanVann. If tailwindcss used the "latest" class as the most specific, it would make it easier to create reusable components with a fallback class. For example:

const GenericButton = ({ children, classes }) => {
  const defaultClasses = `px-4 py-2 h-10 w-32 bg-purple-600`;
  const finalClasses = `${defaultClasses} ${classes}`;
  return (
    <button type="button" className={finalClasses}>
      {children}
    </button>
  );
};

const ColorfulButtons = () => (
  <GenericButton classes="bg-red-600" />;
  <GenericButton classes="bg-green-600" />;
  <GenericButton classes="bg-blue-600" />;
)

Wouldn't this be possible by selectively removing properties from the final class output when there are conflicts?

@cipriancaba
Copy link

You can do something like <button class="rounded-lg p-4 inline-block p-!0 default ripple">No?</button> with the new jit compiler

@kuzdogan
Copy link

Agreed! I also thought I could create a default styling for a component and override it by simply passing className props, but does not work if specificities of attributes are different.

const StatusBadge = ({ status, className }) => {
      return (
        <span className={`${className} px-4 py-2 ml-auto text-base rounded-full text-yellow-600  bg-yellow-200 `}> {status}</span>
      );
}

const App = () => {
  return (
    // text-base overrides text-sm both like this and when ${className} is at the end
    <StatusBadge className='text-sm/>  // text appears large
  )
}

@sirrobert
Copy link

For what it's worth, here's a js function I wrote that does this. Later classes are given absolute priority.

function mergeTailwindClasses (...classStrings) {
  let classHash = {};
  classStrings.map(str => {
    str.split(/\s+/g).map(token => classHash[token.split('-')[0]] = token);
  })
  return Object.values(classHash).sort().join(' ');
} 

Usage:

let a = 'p-4 bg-red-200 mx-6';
let b = 'p-2 font-bold m-3';
let f = mergeTailwindClasses(a, b); 

console.log(f);  // bg-red-200 font-bold m-3 mx-6 p-2

Notes:

The sort() isn't necessary, of course, but I like the classes in some order for easier debugging.

This doesn't work in properties that are semantically grouped towards the front (like bg-repeat-y, where the main semantic content is bg-repeat and the value is y, as opposed to bg-red-200, where the main semantic content is bg and the value is red-200).

I haven't needed the version that handles bg-repeat-style classnames yet, but someone might. Here's the function I wrote for that (for completeness).

function mergeTailwindClassesAlt (...classStrings) {
  let classHash = {};
  classStrings.map(str => str.split(/\s+/g).map(
    token => classHash[token.split('-').slice(0,-1).join('-')] = token
  ))
  return Object.values(classHash).sort().join(' ');
} 

Same usage. In reality, you probably need a manual switch to differentiate.

@HertzaHaeon
Copy link

Nice solution @sirrobert, but classes like .bg-transparent and .bg-contain would still cause trouble, it seems. I guess it would be hard to avoid that with Tailwind's nice shorthand naming scheme.

I found this more complicated solution that seems to do the trick even for the above problem: https://github.com/richardgill/tailwind-override

@abrehamgezahegn
Copy link

!p-0 to apply important styles. It's a quick fix.

dgdavid added a commit to jangouts/jangouts that referenced this issue Jun 28, 2022
By swaping the used class for the border instead of trusting in the
order they are defined. In previous version "border-secondary" was
defined before "border-white" which seems to not be longer true in the
new version. Anyway, see [1][2] and [3] for better understanding of the
problem and CSS specificity.

[1] tailwindlabs/tailwindcss#1010 (comment)
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity
[3] https://css-tricks.com/specifics-on-css-specificity/
dgdavid added a commit to jangouts/jangouts that referenced this issue Jun 29, 2022
By swaping the used class for the border instead of trusting in the
order they are defined. In previous version "border-secondary" was
defined before "border-white" which seems to not be longer true in the
new version. Anyway, see [1][2] and [3] for better understanding of the
problem and CSS specificity.

[1] tailwindlabs/tailwindcss#1010 (comment)
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity
[3] https://css-tricks.com/specifics-on-css-specificity/
@bvanderdrift
Copy link

You could use the fact that the closest parent takes precedence to create a fallback hierarchy. Wrap the element in a div and apply the fallback styles to the div.

Before:

export const P = ({ className, children, ...props }) => (
  <p className={`font-sans text-black-500 text-base md:text-lg ${className || ""}`} {...props}>
    {children}
  </p>
);

Fixed:

export const P = ({ className, children, ...props }) => (
  <div className="font-sans text-black-500 text-base md:text-lg">
    <p className={className} {...props}>
      {children}
    </p>
  </div>
);

@bitttttten
Copy link

In a perfect world, you could write a function that first takes a string of tailwind classes and turns it into CSS. Then secondly, takes that CSS, ignoring and purging all the duplicated and conflicting styles, and turns it back into tailwind classes.

Input:

const class = tw("text-red-500 p-4", "p-0");
// css: { color: rgb(239 68 68); padding: 1rem; padding: 0rem; }
// purged: { color: rgb(239 68 68); padding: 0rem; }
// result: text-red-500 py-0

@jahilliard
Copy link

jahilliard commented Nov 16, 2022

I found this the package tailwind-merge seems to be what people are looking for.

@mohitkumawat310
Copy link

I found this the package tailwind-merge seems to be what people are looking for.

Thanks bro this works very well

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

No branches or pull requests