emotion-js / emotion

👩‍🎤 CSS-in-JS library designed for high performance style composition
https://emotion.sh/
MIT License
17.47k stars 1.11k forks source link

Emotion styles overriding equally-specific non-Emotion styles. #1078

Closed DesignerDave closed 5 years ago

DesignerDave commented 5 years ago

I'm currently in the process of building a shared component library, and one of the requirements is that any given component's style can easily be overridden by the consumer of said library.

Take the following (pseudo-code) example:

<html>
  <head>
    <style>.custom-style {color: red;}</style>
  </head>
  <body>
    <div id="react-root">
      <!-- React Component -->
      <CustomComponent className='custom-style'>Hello</CustomComponent>
    </div>
  </body>
</html>
const style = css`
  color: blue;
`
const CustomComponent = ({className, children}) => (
  <div className={`${style} ${className}`}>{children}</div>
)

Reproduction

Attempt to override any Emotion CSS property with an equally-selective style in the document <head> not created with Emotion.

Problem description:

The behavior I'd like here is for the color of the word "Hello" to be red. Currently, it seems that emotion appends, rather than prepends its style tags in the document <head>, giving it precedence over application styles with the same CSS specificity.

Suggested solution:

Ideally, you would be able to configure whether or not you want the emotion styles to take precedence over non-emotion styles. One solution for this would be to specify whether style tags are prepended or appended. I know Emotion 10 puts the style tags in an entirely different part of the document, but I'm hoping Emotion 9 has this ability somewhere, or that there is some workaround.

Andarist commented 5 years ago

Emotion appends automatically a style tag into head, but u can create emotion instance (in v9) or use CacheProvider in v10 (or smth similar, im not yet proficient with new emotion@10 APIs) to specify the container element for the emotion style tag. For v9 docs look here - https://5bb1495273f2cf57a2cf39cc--emotion.netlify.com/docs/create-emotion

DesignerDave commented 5 years ago

Managed to get this working using those docs. Thanks!

hoetmaaiers commented 5 years ago

@DesignerDave , would you like to share you solution?

I also have a component rendering styles through the css prop. Where I use this emotion styled component and want to extend the styling via className="my-custom-class", the emotion css takes precedence over my-custom-class.

My component is constructed this way:

export const MapControl = (props: MapControlProps) => {
  return (
    <div
      css={css`
        ${mapControlStyle}
      `}
      {...props}
    >
      {props.children}
    </div>
  );
};

So it looks like the order of style injection is my problem? Can I define the order of <style /> injection emotion takes? My custom-class-name is a styled component. So to be clear, I define it as className={styles.myCustomClass}

Andarist commented 5 years ago

You can control where emotion injects its styles by using custom emotion instance. If using React and emotion@10 you might want to look at CacheProvider API

hoetmaaiers commented 5 years ago

Ok, I looked into this. I am using emotion in a component library which I include in serveral projects. Will the CacheProvider also work in this situation?

Andarist commented 5 years ago

It depends on what APIs you are already using and how the whole thing is structured. It might work - or you might need custom emotion instance created through create-emotion.

hoetmaaiers commented 5 years ago

Ok, thank you for the insights.

On Mon, 14 Oct 2019 at 11:10, Mateusz Burzyński notifications@github.com wrote:

It depends on what APIs you are already using and how the whole thing is structured. It might work - or you might need custom emotion instance created through create-emotion.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/emotion-js/emotion/issues/1078?email_source=notifications&email_token=AADSITPTAVSRPJHDVGYCOYTQOQZOPA5CNFSM4GIVBH2KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEBD2XTY#issuecomment-541567951, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADSITKIPAL7UWA3RUC7TWLQOQZOPANCNFSM4GIVBH2A .

--

Beste groet,

Robin Houdmeyers

E robin.houdmeyers@gmail.com M +32 485 12 82 61

teknotica commented 2 years ago

I know this issue has been closed but hopefully someone can read this message still

Here's my issue:

We have a main app using SASS and a Design System library using Emotion, both using React. Problem is that because Emotion styles are being appended at the very end of the head, we are unable to override some styles from the app.

I created successfully the instance approach from the docs however, since I'm using React (and emotion/react) I believe I need to also create a custom instance of emotion react in order for this to work completely.

Found an article where someone was able to do this with emotion styled components, using the create-emotion-styled package:

export default createEmotionStyled(emotion, React)

is there a way to do this with emotion/react too? I haven't found anything at all on the web yet.

Thanks a lot in advance!

DesignerDave commented 2 years ago

Hey @teknotica! We're still building the component library I wrote about 4(!) years ago using Emotion. We're still very much in the same boat, except that luckily custom instances got the capability to prepend natively using the custom instance config. Originally, we had a hacky workaround where we inserted a <div> into the <head> (which is technically not to HTML specs, but did work across browsers consistently), and setting that div to be the "container" for Emotion's style tags.

It's my understanding that the create-emotion-styled package isn't compatible with the latest version of Emotion. I believe we tried using this a while ago, and found that the package hasn't been updated in roughly 4 years. I'd be interested to hear if you have been able to use the styled API using this package on the more recent versions of Emotion! There's another issue where the maintainers clarify that create-emotion-styled doesn't have an Emotion 10 counterpart, and I don't think this changed in Emotion 11: https://github.com/emotion-js/emotion/issues/1207#issuecomment-459995445

The main concessions we've made to support prepending the styles with Emotion are:

I will be clear that in Emotion with these limitations, you can make an effective and scalable design system. Sure, it'd be a bit nicer to maintain with styled, but it's still better than using something like CSS modules for a design system library.

I still would love the maintainers to support styled using custom instances of Emotion. For Design Systems library maintainers like us, the CacheProvider approach is a non-starter since we usually don't control where our components are rendered (at least in our case, where our library was built to work with a ton of pre-existing applications). We also have to work with these applications that may also use Emotion, and we don't want our code to potentially modify the behavior of their own styles.

I apologize that this isn't as satisfying an answer as you were probably looking for. Ultimately, it definitely feels as though the custom instances of Emotion are second-class way of using the library, even though DS libraries must use this approach if they need to be compatible with other libraries, vanilla/precompiled CSS, etc.

teknotica commented 2 years ago

Hi @DesignerDave Thanks a lot for your comprehensive response. It was really helpful.

I'm really curious to hear how did you approach this bit you mentioned in you message:

We don't use the @emotion/react package despite using React. This does unfortunately prevent us from using the css prop and some other small features, but you may not need it overall.

How do you use Emotion + React without the @emotion/react package? 🤔

DesignerDave commented 2 years ago

@teknotica We simply use patterns like this:

import { cx, css } from '@leafygreen-ui/emotion'; // We have a package containing our custom instance.

const componentStyles = css`
  // Whatever styles you need
`;

const otherComponentStyles = css`
  // Perhaps some conditional styles based on a prop
`;

function MyComponent({ someBooleanProp }) {
  return (
    <div
      className={cx(componentStyles, {
        [otherComponentStyles]: someBooleanProp,
      })} />
  );
}

You miss out on a few features and "optimizations" (according to the docs, a little unclear myself on which optimizations they're talking about), but if you keep to using cx and css, it's altogether optional, and not a dependency of any of the 40-50 packages our design system ships now!

Andarist commented 2 years ago

I'm not completely sold on the idea yet but maybe we could allow you to override the context using a prop. This way you could create wrappers around our APIs that would just inject this prop. You can probably even do this today - sort of - but that would force you to add quite a bit of redundant components to your React tree. Proof of concept can be found here: https://codesandbox.io/s/interesting-kirch-1fje24?file=/src/App.js:234-658

DesignerDave commented 2 years ago

Overriding using a prop could work well for us! That POC could work now if I wasn't worried about a ton of bloat in the React tree (which you noted as a concern).

What might be compelling for my own use case, if possible, would be to allow the override on the styled function itself through the styled options that are provided, instead of a prop on the component. It could allow a pattern like this:

const dsStyled = (element, options = {}) => {
  return styled(element, { ...options, prepend: true });
}

const MyComponent = dsStyled('div')`
  background-color: blue;
`;

function App() {
  return <MyComponent />
}

FWIW, I believe we could leverage either API and it would be a vast improvement for us.

Andarist commented 2 years ago

It would be great take other context-based APIs into consideration too (css prop, ClassNames, etc), when thinking about this API

DesignerDave commented 2 years ago

That's fair. I'm curious, as a prop, how would the override API work in your idea with classNames (assuming that the component was not created with the styled API)? Would that rely on babel transformations or the JSX pragma to work (like the css prop)?

The thing that's holding us back from switching to the Emotion cache provider is that we need to both ensure that our provider doesn't interfere with a consuming application's use of Emotion, and that the consuming application's provider (if it has one) doesn't interfere with our component's use of Emotion. What if you could "key" an Emotion cache to methods generated using createEmotion?

This might be a bit out there, but just to get the ideas flowing:

// Instead of a component-specific config, createEmotion could accept an id that corresponds with a provider in your application.
// It would be awesome if in this world, create Emotion also could return the styled method.
const cacheId = 'ds-cache-id';
const { cx, css, styled } = createEmotion(cacheId);

const dsCache = createCache({
  key: 'ds-prefix',
  prepend: true,

  // Optional – when set, this cache (and providers using it) are only used by Emotion functions
  // returned from the createEmotion method passed this id.
  cacheId: cacheId,
});

const MyComponent = styled.div`
  background-color: blue;
`;

function App() {
  return (
    <EmotionCache value={dsCache}>
      <MyComponent />
    </EmotionCache>
  );
}

Maybe this approach can be simplified a bit:

const dsCache = createCache({
  key: 'ds-prefix',
  prepend: true,

  // Restricts the cache to methods generated using this cache as input
  boundedCache: true,
});

const { cx, css, styled } = createEmotion(dsCache);

const MyComponent = styled.div`
  background-color: blue;
`;

function App() {
  return (
    <EmotionCache value={dsCache}>
      <MyComponent />
    </EmotionCache>
  );
}

I'm not 100% sure if these approaches could work with the CSS prop, but since the prop requires the css function to be used, I think so? Obviously, these would likely be massive changes under the hood, and would need to align with the roadmap of the library.