mayank99 / open-props-scss

open-props as sass variables
MIT License
22 stars 2 forks source link

dark vs light shadows #2

Closed mayank99 closed 1 year ago

mayank99 commented 1 year ago

currently only the light theme shadows are included. need to find a way to deliver dark theme shadows without media queries.

also could be useful to customize shadow-color and shadow-strength at build time.

WebMechanic commented 1 year ago

What's wrong with two (or more) small separate files that only contain the colours for light and dark theme?

It seems like people have forgotten that you can use media queries in link elements. Typical theme files will be small (and one can even stick to CSS props here). Only the active theme is applied and the other ones cached by the browser. Instantly available should the user switch.


<link rel="stylesheet" href="light.css" media="screen and (prefers-color-scheme: light)" />
<link rel="stylesheet" href="dark.css" media="screen and (prefers-color-scheme: dark)" />

<link rel="stylesheet" href="contrasty.css" media="screen and (prefers-contrast: more)" />
<link rel="stylesheet" href="dimmed.css" media="screen and (prefers-contrast: less)" />
<link rel="stylesheet" href="animations.css" media="screen and (prefers-reduced-motion: no-preference)" />

<link rel="stylesheet" href="monolothic-monster.css" media="screen" />
`` `
This looks like the browser would load six stylesheets, but it initially only loads two for theme + site, then defers loading all others of they don't apply. This simple setup might cause a 3rd css to be loaded if one cares to go to the length creating them in the first place.

If one's terrified of possible additional request (but serving gigs of hires images at the same time), each small stylesheet can be inlined into the page. The media query then goes to the style element instead.

Works for me since media queries were invented.
mayank99 commented 1 year ago

well, the first step is to split into separate files (#3). how to handle light vs dark can be explored after that.

what you're suggesting re: multiple stylesheets is large irrelevant here, since it's upto the user how they want to split their css in the final bundle.

woodcox commented 1 year ago

Have you noticed that borders also have the --radius-1, --radius-2, etc css variables in the $radius-conditional-1 to 6.

For example:

$radius-conditional-1: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-1));

Any ideas on how to deal with the few css variables within a sass variable that exist within the open props animations, borders and shadows modules? I think its best to deal with them programatically in case there are more in open props in the future.

mayank99 commented 1 year ago

@woodcox Oh wow, good catch. But I think it should be straightforward to replace var(--radius-1) with $radius-1, etc.

The dark vs light shadows are a bit more tricky. Maybe we could expose -light and -dark variables separately. 🤔

woodcox commented 1 year ago

I've already done the work to convert var(--radius-1) to $radius-1 and the easings in the animations module with:

if (typeof value === 'string' && value.includes('var(--')) {
        value = value.replace(/var\(--(.*?)\)/g, '#{$$$1}'); // replace var(--cssvar) with #{$cssvar} when they occurs in a value.
}

Yeah, the shadows module is an issue, as shadows uses hsl in '--shadow-color: 220 3% 15%;'. When this is converted to a sass variable sass throws an error because the sass variable is not split into $hue, $saturation and $lightness. I was wondering if these should be kept as css variables so they remain dynamic?

If so, the minimum outputted css would be (if the shadows module is included):

:where(html) {
  --shadow-color: 220 3% 15%;
  --shadow-strength: 1%;
}

@media (prefers-color-scheme: dark) {
  :where(html) {
    --shadow-color: 220 40% 2%;
    --shadow-strength: 25%;
  }
}

I think that is acceptable as css variables are particularly useful with colors. But if you want to include further Open Props modules in the future such as the okch color modules and use the same approach, the minimum css output will get larger. But as its modular the output can be controlled.

mayank99 commented 1 year ago

The space-separated hsl syntax error can be fixed by using Hsl instead of hsl. For some reason, Sass's own hsl function is not smart enough.

But I'm still not sure about keeping any CSS variables. Also, using media queries is not enough, because in some cases you might want to use the dark shadow even when the OS theme is light (and vice-versa).

After #6 gets merged, it should be possible to make use of !default to allow controlling the shadow color and strength. So the usage could look something like:

@use 'open-props-scss/shadows-light.scss' with (
  $shadow-strength: 5%
);
woodcox commented 1 year ago

I've been thinking about your suggestion of overriding the !defaults. What if there was a small variables.scss file with:

$op-shadows: (
  'light': (
    'shadow-color': 220 3% 15%,
    'shadow-strength': 1%
  ),  
  'dark': (
    'shadow-color': 220 40% 2%,
    'shadow-strength': 25%
  )
) !default;

Then they could be overridden in the same way. Then could also add a couple of functions such as:

@function get-shadow($theme, $key) {
  $result: map.get(_v.$op-shadows, $theme, $key);

  @if $result {
    @return $result;
  }
  @warn #{'Open props "' + $key + '" not found in $op-shadows for theme "' + $theme + '"'};

  @return null;
}

@function mix-hsl($args...) {
  @return #{'hsl(#{$args})'};
}

@function mix-hsla($args...) {
  @return mix-hsl($args...);
}

This creates the flexibility you were looking for where the media query suggestion was lacking. So the following could be achieved:

@use '../index' as op;

$shdw-color: op.get-shadow('light', 'shadow-color');
$shdw-str: op.get-shadow('light', 'shadow-strength');

html {
  background: op.mix-hsl($shdw-color $shdw-str);
}

Which outputs the following css:

html {
  background: hsl(220 3% 15% 1%);
}

The mix-hsl function proved to be necessary to mix two scss variables together.

mayank99 commented 1 year ago

what would be the benefit of your suggestion vs something simpler like this?

// shadows.scss

$theme: light !default;
$shadow-color:  220 3% 15% !default;
$shadow-strength: 1% !default;

@if $theme == dark {
  $shadow-color:  220 40% 2% !default;
  $shadow-strength: 25% !default;
}

$shadow-1: 0 1px 2px -1px hsl($shadow-color / calc($shadow-strength + 9%));
$shadow-2: // ...
@use 'open-props-scss/shadows.scss' with ($theme: dark) as shadows-dark;
woodcox commented 1 year ago

I feel like I am missing something but with those !defaults you could only ever access the light theme or the dark theme from the stylesheet.

Whereas, if the $shadows-[1-6] still had --shadow-strength and --shadow-color internally you could create a light and dark theme in the stylesheet.

For example:

// shadows.scss
$shadow-1: 0 1px 2px -1px hsl(--shadow-color / calc(--shadow-strength + 9%));
$shadow-2: // ...

$shadows-map: (
  'light': (
    'shadow-color': 220 3% 15%,
    'shadow-strength': 1%
  ),  
  'dark': (
    'shadow-color': 220 40% 2%,
    'shadow-strength': 25%
  )
) !default;

Then in the your root stylesheet you have the ability to create:

// style.scss
@use 'open-props-scss' as op;

$shadow-color-light: op.get-theme('light', 'shadow-color');
$shadow-str-light: op.get-theme('light', 'shadow-strength');

$shadow-color-dark: op.get-theme('dark', 'shadow-color');
$shadow-str-dark: op.get-theme('dark', 'shadow-strength');

:root {
  --shadow-color: #{$shadow-color-light};
  --shadow-strength: #{$shadow-str-light};
  // ...
}

@media (prefers-color-scheme: dark) {
 :root {
   --shadow-color: #{$shadow-color-dark};
   --shadow-strength: #{$shadow-str-dark};
    // ...
  }
}

May be this is too complex? But I was thinking that a sass map was the only way to access both themes. I realise we are only talking about two props - --shadow-strength and --shadow-color. So there are much simpler options to create light and dark themes. But I was thinking about, what if you wanted to add some of the extras from Open Props such as theme.light.css and theme.dark.css?

woodcox commented 1 year ago

What if we did both to account for a simple use case and a complex one? So the shadows.scss file could be:

// shadows.scss

$theme: light !default;
$shadow-color:  220 3% 15% !default;
$shadow-strength: 1% !default;

@if $theme == dark {
  $shadow-color:  220 40% 2% !default;
  $shadow-strength: 25% !default;
}

$shadow-1: 0 1px 2px -1px hsl($shadow-color / calc($shadow-strength + 9%));
$shadow-2: // ...

$shadows-map: (
  'light': (
    'shadow-color': 220 3% 15%,
    'shadow-strength': 1%
  ),  
  'dark': (
    'shadow-color': 220 40% 2%,
    'shadow-strength': 25%
  ),
  'cssvar': (
    'shadow-1': 0 1px 2px -1px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 9%)),
    'shadow-2': // ...
  )
) !default;
mayank99 commented 1 year ago

You're right, it's not enough. I was thinking it would be possible to import the module twice under different names, but of course it doesn't work.

@use 'open-props-scss/shadows.scss' as shadows-light;
@use 'open-props-scss/shadows.scss' as shadows-dark with ($theme: dark);

It seems like the sass docs recommend using mixins for configuration and functions for returning values. https://sass-lang.com/documentation/at-rules/use#with-mixins

Something like this could work:

// shadows.scss

$-shadow-color: null;
$-shadow-strength: null;

@mixin configure($shadow-color: null, $shadow-strength: null) {
 $-shadow-color: $shadow-color !global;
 $-shadow-strength: $shadow-strength !global;
}

@function shadow($level, $theme: light) {
 $shadow-color: $-shadow-color or if($theme == dark, 220 40% 2%, 220 3% 15%);
 $shadow-strength: $-shadow-strength or if($theme == dark, 25%, 1%); 

 $shadows: (
  1: 0 1px 2px -1px Hsl($shadow-color / calc($shadow-strength + 9%)),
  2: // ...
 );

 @return map.get($shadows, $level);
}

Basic usage:

@use 'open-props-scss' as op;

.my-thing {
  box-shadow: $op.shadow(2, dark);
}

Advanced usage with full flexibility:

@use 'open-props-scss/shadows.scss';

@include shadows.configure(
  $shadow-color: var(--my-shadow-color-hsl),
  $shadow-strength: 10%
);

:root {
  --my-shadow-color-hsl: 0 0% 0%;
}

.my-thing {
  box-shadow: $shadows.shadow(2);
}
woodcox commented 1 year ago

That function and mixin is sweet! It's stuff like this which makes me think I won't be moving away from sass any time soon! The only thing I think I'd change is the name of the mixin to:

// I'm open to other suggestions, I just thought `configure` was too generic.

@mixin custom-shadow($shadow-color: null, $shadow-strength: null) {
  $-shadow-color: $shadow-color !global;
  $-shadow-strength: $shadow-strength !global;
}

And calling the map $shadows-map. I hope you don't mine me documenting the mixin and function.

So just to clarify the shadows.scss should look like the following:


// shadows.scss

$-shadow-color: null;
$-shadow-strength: null;

/// CUSTOM-SHADOW MIXIN
/// Configures the global variables for $-shadow-color and $-shadow-strength.
/// Allows overriding the default values with custom values.
///
/// @param {hsl color} $shadow-color - The custom shadow color value. Defaults to null.
/// @param {Percentage} $shadow-strength - The custom shadow strength value. Defaults to null.

@mixin custom-shadow($shadow-color: null, $shadow-strength: null) {
  $-shadow-color: $shadow-color !global;
  $-shadow-strength: $shadow-strength !global;
}

/// SHADOW FUNCTION
/// Generates a shadow value based on the provided level and theme.
///
/// @param {Number or String} $level - The level of the shadow.
/// @param {String} $theme - The theme ('light' or 'dark'). Defaults to 'light'.
/// @returns {List} - The shadow value for the specified level and theme.
/// It uses the global variables $-shadow-color and $-shadow-strength to determine the shadow color and strength. 
/// If the global variables are not set, it falls back to default values based on the 'light' or 'dark' theme

@function shadow($level, $theme: light) {
  $shadow-color: $-shadow-color or if($theme == dark, 220 40% 2%, 220 3% 15%);
  $shadow-strength: $-shadow-strength or if($theme == dark, 25%, 1%);

  // Must wrap the values in parentheses ()
  // See https://stackoverflow.com/questions/51792566/comma-inside-scss-map-value-box-shadow
  $shadows-map: (
    1: (0 1px 2px -1px Hsl($shadow-color / calc($shadow-strength + 9%))),
    2: (0 3px 5px -2px Hsl($shadow-color / calc($shadow-strength + 3%)), 0 7px 14px -5px Hsl($shadow-color / calc($shadow-strength + 5%))),
    3: (0 -1px 3px 0 Hsl($shadow-color / calc($shadow-strength + 2%)), 0 1px 2px -5px Hsl($shadow-color / calc($shadow-strength + 2%)), 0 2px 5px -5px Hsl($shadow-color / calc($shadow-strength + 4%)), 0 4px 12px -5px Hsl($shadow-color / calc($shadow-strength + 5%)), 0 12px 15px -5px Hsl($shadow-color / calc($shadow-strength + 7%))),
    4: (0 -2px 5px 0 Hsl($shadow-color / calc($shadow-strength + 2%)), 0 1px 1px -2px Hsl($shadow-color / calc($shadow-strength + 3%)), 0 2px 2px -2px Hsl($shadow-color / calc($shadow-strength + 3%)), 0 5px 5px -2px Hsl($shadow-color / calc($shadow-strength + 4%)), 0 9px 9px -2px Hsl($shadow-color / calc($shadow-strength + 5%)), 0 16px 16px -2px Hsl($shadow-color / calc($shadow-strength + 6%))),
    5: (0 -1px 2px 0 Hsl($shadow-color / calc($shadow-strength + 2%)), 0 2px 1px -2px Hsl($shadow-color / calc($shadow-strength + 3%)), 0 5px 5px -2px Hsl($shadow-color / calc($shadow-strength + 3%)), 0 10px 10px -2px Hsl($shadow-color / calc($shadow-strength + 4%)), 0 20px 20px -2px Hsl($shadow-color / calc($shadow-strength + 5%)), 0 40px 40px -2px Hsl($shadow-color / calc($shadow-strength + 7%))),
    6: (0 -1px 2px 0 Hsl($shadow-color / calc($shadow-strength + 2%)), 0 3px 2px -2px Hsl($shadow-color / calc($shadow-strength + 3%)), 0 7px 5px -2px Hsl($shadow-color / calc($shadow-strength + 3%)), 0 12px 10px -2px Hsl($shadow-color / calc($shadow-strength + 4%)), 0 22px 18px -2px Hsl($shadow-color / calc($shadow-strength + 5%)), 0 41px 33px -2px Hsl($shadow-color / calc($shadow-strength + 6%)), 0 100px 80px -2px Hsl($shadow-color / calc($shadow-strength + 7%))),
    'inner-0': (inset 0 0 0 1px Hsl($shadow-color / calc($shadow-strength + 9%))),
    'inner-1': (inset 0 1px 2px 0 Hsl($shadow-color / calc($shadow-strength + 9%))),
    'inner-2': (inset 0 1px 4px 0 Hsl($shadow-color / calc($shadow-strength + 9%))),
    'inner-3': (inset 0 2px 8px 0 Hsl($shadow-color / calc($shadow-strength + 9%))),
    'inner-4': (inset 0 2px 14px 0 Hsl($shadow-color / calc($shadow-strength + 9%)))
  );

  @return map.get($shadows-map, $level);
}
mayank99 commented 1 year ago

As I was trying to think of the name, I actually became unsure if we even need a mixin. Those arguments could just be passed in the same shadow() function.

@function shadow($level, $theme: light, $shadow-color: null, $shadow-strength: null) {

However, the rest looks good if you want to open a PR 👀