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

Consider dynamic theming support #37983

Open
2 tasks done
Tracked by #37549 ...
yugabe opened this issue Jan 31, 2023 · 12 comments
Open
2 tasks done
Tracked by #37549 ...

Consider dynamic theming support #37983

yugabe opened this issue Jan 31, 2023 · 12 comments

Comments

@yugabe
Copy link

yugabe commented Jan 31, 2023

Prerequisites

Proposal

Consider adding support to virtually any number of themes, or even theming dynamically.

The theming support introduced in 5.3.0-alpha1 only made it harder to customize or extend the color palette. This proposal would make it arguably a lot easier to add custom themes, extend the color palette, shrink the CSS output by trimming unused colors from the palette and make both the user and framework codes easier to maintain.

Motivation and context

In the end, the current $primary/$secondary/$info etc. variables are leftovers of a time before CSS variables (custom properties) were made available to use. Consider defining a rule as follows, either by user code or in Bootstrap's source itself:

// Some user code:
.some-class {
  color: $primary;
}

This approach is problematic, because if $primary refers to a raw value (like it does currently, which is $blue, that resolves to #0d6efd by default), the .some-class rule above won't respond correctly to theming changes (#37976). This also happens if the user changes the default value to whatever else: in the end, the raw color value will be compiled into the raw CSS. A much more preferable way would be if both user code and framework code would refer to the CSS variable, like so:

// Some user code:
.some-class {
  color: var(--#{$prefix}primary);
}

Although, this interpolation is very prone to breaking, because nothing guarantees that the given hard-coded variable name will be correct. The user might simply make a typo and it would be very hard to debug an issue like this in some cases.
A simple solution would be something like this:

// Some user code:
$primary-var: var(--#{$prefix}primary);
.some-class {
  color: $primary-var;
}

It is very much also possible that making the theme colors (like $primary) available would serve no other purpose than for users to incorrectly use the raw, default, initial values of. Thus, it makes sense to fully repurpose the SCSS variables so that they point to the CSS variables instead of the raw values.

// Proposed change to Bootstrap variable declaration:
$primary: var(--#{$prefix}primary); // notice no '!default', as there is no reason to change this variable at all

This way, user code would, in most cases, automatically follow theming changes as well, because they wouldn't have to change from the initial example above:

// Some user code:
.some-class {
 color: $primary;
}

A breaking change though would be how the default values would need to be set for the users for themes:

// Proposed change to Bootstrap variable declaration:
:root {
    #{$primary}: $blue;
}

The above would set the CSS variable corresponding to the "old" $primary SCSS variable to $blue, its current default value. The problem with this approach is that there is no way to set these as "default" values, as because technically this isn't an SCSS variable declaration, and !default is SCSS's syntax - obviously not supported in compiled CSS. Thus, the user would have to manually define the variable value themselves at :root.

A more elegant way to define a full themeset could look as follows:

// Proposed change to Bootstrap theme declaration:
$themes: (
  "light": (
    "body-bg":    $white,
    "body-color": $gray-900,
    "primary":    $blue,
    "success":    $green,
    "info":       $cyan,
    "warning":    $yellow,
    "danger":     $red
  ),
  "dark": (
    "body-bg":    $gray-900,
    "body-color": $gray-500,
    "primary":    $blue-300,
    "success":    $green-300,
    "info":       $cyan-300,
    "warning":    $yellow-300,
    "danger":     $red-300
  )
) !default;

Notice that there are no more variable declarations at all (no $primary, nor $primary-var). That's because the above 5 values in the themes can be considered fully agnostic of context. The user can:

  • name the colors however they'd like,
  • can add or remove any number of them,
  • can simply add more themes by providing a theme key with a map structured as all other maps in the $themes map.

A user can then simply add a theme by doing something like:

$themes: (
  "light": (
    "body-bg": $gray-100,
    "body-color": $gray-900,
    "accent-1": #9ffa5c,
    "accent-2": $green-700,
    "error": $red
  ),
  "dark": (
    "body-bg":    $black,
    "body-color": $gray-200,
    "accent-1": tint-color(#9ffa5c, 60%),
    "accent-2": $green-300,
    "error": $red-200
  ),
  "blue": (
    "body-bg": $blue-100,
    "body-color": $gray-900,
    "accent-1": $blue-600,
    "accent-2": $cyan-300,
    "error": $purple-200
  )
)

Shading, borders, emphasis etc. can all be calculated based on the given values as well, and can be applied to themable components at the component level. Overriding any variable would be done in CSS variables instead. Notice that in the last example, body-bg and body-color were required colors, and "secondary", "light" and "dark" were removed. This is because $secondary (and tertiary) were repurposed in the most recent release, so they are essentially shades of gray or shades/tints of a given them color (-subtle).

@yugabe yugabe added the feature label Jan 31, 2023
@mdo
Copy link
Member

mdo commented Feb 1, 2023

This is super interesting, thanks for sharing! We're definitely in a weird place with v5.3.0 with theming—it's both powerful and slightly cumbersome. We've known it wasn't the complete solution for awhile and that's what we aim to address most in v6. We've talked about reimagining theme settings with a new map as well:

$theme: (
  "colors": (
    "primary": $primary,
    "secondary": $secondary,
    "success": $success,
    "info": $info,
    "warning": $warning,
    "danger": $danger,
  ),
  
  "fgs": (
    "default": $body-color,
    "secondary": $body-secondary,
    "tertiary": $body-tertiary,
  ),

  "bgs": (
    "default": $body-bg,
    "secondary": $body-bg-secondary,
    "tertiary": $body-bg-tertiary,
  ),

  "borders": (
    "default": $body-border,
    "secondary": $body-border-secondary,
    "tertiary": $body-border-tertiary,
  ),
) !default;

But adding in another layer for each theme, and iterating over those themes, is also really cool. Finding a middle ground here would be tremendous in v6.

@yugabe
Copy link
Author

yugabe commented Feb 1, 2023

Thanks for considering this, @mdo!

I also considered the secondary-tertiary colors, but I think they can be inferred from the defaults. Overriding all of those could be done as well, but maybe only via CSS variables, as the reuse of those variables across user code would provide issues that make Bootstrap non-responsive of theme changes currently.

Having a "minimal configuration, good defaults" policy could go a long way, if a way to override those defaults is also provided.

In my final example above, I intentionally avoided the $primary, $secondary, $success, $info, $warning, and $danger variables. These variables are a bit misguided in my opinion, because they refer to discreet colors in a static palette, so they cannot be reused anywhere in the code except where the theme is being defined. But at that point, it's easier to just skip them and refer to the colors themselves. Secondly, and seeing as how the default palette's colors (still, $primary and co.) are essentially only a map of string-colors, the user can be left to define the keys/names for these colors.

Consider the following part of the example:

  "blue": (
    "body-bg": $blue-100,
    "body-color": $gray-900,
    "accent-1": $blue-600,
    "accent-2": $cyan-300,
    "error": $purple-200
  )

This means that components that support theming would gain (at least) accent-1, accent-2, error, variants, like alert-accent-2. Moving the "default" variables, like body-bg to another named map, like you said, would be a good way to define these.

I have to say that the only things that shouldn't be allowed as variables are discreet colors. Considering all colors are part of a theme, it might be a good practice to never allow SCSS variables to have a discreet color value (except maybe the variables that refer to the colors themselves, like $blue). Other variables, like spacings or transforms are not affected, because they can be considered constant regardless of theme.


As for me, unfortunately, I have to move away from using Bootstrap in my current project, because I think it's going to be extremely hard to change the current behaviors, even in v6. Some places use the $primary variable directly or indirectly ($component-active-bg is set to $primary as well). Whatever uses these variables or depend on them, cannot be themed. What's even more problematic, is that there are places where these values are needed at compile time, so they cannot be refactored easily either:

$form-range-thumb-active-bg:               tint-color($component-active-bg, 70%) !default;

The solution to this would be to refactor all colors INTO the theme declaration. Obviously, that would make the theme definition very large. Hence, the defaults should be inferred as much as possible and refer to a limited set of colors in the theme (or prefer the use of opacity where possible).

I tried to fix these issues at usercode level, but it's simply not feasible. I started fixing it at the framework level (inside Bootstrap's SCSS), but it's too woven into the fabric that it is a huge undertaking that it's simply not viable for me to invest another two weeks of my time. Because of this, I have to cut my losses here and move on, but before that I wanted to open a few issues so that you guys at Bootstrap can consider moving in this direction if you feel it aligns with your goals.

@Karl1b

This comment was marked as off-topic.

@mdo
Copy link
Member

mdo commented Feb 8, 2023

@Karl1b Yes, that's been the intention with our migration to CSS variables, but we're not 100% there due to technical issues and concerns around breaking changes.

@mdo mdo added the v6 label Apr 4, 2023
@Banner-Keith
Copy link

I personally think that $primary, $secondary, $success, $info, $warning, and $danger are essential to the way bootstrap has been built for a long time. Changing the names of these common variables to something like accent-1 and error does nothing to change the functionality so it's a breaking change in the way people have used bootstrap for no gain. There has to be a better reason to change naming than just that you like it better.

Also, removing secondary is a bad idea too. It's not just another gray. It's gray in the documentation site but not everyone uses it that way.

I really do like the idea of having a map of themes. It's a great idea. But I think we should decouple the renaming of variables from that idea as they are really unrelated. Adding the maps, even with no other changes would add value. That can stand on its own merit without anything else IMO.

@nkostadinov
Copy link

Can you just separate all color related classes in a separate SCSS files so BS can be easily theme-able. It is still really hard to add themes to bootstrap even in v5. I use this pattern in our product and it is working pretty good. The only hard thing is to "stop" BS colors :) .

@Banner-Keith
Copy link

Banner-Keith commented Oct 16, 2023

nkostadinov

It's pretty easy to change the theme colors. You can just Add your own theme variables and then @import the main bootstrap scss file, or you can do

@use "bootstrap" with (
    $primary: #2c3e50,
    $secondary: #b4bcc2,
    $success: #18bc9c,
    $info: #3498db,
    $warning: #f39c12,
    $danger: #e74c3c
);

@nkostadinov
Copy link

nkostadinov

It's pretty easy to change the theme colors. You can just Add your own theme variables and then @import the main bootstrap scss file, or you can do

@use "bootstrap" with (
    $primary: #2c3e50,
    $secondary: #b4bcc2,
    $success: #18bc9c,
    $info: #3498db,
    $warning: #f39c12,
    $danger: #e74c3c
);

This way you will generate all the CSS for each theme instead just the color difference. And that's a lot of duplicate css.

@Banner-Keith
Copy link

@nkostadinov this is true, but it's the only way with how this is all set up right now. A separate css file for themes can only be done when everything is switched to css variables. Which is an ongoing effort. For now this is the workaround. I will be very happy when the theme colors are no longer scss variables passed around everywhere.

@nkostadinov
Copy link

@nkostadinov this is true, but it's the only way with how this is all set up right now. A separate css file for themes can only be done when everything is switched to css variables. Which is an ongoing effort. For now this is the workaround. I will be very happy when the theme colors are no longer scss variables passed around everywhere.

css variables have nothing to do with it. I mean one scss containing all positioning properties and one for colors e.g. :

butons.scss:

.btn {
 padding: ...
margin: ...
}

The define the colors and include scss containing these colors.

themes.scss

.theme_dark {
  @import "colors/dark";
  @import "themes/buttons.scss";
}

themes/buttons.scss

.btn-primary {
  background-color: $primary; // defined in themes/colors/dark.scss
}

This way you can have unlimited number of themes in themes.scss based on prefix. You can also export different css files for each theme and change them by js.

@Banner-Keith
Copy link

@nkostadinov that's going to result in extra css for each theme for every place a theme is used. The css variables that they are already doing and is the direction they are already heading is a better solution.

You just end up overriding the css variables for the theme instead of having duplicate styles for every class that uses themes. They have already done this for dark mode and it's super simple. Way simpler than having duplicate output css for each class.

In Bootstrap 5.0.2 the use of css variables was minimal and there were 407 selectors using theme colors in the compiled css. Now in 5.3 that's down to 152 selectors using theme colors. The resulting file is also significantly smaller due to that change.

I'd much rather see the bootstrap team keep heading in the direction of switching to css variables than get sidetracked by reorganizing the scss when the css variables will provide a much better solution in the long term.

You'll be able to just create one css file for your theme like this:

[data-bs-theme="light"] {
  --bs-primary: #0d6efd;
  --bs-secondary: #6c757d;
  --bs-success: #198754;
  --bs-info: #0dcaf0;
  --bs-warning: #ffc107;
  --bs-danger: #dc3545;
  --bs-light: #f8f9fa;
  --bs-dark: #212529;
  /* Light overrides */
}
[data-bs-theme="dark"] {
/* Dark overrides */
}

And then you import that file after the bootstrap minified css.

@extrabright
Copy link

extrabright commented Nov 6, 2023

I am working on making themes easier to use, while transitioning to v6: https://learn.webpixels.io/themes My focus is to create multiple themes with light/dark mode support:

// Functions, variables, maps, and mixins
@import "@webpixels/css/core";

// Variables
@import "blue/variables";

// Mixins
@import "blue/light";
@import "blue/dark";
@import "blue/styles";

.theme-blue {
  @include blue-styles;
}

[data-bs-theme="light"],
:root:not([data-bs-theme="dark"]) {
  .theme-blue {
    @include blue-light;
  }
}

// Dark theme (System)
// Automatically enabled if user's prefered scheme is dark

@media only screen and (prefers-color-scheme: dark) {
  :root:not([data-bs-theme]) {
    .theme-blue {
      @include blue-dark;
    }
  }
}

// Dark theme (Manual)
// Enabled if the data-bs-theme="dark" is added
[data-bs-theme="dark"] {
  .theme-blue {
    @include blue-dark;
  }
}

The dark mode can be activated based on user's preferred scheme, or manually using the existing data-bs-theme="dark". You can use them in your Sass or simply drop the CDN link directly in your HTML. Just published the new docs (still in beta).

<!doctype html>
<html lang="en" data-bs-theme="light">
  <body class="theme-blue">
    ...
  </body>
</html>

You can see the entire code here in this repo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants