JedWatson / react-select

The Select Component for React.js
https://react-select.com/
MIT License
27.62k stars 4.12k forks source link

Colors are very difficult to theme #3692

Closed SimeonC closed 2 years ago

SimeonC commented 5 years ago

I've been working with integrating ReactSelect with our own theming library and I've come across a few issues.

  1. Conflicts in theme namespacing - If you use Emotion components to override ReactSelect components you can get conflicts in the theme object that we override. For example, in my global ThemeProvider I have a colors key with a different format that means that I end up having a weird hybrid of the colors object passed down and this causes issues. It would be better for react select to pass down theme={{ 'react-select': theme }} instead of theme={theme} as this allows prevents collisions.
  2. The named colors are too generic; primary primary75 primary50 and primary25 - I can't control what colors are used in what situations so it would make more sense to use named values like menuBorder activeBackground activeText etc so we know what we are theming and what it affects.

I know this is a pretty opinionated and technically I can work around this by replacing all Components with custom ones but this feels like it defeats the purpose of a "theme" option. To me, it makes more sense to name the theme colors as for what they are actually used for rather than having to figure out that primary25 is used only for when an element is "focused". Another issue is that due to us using neutral0 for both the selected text color and the control background I can't theme these individually, not to mention there is no text color available for when a value is focussed so using darker colors there won't work - for example when implementing a Dark Theme.

steelej commented 5 years ago

Ran into this problem today. Took over an hour to tease out what generic named colour was used for what aspect of the select box (in retrospect I should have just looked at the source, it's relatively easy to see the usages there). Using semantic names would definitely be way better.

jqueryisamonad commented 5 years ago

Working with the theme is just terrible.

Maybe it'll be helpful to somebody.

const colors = {
 /*
  * multiValue(remove)/color:hover
  */
  danger: 'var(--danger)',

 /*
  * multiValue(remove)/backgroundColor(focused)
  * multiValue(remove)/backgroundColor:hover
  */
  dangerLight: 'var(--danger-light)',

 /*
  * control/backgroundColor
  * menu/backgroundColor
  * option/color(selected)
  */
  neutral0: 'var(--neutral-0)',

 /*
   * control/backgroundColor(disabled)
  */
  neutral5: 'var(--neutral-5)',

 /*
  * control/borderColor(disabled)
  * multiValue/backgroundColor
  * indicators(separator)/backgroundColor(disabled)
  */
  neutral10: 'var(--neutral-10)',

 /*
  * control/borderColor
  * option/color(disabled)
  * indicators/color
  * indicators(separator)/backgroundColor
  * indicators(loading)/color
  */
  neutral20: 'var(--neutral-20)',

 /*
  * control/borderColor(focused)
  * control/borderColor:hover
  */
  neutral30: 'var(--neutral-30)',

 /*
  * menu(notice)/color
  * singleValue/color(disabled)
  * indicators/color:hover
  */
  neutral40: 'var(--neutral-40)',

 /*
  * placeholder/color
  */
  neutral50: 'var(--neutral-50)',

 /*
  * indicators/color(focused)
  * indicators(loading)/color(focused)
  */
  neutral60: 'var(--neutral-60)',

  neutral70: 'var(--neutral-70)',

 /*
  * input/color
  * multiValue(label)/color
   * singleValue/color
  * indicators/color(focused)
  * indicators/color:hover(focused)
  */
  neutral80: 'var(--neutral-80)',

  neutral90: 'var(--neutral-90)',

 /*
  * control/boxShadow(focused)
  * control/borderColor(focused)
  * control/borderColor:hover(focused)
  * option/backgroundColor(selected)
  * option/backgroundColor:active(selected)
  */
  primary: 'var(--primary)',

 /*
  * option/backgroundColor(focused)
  */
  primary25: 'var(--primary-25)',

 /*
  * option/backgroundColor:active
  */
  primary50: 'var(--primary-50)',

  primary75: 'var(--primary-75)',
};
Nantris commented 4 years ago

@JedWatson is there any way for users to use their own, existing Emotion theme object?

I figured when we resorted to custom sub-components like a custom <Option />, we'd be able to use our theme object.


Edit: We ended up wrapping the parent of our react-select component with withTheme() and then overriding the built in theme by passing that to the custom components. Pretty clunky, but workable. It would be a lot better if React-Select didn't override the user theme by default.

jeffshek commented 4 years ago

@hovoodd thank you so much for posting that, it was so helpful! I'm quite meh at frontend, so I had to jam a lot of colors (ie. pink, purple) while debugging to see what would happen. I don't think I would have been able to work if you hadn't posted that!!!

My use case : I needed to make react-select work with light/dark mode in Material UI's theme system, so I'm posting my code I used to make that work for someone who has to do something similar.

import useTheme from '@material-ui/core/styles/useTheme';

const getSelectTheme = (theme) => {
  return ({
    /*
    * multiValue(remove)/color:hover
    */
    danger: 'purple',

    /*
     * multiValue(remove)/backgroundColor(focused)
     * multiValue(remove)/backgroundColor:hover
     */
    dangerLight: theme.palette.grey[200],

    /*
     * control/backgroundColor
     * menu/backgroundColor
     * option/color(selected)
     */
    neutral0: theme.palette.background.default,

    /*
      * control/backgroundColor(disabled)
     */
    neutral5: "orange",

    /*
     * control/borderColor(disabled)
     * multiValue/backgroundColor
     * indicators(separator)/backgroundColor(disabled)
     */
    neutral10: 'pink',

    /*
     * control/borderColor
     * option/color(disabled)
     * indicators/color
     * indicators(separator)/backgroundColor
     * indicators(loading)/color
     */
    neutral20: theme.palette.grey['A200'],

    /*
     * control/borderColor(focused)
     * control/borderColor:hover
     */
    // this should be the white, that's normally selected
    neutral30: theme.palette.text.primary,

    /*
     * menu(notice)/color
     * singleValue/color(disabled)
     * indicators/color:hover
     */
    neutral40: 'green',

    /*
     * placeholder/color
     */
    // seen in placeholder text
    neutral50: theme.palette.grey['A200'],

    /*
     * indicators/color(focused)
     * indicators(loading)/color(focused)
     */
    neutral60: 'purple',
    neutral70: 'purple',

    /*
     * input/color
     * multiValue(label)/color
      * singleValue/color
     * indicators/color(focused)
     * indicators/color:hover(focused)
     */
    neutral80: theme.palette.text.primary,

    // no idea
    neutral90: "pink",

    /*
     * control/boxShadow(focused)
     * control/borderColor(focused)
     * control/borderColor:hover(focused)
     * option/backgroundColor(selected)
     * option/backgroundColor:active(selected)
     */
    primary: theme.palette.text.primary,

    /*
     * option/backgroundColor(focused)
     */
    primary25: theme.palette.background.dark,

    /*
     * option/backgroundColor:active
     */
    primary50: theme.palette.background.paper,
    primary75: theme.palette.background.paper,
  })}

export const useActivityLogDataForm = ({
  const theme = useTheme()
  const formThemeColors = getSelectTheme(theme)

  return (<Select options={options}
                theme={theme => ({
                  ...theme,
                  colors: {
                    ...formThemeColors
                  }})}
        />)

2020-08-09 13 10 38 2020-08-09 13 10 55

(A demo of working react-selects should be on the forms w/light and dark themes on https://app.betterself.io/demo later tonight)

joeyfigaro commented 2 years ago

Did anything ever come of this @JedWatson? Trying to deal with overriding the theme to support a dark mode.

gsusI commented 2 years ago

Super helpful resource here: https://stackoverflow.com/a/60993034/3198983

Especially when you don't know on your back-end which theme will be loaded, for example when using Tailwindcss

gsusI commented 2 years ago

My final approach has been to inject styles for dark mode using jsx:

  1. I declare the Select component with an id and a classNamePrefix:
                <Select
                    id={this.props.id}
                    classNamePrefix={this.props.id}
                    ...
                />
  1. I inject the styles for dark mode using jsx (this works by default in Next.js, I'm not sure what the requirements are for other stacks):

                <style global jsx>{`
                  @media (prefers-color-scheme: dark) {
                    #${this.props.id} .${this.props.id}__control {
                      background-color: #6b727f !important;
                      border-color: #2b2b2b00;
                      padding: 4px;
                    }
    
                    #${this.props.id} .${this.props.id}__placeholder {
                      color: #d1d5db !important;
                    }
    
                    #${this.props.id} .${this.props.id}__multi-value {
                      background-color: slategray;
                      color: #fff;
                    }
    
                    #${this.props.id} .${this.props.id}__input-container {
                      color: #d1d5db;
                      border: none;
                    }
    
                    #${this.props.id} .${this.props.id}__input-container:focus {
                      border: none !important;
                      outline: none !important;
                    }
    
                    #${this.props.id} .${this.props.id}__input:focus {
                      border: none !important;
                      outline: none !important;
                    }
    
                    #${this.props.id} .${this.props.id}__input::after {
                      border: none !important;
                      display: none;
                    }
    
                    #${this.props.id} .${this.props.id}__multi-value__label {
                      color: #fff;
                    }
    
                    #${this.props.id} .${this.props.id}__menu {
                      background-color: #2d3748;
                    }
    
                    #${this.props.id} .${this.props.id}__option {
                      color: #fff;
                    }
    
                    #${this.props.id} .${this.props.id}__option--is-focused {
                      background-color: slategray;
                    }
                  }
                `}</style>

In my case, I detect the user preference using a media query. However, you might be using a class set higher up the hierarchy; in that case, you could do:

                <style global jsx>{`
                  body.dark #${this.props.id} .${this.props.id}__control {
                     ...