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

Add dark mode variant #2279

Merged
merged 4 commits into from Sep 1, 2020
Merged

Add dark mode variant #2279

merged 4 commits into from Sep 1, 2020

Conversation

adamwathan
Copy link
Member

@adamwathan adamwathan commented Sep 1, 2020

This PR introduces experimental support for a new dark variant that allows you to conditionally add different styles based on whether "dark mode" is enabled.

In practice it looks like this:

<div class="bg-white text-black dark:bg-black dark:text-white"></div>

It is made available under the darkModeVariant experimental flag:

// tailwind.config.js
module.exports = {
  experimental: {
    darkModeVariant: true
  },
  // ...
}

Functionality

Stackable by default

The dark variant automatically stacks against basic variants like hover and focus, and also combines with responsive prefixes like sm, lg, etc., so you can do things like this no problem:

<button class="lg:dark:hover:bg-purple-300 ...">
  <!-- ... -->
</button>

The dark variant does not stack with motion-safe and motion-reduce variants, but I don't expect that will actually be an issue for anyone. In the future we will likely provide more control over how variants are stacked in general.

Media query and class-based strategies

By default we use a "media" strategy, which generates classes like this:

@media (prefers-color-scheme: dark) {
  .dark\:bg-black {
    background-color: black;
  }
}

This is fine for many projects, but has the limitation of not being able to store a user's preference, or allow them to switch between light mode and dark mode with a toggle since it is tied to their operating system preferences.

To support more sophisticated needs, I've also implemented a "class" strategy that you can enable like so:

// tailwind.config.js
module.exports = {
  dark: 'class',
  // ...
}

This will generate classes like this instead:

.dark .dark\:bg-black {
  background-color: black;
}

To use this strategy, you'll need to add a parent class of "dark" whenever you'd like to enable dark mode, usually to the html element:

<html class="dark">
  <!-- ... -->
  <body>
    <div class="bg-white text-black dark:bg-black dark:text-white"></div>
  </body>
</html>

Default configuration

By default, the dark option is set to media:

// tailwind.config.js
module.exports = {
  dark: 'media',
  // ...
}

The dark variant is enabled for the following core plugins:

  • backgroundColor
  • borderColor
  • divideColor
  • textColor
  • placeholderColor
  • gradientColorStops

Enabling in your config file

To enable the dark variant for any of Tailwind's utilities, add dark to the variants configuration for any plugin:

// tailwind.config.js
module.exports = {
  variants: {
    opacity: ['responsive', 'hover', 'focus', 'dark']
  },
  // ...
}

Design Rationale

The obvious question when implementing this feature is "why not build some generalized universal theming solution that supports an arbitrary number of themes?"

The main reasons are:

  1. Operating systems only support light and dark.
  2. Supporting more than a single theme introduces significantly more complexity and challenging decisions.
  3. 99% of people only care about dark mode.
  4. We can always add a universal theming feature down the road that supersedes this if we like.

So for now, this feature is unapologetically dark mode only. Every design decision has been made with the mindset of "we only support light mode and dark mode and we don't care about supporting anything else".

That's why the parent class is simply dark, the prefix is just dark, the configuration option is just dark, etc.


Would love any feedback on this. One concern I have myself is that using the class strategy, you can't easily do this:

<html class="dark bg-pink-500 dark:bg-black">
  <!-- ... -->
</html>

...because only child elements are affected. We could update the selector to make this work but it would result in a significant increase in file size. If anyone has any ideas on how to make that work without doubling the number of color-based selectors I'd love to hear them.

In general, this feature introduces a lot of classes because of the fact that it stacks and targets color classes, so it adds a lot of weight to the default CSS file. I personally don't care (I use PurgeCSS and you should too) but I expect it will cause more "wow Tailwind is huge how does anyone use this?" type of remarks. I'm considering building a "TailwindLite" CDN version for 2.0 to try and combat this a bit, but still eventually we are going to hit some critical threshold where the file size becomes an issue even in development mode, so do have to be careful with this stuff.

@zaknesler
Copy link

zaknesler commented Sep 1, 2020

I think as long as this is kept as opt-in somehow, it shouldn't be a problem for most people. Purge CSS is our friend. I also think that utility classes are such a great way to implement dark mode color differences. Great work!

@pspeter3
Copy link

pspeter3 commented Sep 1, 2020

Is there a way to have a hybrid strategy? Eg default to the media query unless the theme is explicitly set as a class?

@ekvedaras
Copy link

ekvedaras commented Sep 1, 2020

Would we really need to add other color affecting classes then dark to html tag? I think those should be placed at body. So, dark affecting only children is fine.

@OwenMelbz
Copy link
Contributor

OwenMelbz commented Sep 1, 2020

Regarding the file size comments, maybe this is where the divergence starts and bigger features start getting pulled into non-default plugins like typography.

That way you specifically import it if you want it, otherwise everybody else is in affected until the dark mode fad goes away :p

@lihbr
Copy link
Contributor

lihbr commented Sep 1, 2020

Just dropping that here in case it wasn't considered but perhaps also including a dark: "attribute" options along with the planned dark: "class" one could be helpful (as having classes on the html tag is sometimes not convenient), mapping it to data-dark="true"?

@adamwathan
Copy link
Member Author

adamwathan commented Sep 1, 2020

Is there a way to have a hybrid strategy? Eg default to the media query unless the theme is explicitly set as a class?

@pspeter3 The best way to do this is just to use the class strategy and use JS to detect the media query and set it.

Something like this as an inline script in the head before any of the page loads:

<html>
<head>
  <script>
  if (('themePreference' in localStorage) && localStorage.themePreference === 'dark') {
    document.querySelector('html').classList.add('dark')
  } else if (!('themePreference' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches) {
    document.querySelector('html').classList.add('dark')
  }
  </script>
</head>
<!-- ... -->
</html>

@adamwathan
Copy link
Member Author

adamwathan commented Sep 1, 2020

Would we really need to add other color affecting classes then dark to html tag? I think those should be placed at body. So, dark affecting only children is fine.

@ekvedaras The main use case is setting a background color that appears when overscrolling in macOS, that can only be done on the HTML tag unfortunately.

@adamwathan
Copy link
Member Author

adamwathan commented Sep 1, 2020

Just dropping that here in case it wasn't considered but perhaps also including a dark: "attribute" options along with the planned dark: "class" one could be helpful (as having classes on the html tag is sometimes not convenient), mapping it to data-dark="true"?

@lihbr I considered that originally but ultimately decided to use a class because then the class automatically respects the user's prefix setting, so dark will become tw-dark for example for people who need that to avoid collisions. What's a situation where you can't put classes on the HTML tag?

@adamwathan adamwathan merged commit 5701d78 into master Sep 1, 2020
4 checks passed
@adamwathan adamwathan deleted the dark-mode branch Sep 1, 2020
@lihbr
Copy link
Contributor

lihbr commented Sep 1, 2020

@lihbr I considered that originally but ultimately decided to use a class because then the class automatically respects the user's prefix setting, so dark will become tw-dark for example for people who need that to avoid collisions. What's a situation where you can't put classes on the HTML tag?

Makes sense indeed! I think I had some issues in the past with Modernizr conflicting with Vue meta but that was probably due to a poor usage of mine 🤔

@garlandcrow
Copy link

garlandcrow commented Sep 2, 2020

I did a simple implementation of dark mode in my last project, at first using the media queries (like this PR), but as another "screen", and when I needed to use tailwind in CSS it looked really nice:

@screen dark {
    .panel {
        @apply text-white;
    }
}

But when I needed to make dark mode settable by the user, I had to switch to doing something like setting a class on html (like in this PR) and then doing this wasn't as nice (I use svelte). I had to write these each time I wanted to target something as dark like:

:global(html.dark) .panel {
    @apply text-white;
}

I don't know how those @screen like helpers are generated, but is there any way to make it easy to target the dark mode by using one of those? Maybe something like

@dark {
    .panel {
        @apply text-white;
    }
}

@adamwathan
Copy link
Member Author

adamwathan commented Sep 2, 2020

IndexedDB is async so you can get a flash of the wrong color whereas localStorage is sync and blocking 😊

Anyways if people want to use the media query strategy of course you’re welcome to, that’s why I’m including both. It’s even the default!

@alexandprivate
Copy link

alexandprivate commented Sep 2, 2020

This is amazing, previous media implementation was on but yet we have to do a lot of work on saving the user preferences, thanks @adamwathan !

@gauravkumar37
Copy link

gauravkumar37 commented Sep 2, 2020

I'll say it again though, a user who actively sets their OS to dark mode is making their selection for light/dark themes already—negating the need to have a toggle.

I think not giving users the option to override OS theme for a particular website is not taking accessibility into account. Depending on the website, one may choose a different theme. This is crucial for users with visual disability. I have seen such users who prefer dark theme overall (since decreased brightness is better for eyes) but prefer light theme for blogs where they have to read a lot of text because reading light text on dark background irritates their eyes.

@brandonpittman
Copy link

brandonpittman commented Sep 2, 2020

@gauravkumar37

We should probably also not be handling this with JavaScript in case the user has disabled it or has an unstable connection. That seems like an accessibility concern as well.

The only reasonable solution is to manage user preference server-side, or maybe a cookie, and then send pre-rendered HTML with the correct CSS down the wire.

@sowenjub
Copy link

sowenjub commented Sep 4, 2020

Have you considered the way Apple approaches this? Instead of defining a dark variant, you would define a dark color for each color in the configuration file.

Below is a screenshot of the color assets in Xcode.

So bg-white in light mode would be white, but default to black in dark mode.

This means the class strategy is not an issue anymore, and you potentially save a lot of code, both in the CSS files and as you create your design (since you don't have to use any class at all).

You could keep the dark variant to give some more flexibility when needed.

@paulpopus
Copy link

paulpopus commented Sep 5, 2020

@sowenjub That's achievable by setting your color variables to CSS variables, no? Which you can then swap on the fly with different stylesheets. The issue with it is that your classes either need to be function based like bg-primary or your bg-white becomes meaningless essentially.

@sei-jdshimkoski
Copy link

sei-jdshimkoski commented Sep 8, 2020

@sowenjub That's achievable by setting your color variables to CSS variables, no? Which you can then swap on the fly with different stylesheets. The issue with it is that your classes either need to be function based like bg-primary or your bg-white becomes meaningless essentially.

Doing this breaks text-opacity and bg-opacity classes, so if you need those, you can't use CSS variables for color declarations.

@adamwathan
Copy link
Member Author

adamwathan commented Sep 9, 2020

@cbenjafield Can you create a GitHub repo that shows it not working so I can easily pull it down and tinker with it? Don't have a ton of time to set it up from scratch but if I can just clone it and sort it out in 5 minutes I can definitely help.

@cbenjafield
Copy link

cbenjafield commented Sep 9, 2020

@cbenjafield Can you create a GitHub repo that shows it not working so I can easily pull it down and tinker with it? Don't have a ton of time to set it up from scratch but if I can just clone it and sort it out in 5 minutes I can definitely help.

Ah, solved it literally just as you replied - just me being stupid and not updating purgecss to handle the dark class 🤦🏼‍♂️

@Im-Fran
Copy link

Im-Fran commented Oct 16, 2020

HI!
How can I implement this on laravel?
I've modified my tailwind.config.js so now it's like this: https://paste.theprogramsrc.xyz/facaafcaaf.js
and I just used npm i && npm run dev but is not working. The script I've added to the head is working properly (adding the class dark to the html tag) and I'm testing it by using the following code: <div class="bg-gray-100 dark:bg-black">test</div> but the background is not black, it stays as gray

@paulpopus
Copy link

paulpopus commented Oct 16, 2020

@Im-Fran You probably need to enable the 'dark' variant for backgrounds too. You currently only have it for opacity. I believe dark:opacity-50 should work.

@Im-Fran
Copy link

Im-Fran commented Oct 17, 2020

Oh, oki, thanks

@IRediTOTO
Copy link

IRediTOTO commented Oct 21, 2020

Hi, how about prefix? Will dark become tw-dark? I didnt test but tell me if you know :D

@GSCrawley
Copy link

GSCrawley commented Oct 21, 2020

@steveszc
Copy link

steveszc commented Nov 16, 2020

I'm a little late to the party here, but it might be nice to offer a way to provide a custom class for the class strategy. For anyone that has already implemented a custom dark mode variant this might smooth the transition. In my case, we use is-dark-mode and we have this class in our preference persistence logic and all over our non-tailwind CSS.

Maybe something like this?

// tailwind.config.js
module.exports = {
  dark: { class: 'is-dark-mode' }
  // ...
}

I'd be happy to take a shot at a PR adding this if others agree it would be useful. It'd be great to land this before 2.0

This was referenced Mar 15, 2021
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

Successfully merging this pull request may close these issues.

None yet