Skip to content

Instantly share code, notes, and snippets.

@tylerpaige
Last active November 23, 2021 19:14
Show Gist options
  • Save tylerpaige/da9738d35f9c2214d8a2a7848eee6da1 to your computer and use it in GitHub Desktop.
Save tylerpaige/da9738d35f9c2214d8a2a7848eee6da1 to your computer and use it in GitHub Desktop.
/*
::::::::::::::::::::
:: REQUIRED SETUP ::
::::::::::::::::::::
At a minimum, you'll want a $themes object that contains all of
your presets. It should be a nested map.
1. The first layer should be the names of your "styles"
(e.g. "outline", "semi-transparent-fill", "fill").
2. The second layer inside each style should be the available
color schemes (e.g. "red", "green", "white")
3. The third layer inside each color scheme should be all of
the individual interactive states. These must be:
inert, hover, focus, and disabled.
4. The fourth layer should finally be all of property declarations.
For the most part, these will be colors, but you could hypothetically
store any themed configuration data there
$themes: (
style: (
color-scheme: (
inert: (),
hover: (),
focus: (),
disabled: (),
)
)
)
It should be notes, however, that this structure can be adapted whatever you need.
The second layer doesn't need to be about colors. That's just the convention we use.
You can make these nested configurations whatever you want, and the functions/mixins should work.
*/
/*
:::::::::::::::::::
:: EXAMPLE SETUP ::
:::::::::::::::::::
*/
// colors
$black: #111111;
$grey: #999999;
$white: #ffffff;
$cyan: cyan;
$green: limegreen;
$yellow: yellow;
$magenta: magenta;
$themes: (
"outline": (
"black": (
"inert": (
border-color: black,
background-color: transparent,
text-color: black,
),
"hover": (
background-color: $green,
),
"focus": (
background-color: $green,
),
"disabled": (
border-color: $grey,
text-color: $grey,
),
),
),
"fill": (
"black": (
"inert": (
border-color: black,
background-color: black,
text-color: white,
),
"hover": (
border-color: black,
background-color: black,
text-color: white,
),
"focus": (
border-color: black,
background-color: black,
text-color: white,
),
"disabled": (
border-color: black,
background-color: black,
text-color: white,
),
),
),
);
// Instead of having to keep track of a massive $themes object, this
// helper will let you add to it in discrete chunks
@mixin add-to-themes(
$style,
$color,
$inert,
$hover: null,
$focus: null,
$disabled: null
) {
$obj: (inert: $inert);
@if $hover {
$obj: map-merge($obj, ("hover": $hover));
}
@if $focus {
$obj: map-merge($obj, ("focus": $focus));
}
@if $disabled {
$obj: map-merge($obj, ("disabled": $disabled));
}
$existing-color-schemes: ();
@if map-has-key($themes, $style) {
$existing-color-schemes: map-get($themes, "#{$style}");
}
$updated-color-schemes: map-merge($existing-color-schemes, ("#{$color}": $obj));
$theme-object-update: ("#{$style}": $updated-color-schemes);
$themes: map-merge($themes, $theme-object-update) !global;
}
@include add-to-themes(
"outline",
"cyan",
$inert: (border-color: $cyan, background-color: transparent, text-color: $black),
$hover: (border-color: $cyan, background-color: $cyan, text-color: $black),
$focus: (border-color: $cyan, background-color: $cyan, text-color: $black),
$disabled: (border-color: $cyan, background-color: transparent, text-color: $grey)
);
@include add-to-themes(
"semi-transparent-fill",
"black",
$inert: (border-color: transparent, background-color: transaprentize($black, 0.5), text-color: $black),
$hover: (border-color: transparent, background-color: $black, text-color: $white),
$focus: (border-color: transparent, background-color: $black, text-color: $white),
$disabled: (border-color: transparent, background-color: transaprentize($black, 0.5), text-color: $black)
);
/*
::::::::::::::::::::::::::::::::
:: COLOR FUNCTIONS AND MIXINS ::
::::::::::::::::::::::::::::::::
*/
// Returns the themed preset for a given style, color scheme, and interactive state.
// Returns the preset as a map
@function get-theme-colors($style, $color-scheme, $state: "inert") {
@if not(map-has-key($themes, $style)) {
@warn 'No style called #{$style} found in the $themes object. Available styles: #{map-keys($themes)}';
@return "";
}
$style-preset: map-get($themes, $style);
@if not(map-has-key($style-preset, $color-scheme)) {
@warn 'No color scheme called #{$color-scheme} found for the #{$style} style. Available color schemes: #{map-keys($style-preset)}';
@return "";
}
$color-preset: map-get($style-preset, $color-scheme);
@if not(map-has-key($color-preset, $state)) {
@warn 'No state preset called #{$state} found for the #{$style} style in the #{$color-scheme} color scheme.';
@return "";
}
@return map-get($color-preset, $state);
}
// Gets the themed preset for a given style, color scheme, and interactive state.
// For each property in the preset, create a CSS variable
@mixin set-theme-colors($style, $color-scheme, $state: inert) {
$color: get-theme-colors($style, $color-scheme, $state);
@if $color {
@each $property-name, $property-value in $color {
--#{$property-name}: #{$property-value};
}
}
}
// Maps specific interactive states to the appropriate pseudoselectors
// Used by the `set-interactive-theme-colors` mixin
$interactive-pseudoselectors: (
"inert": "",
"hover": ":hover:not(:disabled)",
"focus": ":focus",
"disabled": ":disabled",
);
// Gets the themed preset for a given style and color scheme.
// For each available interactive state in the preset,
// create a styling block that will set CSS variables for each
// property in the preset.
@mixin set-interactive-theme-colors(
$style,
$color-scheme,
$states: ("inert", "hover", "focus", "disabled"),
$selector: &
) {
@each $state in $states {
$pseudoselector: map-get($interactive-pseudoselectors, $state);
$color: get-theme-colors($style, $color-scheme, $state);
@if $color {
@at-root {
#{$selector}#{$pseudoselector} {
@each $property-name, $property-value in $color {
--#{$property-name}: #{$property-value};
}
}
}
}
}
}
// Given a string, this mixin will create a CSS block for each
// style, color scheme, and interactive state in the $themes config object.
@mixin create-themed-classnames($component-name) {
@each $style-name, $style-colors in $themes {
@each $color-name, $color-scheme in $style-colors {
$class-name: ".#{$component-name}--#{$style-name}--#{$color-name}";
@include set-interactive-theme-colors(
$style-name,
$color-name,
$selector: $class-name
);
}
}
}
/*
::::::::::::::
:: EXAMPLES ::
::::::::::::::
*/
// Firstly, style your component and make use of CSS variables
.component {
border: 1px solid var(--border-color);
border-radius: 3px;
background-color: var(--background-color);
color: var(--text-color);
padding: 1em;
text-align: center;
}
// At it's most basic, you can set up the component's default colors
// .component {
// @include set-theme-colors("outline", "black", "inert");
// }
// Or you could build out all of the interactive styles
.component {
@include set-interactive-theme-colors("outline", "black");
}
// You can do this with other color schemes too
// .component--outline--cyan {
// @include set-theme-colors("outline", "cyan", "inert");
// }
// Using other color schemes works with interactive styles, too
.component--outline--cyan {
@include set-interactive-theme-colors("outline", "cyan");
}
// Feel free to mix and match
.super-component .component {
@include set-theme-colors("outline", "black");
&:hover {
@include set-theme-colors("outline", "cyan", "hover");
}
&:focus {
@include set-theme-colors("fill", "black", "focus");
}
}
// If you want your component to automatically receive class names
// for each available style, color scheme, and interactive state
// (e.g. .component--outline--black, .component--outline--black:hover, .component--fill--black, etc etc)
// this mixin will create them for you.
// @include create-themed-classnames("component");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment