/*
¶ ↑
Functions
*/
// The following vars need to be set // here, before the rest of the system // variables are set
$root-font-size: if($theme-respect-user-font-size, 100%, $theme-root-font-size);
$root-font-size-equiv: if(
$theme-respect-user-font-size, 16px, $theme-root-font-size
);
/*
¶ ↑
General-purpose functions
*/
/*
uswds-error()
Allow the system to pass an error as text to test error states in unit testing
*/
$_error-output-override: false !default; @function uswds-error($message, $override: $_error-output-override) {
@if $override { @return "Error: #{$message}"; } @error "#{$message}";
}
/*
error-not-token()
Returns a common not-a-token error.
*/
@function error-not-token($token, $type, $valid-token-map: false) {
$valid-token-message: if( $valid-token-map, " Valid tokens: #{map-keys($valid-token-map)}", "" ); @return uswds-error( "'#{$token}' is not a valid USWDS #{$type} token.#{$valid-token-message}" );
}
/*
map-deep-get()
@author Hugo Giraudel @access public @param {Map} $map - Map @param {Arglist} $keys - Key chain @return {*} - Desired value
*/
@function map-deep-get($map, $keys…) {
@each $key in $keys { $map: map-get($map, $key); } @return $map;
}
/*
strip-unit()
Remove the unit of a length @author Hugo Giraudel @param {Number} $number - Number to remove unit from @return {Number} - Unitless number
*/
@function strip-unit($number) {
@if type-of($number) == "number" and not unitless($number) { @return $number / ($number * 0 + 1); } @return $number;
}
/*
multi-cat()
Concatenate two lists
*/
@function multi-cat($list1, $list2) {
$this-list: (); @each $e in $list1 { @each $ee in $list2 { $this-block: $e + $ee; $this-list: join($this-list, $this-block); } } @return $this-list;
}
/*
map-collect()
Collect multiple maps into a single large map source: gist.github.com/bigglesrocks/d75091700f8f2be5abfe
*/
@function map-collect($maps…) {
$collection: (); @each $map in $maps { $collection: map-merge($collection, $map); } @return $collection;
}
/*
smart-quote()
Quotes strings Inspects `px`, `xs`, and `xl` numbers Leaves bools as is
*/
@function smart-quote($value) {
@if type-of($value) == "string" { @return quote($value); } @if type-of($value) == "number" and index(("px", "xl", "xs"), unit($value)) { @return inspect($value); } @if type-of($value) == "color" { @error 'Only use quoted color tokens in USWDS functions and mixins. ' + 'See designsystem.digital.gov/design-tokens/color ' + 'for more information.'; } @return $value;
}
/*
remove()
Remove a value from a list
*/
@function remove($list, $value, $recursive: false) {
$result: (); @for $i from 1 through length($list) { @if type-of(nth($list, $i)) == list and $recursive { $result: append($result, remove(nth($list, $i), $value, $recursive)); } @else if nth($list, $i) != $value { $result: append($result, nth($list, $i)); } } @return $result;
}
/*
strunquote()
Unquote a string
*/
@function strunquote($value) {
@if type-of($value) == "string" { $value: unquote($value); } @return $value;
}
/*
to-map()
Convert a single value to a USWDS value map.
Candidate for deprecation if we remove isReadable
*/
@function to-map($key, $values) {
$l: length($values); @if $key == "noModifier" or $key == "noValue" { $key: ""; } @return (slug: $key, content: $values);
}
/*
base-to-map()
Convert a single base to a USWDS value map.
Candidate for deprecation if we remove isReadable
*/
@function base-to-map($values) {
$l: length($values); @if $l == 1 or nth($values, $l) != isReadable { @return (slug: $values, isReadable: true); } @else { $values: remove($values, isReadable); @return (slug: unquote(nth($values, 1)), isReadable: true); }
}
/*
ns()
Add a namesspace of $type if that namespace is set to output
*/
@function ns($type) {
$type: smart-quote($type); @if not map-deep-get($theme-namespace, $type, output) { @return ""; } @return map-deep-get($theme-namespace, $type, namespace);
}
/*
de-list()
Transform a one-element list or arglist into that single element.
(1) => 1 ((1)) => (1)
*/
@function de-list($value) {
$types: ("list", "arglist"); @if not index($types, type-of($value)) { @return $value; } $output: if(length($value) == 1, nth($value, 1), $value); @return $output;
}
/*
unpack()
Create lists of single items from lists of lists.
(1, (2.1, 2.2), 3) –> (1, 2.1, 2.2, 3)
*/
@function unpack($value) {
$output: (); @if length($value) == 0 { @return $value; } @each $i in $value { @if type-of($i) == "list" { @each $ii in $i { $output: append($output, $ii, comma); } } @else { $output: append($output, $i, comma); } } @return de-list($output);
}
/*
get-last()
Return the last item of a list, Return null if the value is null
*/
@function get-last($props) {
$length: length($props); $last: if($length == 0, null, nth($props, -1)); @return $last;
}
/*
has-important()
Check to see if `!important` is being passed in a mixin's props
*/
@function has-important($props) {
$props: de-list($props); @if get-last($props) == "!important" { @return true; } @return false;
}
/*
append-important()
Append `!important` to a list
*/
@function append-important($source, $destination) {
@if get-last($source) == "!important" { @return append($destination, !important, comma); } @return $destination;
}
/*
spacing-multiple()
Converts a spacing unit multiple into the desired final units (currently rem)
*/
@function spacing-multiple($unit) {
$grid-to-rem: ($system-spacing-grid-base * $unit) / $root-font-size-equiv * 1rem; @return $grid-to-rem;
}
/*
rem-to-px()
Converts a value in rem to a value in px
*/
@function rem-to-px($value-in-rem) {
@if unit($value-in-rem) == "rem" { $rem-to-px: ($value-in-rem / 1rem) * $root-font-size-equiv; @return $rem-to-px; } @if unit($value-in-rem) != "px" { @error 'This value must be in either px or rem'; } @return $value-in-rem;
}
/*
rem-to-user-em()
Converts a value in rem to a value in
- user-settings
-
em for use in media
queries
*/
@function rem-to-user-em($grid-in-rem) {
$rem-to-user-em: ($grid-in-rem / 1rem) * 1em; @return $rem-to-user-em;
}
/*
validate-typeface-token()
Check to see if a typeface-token exists. Throw an error if a passed token does not exist in the typeface-token map.
*/
@function validate-typeface-token($typeface-token) {
@if not map-has-key($all-typeface-tokens, $typeface-token) { @return error-not-token($typeface-token, "typeface", $all-typeface-tokens); } @return $typeface-token;
}
/*
cap-height()
Get the cap height of a valid typeface
*/
@function cap-height($typeface-token) {
@if not $typeface-token { @return false; } $typeface-token: validate-typeface-token($typeface-token); $token-data: map-get($all-typeface-tokens, $typeface-token); @return map-get($token-data, "cap-height");
}
/*
px-to-rem()
Converts a value in px to a value in rem
*/
@function px-to-rem($pixels) {
@if not $pixels { @return false; } $px-to-rem: ($pixels / $root-font-size-equiv) * 1rem; $px-to-rem: round($px-to-rem * 100) / 100; @return $px-to-rem;
}
/*
normalize-type-scale()
Normalizes a specific face's optical size to a set target
*/
@function normalize-type-scale($cap-height, $scale) {
@if not $cap-height { @return false; } $this-scale: $system-base-cap-height * strip-unit($scale) / $cap-height * 1px; @return px-to-rem($this-scale);
}
/*
utility-font()
Get a normalized font-size in rem from a family and a type size in either system scale or project scale
Not the public-facing function. Used for building the utilities and withholds certain errors.
*/
@function utility-font($family, $scale) {
@if not map-has-key($project-cap-heights, $family) { @return error-not-token($family, "font family", $project-cap-heights); } $quote-scale: smart-quote($scale); @if not map-get($all-type-scale, $quote-scale) { @return error-not-token($scale, "font scale", $all-type-scale); } $this-cap: map-get($project-cap-heights, $family); $this-scale: map-get($all-type-scale, $quote-scale); @if not $this-scale and $this-cap { @return false; } @return normalize-type-scale($this-cap, $this-scale);
}
/*
line-height() lh()
Get a normalized line-height from a family and a line-height scale unit
*/
@function lh($props…) {
$props: unpack($props); @if not(length($props) == 2) { @error 'lh() needs both a valid face and line height token ' + 'in the format `lh(FACE, HEIGHT)`.'; } $family: smart-quote(nth($props, 1)); $scale: smart-quote(nth($props, 2)); @if not map-has-key($project-cap-heights, $family) { @return error-not-token($family, "font family", $project-cap-heights); } @if not map-get($system-line-height, $scale) { @return error-not-token($scale, "line-height", $system-line-height); } @if not map-get($project-cap-heights, $family) { @return false; } $this-cap: map-get($project-cap-heights, $family); $this-line-height: map-get($system-line-height, $scale); $normalized-line-height: $this-line-height / ($system-base-cap-height / $this-cap); $normalized-line-height: round($normalized-line-height * 10) / 10; @return $normalized-line-height;
}
@function line-height($props…) {
@return lh($props...);
}
/*
convert-to-font-type()
Converts a font-role token into a font-type token. Leaves font-type tokens unchanged.
*/
@function convert-to-font-type($token) {
@if map-has-key($project-font-role-tokens, $token) { @return map-get($project-font-role-tokens, $token); } @return $token;
}
/*
get-font-stack()
Get a font stack from a style- or role-based font token.
*/
@function get-font-stack($token) {
// Start by converting to a type token (sans, serif, etc) $type-token: convert-to-font-type($token); $output-display-name: true; $this-stack: null; // Get the font type metadata $this-font-map: map-get($project-font-type-tokens, $type-token); // Only output if the font type has an assigned typeface token @if map-get($this-font-map, "typeface-token") { $this-font-token: map-get($this-font-map, "typeface-token"); // Get the typeface metadata $this-typeface-data: map-get($all-typeface-tokens, $this-font-token); $this-name: map-get($this-typeface-data, "display-name"); // If it's a system typeface, don't output the display name @if map-has-key($this-typeface-data, "system-font") { $output-display-name: false; } // If there's a custom stack, use it and output the display name @if map-get($this-font-map, "custom-stack") { $this-stack: map-get($this-font-map, "custom-stack"); $output-display-name: true; } // Otherwise, just get the token's default stack @else { $this-stack: map-deep-get( $all-typeface-tokens, $this-font-token, "stack" ); } // If the typeface has no display name (system fonts), don't output the display name @if map-get($this-typeface-data, "display-name") == null { $output-display-name: false; } @if not $output-display-name { @return #{$this-stack}; } @return unquote("#{$this-name}, #{$this-stack}"); } @return false;
}
/*
get-typeface-token()
Get a typeface token from a font-type or font-role token.
*/
@function get-typeface-token($font-token) {
$this-token: $font-token; @if map-has-key($project-font-role-tokens, $font-token) { $this-token: map-get($project-font-role-tokens, $font-token); } @return map-deep-get( $project-font-type-tokens, $this-token, "typeface-token" );
}
/*
get-system-color()
Derive a system color from its family, value, and vivid or a passed variable that is, itself, a list
*/
@function get-system-color(
$color-family: false, $color-grade: false, $color-variant: false
) {
// If the arg being passed to the fn // is a variable defined as a list, // $color-family will contain this // entire list, and needs to be // unpacked. // ex: // in settings: // $theme-color-primary.'dark': 'blue', 70 // in the theme colors map: // $color-primary-dark: get-system-color($theme-color-primary.'dark'), @if type-of($color-family) == "list" { @if length($color-family) > 2 { $color-variant: nth($color-family, 3); } $color-grade: nth($color-family, 2); $color-family: nth($color-family, 1); } $color-family: smart-quote($color-family); $color-variant: smart-quote($color-variant); // If the arg being passed to the fn // is false, it should output as `false` // to preserve a false value in the // target map // ex: // in settings: // $theme-color-primary.'darkest': false; // in the theme colors map: // 'darkest': get-system-color($theme-color-primary.'darkest'), // 'darkest': false, // is the desired outcome // TODO: should a false-pass color function be a separate fn? @if not $color-family { @return false; } @if $color-variant { $output: map-deep-get( $system-colors, $color-family, $color-variant, $color-grade ); @return $output; } $output: map-deep-get($system-colors, $color-family, $color-grade); @return $output;
}
/*
system-type-scale()
Get a value from the system type scale
*/
@function system-type-scale($scale) {
$scale: smart-quote($scale); @if not $scale { @return false; } @if not map-has-key($system-type-scale, $scale) { @return error-not-token($scale, "type scale", $system-type-scale); } @return map-get($system-type-scale, $scale);
}
/*
calc-gap-offset()
Calculate a valid uswds unit that is half the width of a given unit, for calculating gap offset in the layout grid.
*/
@function calc-gap-offset($gap-size) {
$gap-size: smart-quote($gap-size); @if not map-has-key($spacing-to-value, $gap-size) { @return error-not-token($gap-size, "gap size"); } $numeric-eq: map-get($spacing-to-value, $gap-size); $numeric-eq-half: inspect($numeric-eq / 2); @if not map-has-key($spacing-to-token, $numeric-eq-half) { @error '`#{$gap-size}` is not a valid USWDS gap size token. ' + 'Column gaps need to have a standard size half their width.'; } @return map-get($spacing-to-token, $numeric-eq-half);
}
/*
get-standard-values()
Gets a map of USWDS standard values for a property
*/
@function get-standard-values($property) {
@return map-deep-get($system-properties, $property, standard);
}
/*
number-to-token()
Converts an integer or numeric value into a system value
Ex: 0.5 –> '05'
-1px --> 'neg-1px'
*/
@function number-to-token($number) {
$number: inspect($number); @if not map-has-key($number-to-value, $number) { @return false; } @return map-get($number-to-value, $number);
}
/*
columns()
outputs a grid-col number based on the number of desired columns in the 12-column grid
Ex: columns(2) –> 6
grid-col(columns(2))
*/
@function columns($number) {
$options: "auto", "fill"; $number: smart-quote($number); @if index($options, $number) { @return $number; } @if 12 % $number != 0 { @error '`#{$number}` must be a divisor of 12.'; } $columns: 12 / $number; @return $columns;
}
/*
get-uswds-value()
Finds and outputs a value from the USWDS standard values.
Used to build other standard utility functions and mixins.
*/
@function get-uswds-value($property, $value…) {
@if type-of($value) == "arglist" and nth($value, 1) == override { @return nth($value, 2); } $value: nth($value, 1); $converted: number-to-token($value); $quoted-value: if( $converted, smart-quote($converted), smart-quote(nth($value, 1)) ); $our-standard-values: map-deep-get($system-properties, $property, standard); $our-extended-values: map-deep-get($system-properties, $property, extended); @if map-has-key($our-standard-values, $quoted-value) { $output: map-get($our-standard-values, $quoted-value); @if not $output { @if $theme-show-compile-warnings { @error '`#{$value}` is set as a `false` value ' + 'for the #{$property} property in your project settings ' + 'and will not output properly. ' + 'Set the value of `#{$value}` in project settings.'; } } @return $output; } @if map-has-key($our-extended-values, $quoted-value) { @if $theme-show-compile-warnings { @warn '`#{$value}` is an extended USWDS `#{$property}` token. ' + 'This is OK, but only components built with standard tokens can be accepted back into the system. ' + 'Standard `#{$property}` values: #{map-keys($our-standard-values)}'; } @return map-get($our-extended-values, $quoted-value); } // TODO: what are these last two cases? Evaluate. @if not(type-of($value) == "number" and not unitless($value)) { @return error-not-token($value, $property, $our-standard-values); } @if $theme-show-compile-warnings { @warn '`#{$value}` is not a USWDS `#{$property}` token. ' + 'This is OK, but only components built with standard ' + 'tokens can be accepted back into the system. ' + 'Standard `#{$property}` values: #{map-keys($our-standard-values)}'; } @return $value;
}
/*
pow()
Raises a unitless number to the power of another unitless number
Includes helper functions
*/
@function pow($number, $exponent) {
@if (round($exponent) != $exponent) { @return exp($exponent * ln($number)); } $value: 1; @if $exponent > 0 { @for $i from 1 through $exponent { $value: $value * $number; } } @else if $exponent < 0 { @for $i from 1 through -$exponent { $value: $value / $number; } } @return $value;
}
@function factorial($value) {
$result: 1; @if $value == 0 { @return $result; } @for $index from 1 through $value { $result: $result * $index; } @return $result;
}
@function summation($iteratee, $input, $initial: 0, $limit: 100) {
$sum: 0; @for $index from $initial to $limit { $sum: $sum + call($iteratee, $input, $index); } @return $sum;
}
@function exp-maclaurin($x, $n) {
@return (pow($x, $n) / factorial($n));
}
@function exp($value) {
@return summation(get-function("exp-maclaurin"), $value, 0, 100);
}
@function ln-maclaurin($x, $n) {
@return (pow(-1, $n + 1) / $n) * (pow($x - 1, $n));
}
@function ln($value) {
$ten-exp: 1; $ln-ten: 2.30258509; @while ($value > pow(10, $ten-exp)) { $ten-exp: $ten-exp + 1; } @return summation( get-function("ln-maclaurin"), $value / pow(10, $ten-exp), 1, 100 ) + $ten-exp * $ln-ten;
}
/// Returns the luminance of `$color` as a float (between 0 and 1) /// 1 is pure white, 0 is pure black /// @param {Color} $color - Color /// @return {Number} /// @link www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef Reference @function luminance($color) {
$colors: ( "red": red($color), "green": green($color), "blue": blue($color), ); @each $name, $value in $colors { $adjusted: 0; $value: $value / 256; @if $value < 0.03928 { $value: $value / 12.92; } @else { $value: ($value + 0.055) / 1.055; $value: pow($value, 2.4); } $colors: map-merge( $colors, ( $name: $value, ) ); } $lum: (map-get($colors, "red") * 0.2126) + (map-get($colors, "green") * 0.7152) + (map-get($colors, "blue") * 0.0722); $lum: round($lum * 1000) / 1000; @return $lum;
}
/// Casts a string into a number /// /// @param {String | Number} $value - Value to be parsed /// /// @return {Number} /// @function to-number($value) {
@if type-of($value) == "number" { @return $value; } @else if type-of($value) != "string" { $_: log("Value for `to-number` should be a number or a string."); } $result: 0; $digits: 0; $minus: str-slice($value, 1, 1) == "-"; $numbers: ( "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, ); @for $i from if($minus, 2, 1) through str-length($value) { $character: str-slice($value, $i, $i); @if not(index(map-keys($numbers), $character) or $character == ".") { @return to-length(if($minus, -$result, $result), str-slice($value, $i)); } @if $character == "." { $digits: 1; } @else if $digits == 0 { $result: $result * 10 + map-get($numbers, $character); } @else { $digits: $digits * 10; $result: $result + map-get($numbers, $character) / $digits; } } @return if($minus, -$result, $result);
}
/*
decompose-color-token()
Convert a color token into into a list of form [family], [grade], [variant]
Vivid variants return “vivid” as the variant.
If neither grade nor variant exists, returns 'false'
*/
@function decompose-color-token($token) {
$separator: "-"; $family: false; $grade: false; $variant: false; $exceptions: ( "black": 100, "white": 0, ); $token: if($token == "ink", "base-darkest", $token); // If there's no separator, set family and grade @if not str-index($token, $separator) { $family: $token; $grade: if( map-has-key($exceptions, $family), map-get($exceptions, $family), "root" ); } @else { $split: str-split($token, $separator); $last: nth($split, length($split)); // If the last string is over 3 char, it's a theme token @if str-length($last) > 3 { @if $last == "vivid" { $variant: "vivid"; $grade: "root"; } @else if $last == "warm" or $last == "cool" { $grade: "root"; } @else { $grade: $last; } // Otherwise treat as system token } @else { // Determine if it's a vivid variant @if str-index($last, "v") { $variant: "vivid"; $grade: str-slice($last, 1, (str-index($last, "v") - 1)); } @else { $grade: $last; } // Make sure the grade is a number $grade: if(type-of($grade) == "string", to-number($grade), $grade); } // Collect compound-word families $is-compound-family: false; @if length($split) == 3 or index($split, "warm") or index($split, "cool") { $is-compound-family: true; } @if $is-compound-family { $family: nth($split, 1) + $separator + nth($split, 2); } @else { $family: nth($split, 1); } } @return $family, $grade, $variant;
}
/*
test-colors()
Check to see if all system colors fall between the proper relative luminance range for their grade.
Has a couple quirks, as the luminance() function returns slightly different results than expected.
*/
@function test-colors($map) {
$exceptions: "black", "white", "transparent", "black-transparent", "white-transparent"; @each $token, $value in $map { $family: nth(decompose-color-token($token), 1); $grade: nth(decompose-color-token($token), 2); @if not $value { // empty block } @else if not index($exceptions, $family) { $computed: calculate-grade($value); @debug "Checked #{$family}-#{$grade}"; @if $grade <= 5 { // empty block } @else if $computed != $grade { @warn "#{$token} (#{$value}) lum: #{luminance($value)} is not in the range #{map-get($system-color-grades, $grade)}"; } } } @return 1;
}
/*
str-split()
Split a string at a given separator and convert into a lisrt of substrings
*/
@function str-split($string, $separator) {
$split-arr: (); $index: str-index($string, $separator); @while $index != null { $item: str-slice($string, 1, $index - 1); $split-arr: append($split-arr, $item); $string: str-slice($string, $index + 1); $index: str-index($string, $separator); } $split-arr: append($split-arr, $string); @return $split-arr;
}
/*
str-replace()
Replace any substring with another string
*/
@function str-replace($string, $search, $replace: “”) {
$index: str-index($string, $search); @if $index { @return str-slice($string, 1, $index - 1) + $replace + str-replace( str-slice($string, $index + str-length($search)), $search, $replace ); } @return $string;
}
/*
is-system-color-token()
Return whether a token is a system color token
*/
@function is-system-color-token($token) {
@if map-has-key($system-color-shortcodes, $token) { @return true; } @return false;
}
/*
is-theme-color-token()
Return whether a token is a theme color token
*/
@function is-theme-color-token($token) {
@if map-has-key($project-color-shortcodes, $token) { @return true; } @return false;
}
/*
color-token-assignment()
Get the system token equivalent of any theme color token
*/
@function color-token-assignment($color-token) {
@if is-system-color-token($color-token) { $system-token: $color-token; @return $system-token; } @if not is-theme-color-token($color-token) { @return error-not-token($color-token, "color"); } $theme-token: $color-token; $theme-token-assignment: map-get($assignments-theme-color, $theme-token); @return $theme-token-assignment;
}
/*
is-color-token()
Returns whether a given string is a USWDS color token.
*/
@function is-color-token($token) {
$is-color-token: if(map-has-key($all-color-shortcodes, $token), true, false); @return $is-color-token;
}
/*
calculate-grade()
Derive the grade equivalent any color, even non-token colors
*/
@function calculate-grade($color-token) {
$transparency-error: "USWDS can't calculate the grade of a transparent color. Avoid using transparency in theme colors and text."; $grade: null; $lum: null; $custom-color: false; $color-token-assignment: false; // Determine if the color is a custom color @if type-of($color-token) == "color" { $custom-color: $color-token; } @else { $color-token-assignment: color-token-assignment($color-token); @if type-of($color-token-assignment) == "color" { $custom-color: color-token-assignment($color-token); } } // If it's custom, compare its rLum to USWDS grade rLum ranges @if $custom-color { // If the color uses transparency, throw an error @if alpha($custom-color) != 1 { @return uswds-error($transparency-error); } $lum: luminance($custom-color); // Cycle through grades, knowing current AND next grade $our-grades: map-keys($system-color-grades); $grade-count: length($our-grades); @for $i from 1 through $grade-count { $this-grade: nth($our-grades, $i); $this-grade-min: map-deep-get($system-color-grades, $this-grade, "min"); $this-grade-max: map-deep-get($system-color-grades, $this-grade, "max"); $next-grade: if($i < $grade-count, nth($our-grades, $i + 1), false); $next-grade-min: if( $next-grade, map-deep-get($system-color-grades, $next-grade, "min"), false ); // If the lum fits the range, assign a USWDS grade // Otherwise, set a grade midway between two USWDS grades @if ($lum >= $this-grade-min) and ($lum <= $this-grade-max) { @return $this-grade; } @if ($lum > $this-grade-max) and ($lum < $next-grade-min) { $custom-grade-midpoint: ($this-grade + $next-grade) / 2; $custom-grade: $custom-grade-midpoint; @return $custom-grade; } } } @if not is-color-token($color-token-assignment) { @return error-not-token($color-token-assignment, "color"); } $system-token: $color-token-assignment; $token-split: decompose-color-token($system-token); $token-family: color-token-family($token-split); // If the color uses transparency, throw an error @if str-index($token-family, "transparent") { @return uswds-error($transparency-error); } // Otherwise, return token grade $token-grade: color-token-grade($token-split); @return $token-grade;
}
/*
color()
Derive a color from a color shortcode
*/
@function color($value, $flags…) {
$value: unpack($value); // Non-token colors may be passed with specific flags @if type-of($value) == color { // override or set-theme will allow any color @if index($flags, override) or index($flags, set-theme) { // override + no-warn will skip warnings @if index($flags, no-warn) { @return $value; } @if $theme-show-compile-warnings { @warn 'Override: `#{$value}` is not a USWDS color token.'; } @return $value; } } // False values may be passed through when setting theme colors @if $value == false { @if index($flags, set-theme) { @return $value; } } // Now, any value should be evaluated as a token $value: smart-quote($value); @if map-has-key($system-color-shortcodes, $value) { $our-color: map-get($system-color-shortcodes, $value); @if $our-color == false { @error '`#{$value}` is a color that does not exist ' + 'or is set to false.'; } @return $our-color; } // If we're using the theme flag, $project-color-shortcodes has not yet been set @if not index($flags, set-theme) { @if map-has-key($project-color-shortcodes, $value) { $our-color: (map-get($project-color-shortcodes, $value)); @if $our-color == false { @error '`#{$value}` is a color that does not exist ' + 'or is set to false.'; } @return $our-color; } } @return error-not-token($value, "color");
}
/*
advanced-color()
Derive a color from a color triplet:
- family], [grade], [variant
*/
// color() can have a 1, 2, or 3 arguments passed to it: // // [family] // ex: color('primary') // - the root in a theme palette family // // [family], [grade] // ex: color('red', 50) // - a standard system color // ex: color('accent-warm', 'light') // - a standard theme color // ex: color('primary', 'vivid') // - in theme colors, 'vivid' is considered a grade // // [family], [grade], [vivid] // ex: color('red', 50, 'vivid') // - a vivid system color // - only system colors required three arguments
@function advanced-color(
$color-family: false, $color-grade: false, $color-variant: false
) {
// Convert any arglists into lists $color-family: if( type-of($color-family) == "arglist", unpack($color-family), $color-family ); // If $color-family is a list, color() had a variable // passed to it, and args need to be re-set with the // values from the $color-family list: @if type-of($color-family) == "list" { @if length($color-family) > 2 { $color-variant: nth($color-family, 3); } $color-grade: nth($color-family, 2); $color-family: nth($color-family, 1); } // Set initial state of vars $color-family: smart-quote($color-family); $color-grade: smart-quote($color-grade); $color-variant: smart-quote($color-variant); // @debug '#{$color-family}: #{type-of($color-family)}, #{$color-grade}: #{type-of($color-grade)}, #{$color-variant}: #{type-of($color-variant)}' ; // If there are no args, throw an error @if not $color-family { @error 'Include a color in the form [family], [grade], [vivid]'; } // If the grade is a number, it's a system color // ex: ('red', 50) @if type-of($color-grade) == "number" { @return get-system-color($color-family, $color-grade, $color-variant); } // non-number grades are associated with non-root theme colors // ex: ('base', 'darker') // root theme colors have no grade // ex: ('base') @if map-has-key($all-project-colors, $color-family) { @if not map-has-key(map-get($all-project-colors, $color-family), $color-grade) { @error '`#{$color-grade}` is not a valid grade of `#{$color-family}`. ' + 'Valid grades: ' + '#{map-keys(map-get($all-project-colors, $color-family))}'; } } @else { @return error-not-token($color-family, "theme family", $all-project-colors); } @return map-deep-get($all-project-colors, $color-family, $color-grade);
}
/*
units()
Converts a spacing unit into the desired final units (currently rem)
*/
@function units($value) {
$converted: if( type-of($value) == "string", quote($value), number-to-token($value) ); @if not map-has-key($project-spacing-standard, $converted) { @return error-not-token($value, "spacing unit", $project-spacing-standard); } @return map-get($project-spacing-standard, $converted);
}
/*
get-palettes()
Build a single map of plugin values from a list of plugin keys.
*/
@function get-palettes($list) {
$our-palettes: (); @if type-of($list) == "map" { @error 'Use a list of strings as plugin values.'; } @each $palette in $list { @if not map-has-key($palette-registry, $palette) { @error '#{$palette} isn\'t in the registry.'; } $our-palettes: map-merge( $our-palettes, map-get($palette-registry, $palette) ); } @return $our-palettes;
}
/*
border-radius()
Get a border-radius from the system border-radii
*/
@function border-radius($value) {
@if map-has-key($all-border-radius, $value) { @return map-get($all-border-radius, $value); } @else { @return error-not-token($value, "border radius", $all-border-radius); }
}
/*
font-weight() fw()
Get a font-weight value from the system font-weight
*/
@function font-weight($value) {
@return get-uswds-value(font-weight, $value);
}
@function fw($value) {
@return font-weight($value);
}
/*
feature()
Gets a valid USWDS font feature setting
*/
@function feature($value) {
@return get-uswds-value(feature, $value);
}
/*
flex()
Gets a valid USWDS flex value
*/
@function flex($value) {
@return get-uswds-value(flex, $value);
}
/*
font-family() family()
Get a font-family stack from a role-based or type-based font family
*/
@function font-family($value) {
@return get-uswds-value(font-family, $value);
}
@function ff($value) {
@return font-family($value);
}
@function family($value) {
@return font-family($value);
}
/*
letter-spacing() ls()
Get a letter-spacing value from the system letter-spacing
*/
@function letter-spacing($value) {
$lh-map: map-get($system-properties, letter-spacing); $fn-map: map-get($lh-map, function); @if map-has-key($fn-map, $value) { @return map-get($fn-map, $value); } @if type-of($value) == "number" { @error '`#{$value}` is a not a valid letter-spacing token. ' + 'Valid letter-spacing tokens: #{map-keys($fn-map)}'; } @return get-uswds-value(letter-spacing, $value);
}
@function ls($value) {
@return letter-spacing($value);
}
/*
measure()
Gets a valid USWDS reading line length
*/
@function measure($value) {
@return get-uswds-value(measure, $value);
}
/*
opacity()
Get an opacity from the system opacities
*/
@function opacity($value) {
@return get-uswds-value(opacity, $value);
}
/*
order()
Get an order value from the system orders
*/
@function order($value) {
@return get-uswds-value(order, $value);
}
/*
radius()
Get a border-radius value from the system letter-spacing
*/
@function radius($value) {
@return get-uswds-value(border-radius, $value);
}
/*
font-size()
Get type scale value from a [family] and
- scale
*/
@function font-size($family, $scale, $force: false) {
$our-family: smart-quote($family); $our-scale: smart-quote($scale); @if not map-has-key($project-cap-heights, $our-family) { @return error-not-token($our-family, "font family", $project-cap-heights); } @if not map-get($all-type-scale, $our-scale) { @return error-not-token($our-scale, "font scale", $all-type-scale); } $this-cap: map-get($project-cap-heights, $our-family); $this-scale: map-get($all-type-scale, $our-scale); @if not $force { @if not($this-scale and $this-cap) { @error 'The scale `#{$our-scale}` is disabled ' + 'in your project\'s theme settings. ' + 'Set its value to `true` to use this family.'; } } @return normalize-type-scale($this-cap, $this-scale);
}
@function fs($family, $scale) {
@return font-size($family, $scale);
}
@function size($family, $scale) {
@return font-size($family, $scale);
}
/*
z-index() z()
Get a z-index value from the system z-index
*/
@function z-index($value) {
@return get-uswds-value(z-index, $value);
}
@function z($value) {
@return z-index($value);
}
/*
magic-number()
Returns the magic number of two color grades. Takes numbers or color tokens.
magic-number(50, 10) return: 40
magic-number(“red-50”, “red-10”) return: 40
*/
@function magic-number($grade-1, $grade-2) {
$grade-1: if( type-of($grade-1) == "number", $grade-1, calculate-grade($grade-1) ); $grade-2: if( type-of($grade-2) == "number", $grade-2, calculate-grade($grade-2) ); $magic-number: abs($grade-1 - $grade-2); @return $magic-number;
}
/*
get-default()
Returns the default value from a map of project defaults
get-default(“bg-color”) > $theme-body-background-color
*/
@function get-default($var) {
$value: map-get($project-defaults, $var); @return $value;
}
/*
get-color-token-from-bg()
Returns an accessible foreground color token, given a background, preferred color, fallback color, and WCAG target
returns: color-token
get-color-token-from-bg(
"black", "red-60", "red-10", "AA")
> “red-10”
*/
@function get-color-token-from-bg(
$bg-color: "default", $preferred-text-token: "default", $fallback-text-token: "default", $wcag-target: "AA", $context: false, $for: false
) {
$for-text: if($for, "#{$for} ", ""); $context-text: if($context, "[#{$context}] ", ""); // Set defaults @if $bg-color == "default" { $bg-color: get-default("bg-color"); } @if $preferred-text-token == "default" { $preferred-text-token: get-default("preferred-text-token"); } @if $fallback-text-token == "default" { $fallback-text-token: get-default("fallback-text-token"); } $target-magic-number: map-get($system-wcag-magic-numbers, $wcag-target); $bg-grade: calculate-grade($bg-color); $our-color-tokens: ($preferred-text-token, $fallback-text-token); $accessible-text-token: false; $accessible-text-grade: false; // Get the text color token // Check both text tokens. // Accept a token if it has specified accessible contrast. $best-token: false; $best-magic-number: 0; @each $token in $our-color-tokens { @if not $accessible-text-token { $token-grade: calculate-grade($token); $this-magic-number: magic-number($token-grade, $bg-grade); @if $this-magic-number > $best-magic-number { $best-magic-number: $this-magic-number; $best-token: $token; } @if is-accessible-magic-number($token-grade, $bg-grade, $wcag-target) { $accessible-text-token: $token; $accessible-text-grade: $token-grade; } } } // If neither is accessible, // warn the user and use the Preferred token @if not $accessible-text-token { $accessible-text-token: $best-token; @if $theme-show-compile-warnings { @warn "#{$context-text}Neither the specified preferred #{$for-text}color token (`#{$preferred-text-token}`) nor the fallback #{$for-text}color token (`#{$fallback-text-token}`) have #{$wcag-target} contrast on a `#{$bg-color}` background. Using `#{$best-token}`. Please check your source code and project settings."; } } @return $accessible-text-token;
}
/*
get-link-tokens-from-bg()
Get accessible link colors for a given background color
returns: link-token, hover-token
get-link-tokens-from-bg(
"black", "red-60", "red-10", "AA")
> “red-10”, “red-5”
get-link-tokens-from-bg(
"black", "red-60v", "red-10v", "AA-large")
> “red-60v”, “red-50v”
get-link-tokens-from-bg(
"black", "red-5v", "red-60v", "AA")
> “red-5v”, “white”
get-link-tokens-from-bg(
"black", "white", "red-60v", "AA")
> “white”, “white”
*/
@function get-link-tokens-from-bg(
$bg-color: "default", $preferred-link-token: "default", $fallback-link-token: "default", $wcag-target: "AA", $context: false
) {
$context-text: if($context, "[#{$context}] ", ""); $is-default: false; $is-default-preferred: false; $is-default-fallback: false; $default-reverse: false; $default-standard: false; @if $bg-color == "default" { $bg-color: get-default("bg-color"); } @if $preferred-link-token == "default" { $preferred-link-token: get-default("preferred-link-token"); $default-reverse: true; } @if $fallback-link-token == "default" { $fallback-link-token: get-default("fallback-link-token"); $standard-reverse: true; } $bg-grade: calculate-grade($bg-color); $preferred-hover-token: false; $default-hover-token: false; $accessible-hover-token: false; $accessible-link-token: get-color-token-from-bg( $bg-color, $preferred-link-token, $fallback-link-token, $wcag-target, $context, $for: "link" ); $accessible-link-grade: calculate-grade($accessible-link-token); // Get the hover color token // If link is lighter than bg set $is-reverse to true $is-reverse: if($accessible-link-grade < $bg-grade, true, false); // If using defaults, set the default hover // $link-kind is used for error messaging $link-kind: false; @if $is-reverse { @if $default-reverse { $default-hover-token: $theme-link-reverse-hover-color; $link-kind: "default reverse"; } } @else if $default-standard { $default-hover-token: $theme-link-hover-color; $link-kind: "default"; } @if $default-hover-token { $default-hover-grade: calculate-grade($default-hover-token); @if is-accessible-magic-number( $default-hover-grade, $bg-grade, $wcag-target ) { $accessible-hover-token: $default-hover-token; } @if not $accessible-hover-token and $theme-show-compile-warnings { @warn "#{$context-text}The #{$link-kind} link hover (`#{$default-hover-token}`) does not have #{$wcag-target} contrast on a #{$bg-color} background. Please update your project settings."; } } @if not $accessible-hover-token { $direction: if($is-reverse, "lighter", "darker"); $hover-token: next-token($accessible-link-token, $direction); // Use the next token, if it is valid @if $hover-token { $accessible-hover-token: $hover-token; // Otherwise use the token itself as hover, and warn. } @else { $accessible-hover-token: $accessible-link-token; @if $theme-show-compile-warnings { @warn "#{$context-text}A `#{$accessible-hover-token}` link does not have #{$direction} hover available. Hover set to link color."; } } } @return $accessible-link-token, $accessible-hover-token;
}
/*
color-token-type()
Returns the type of a color token.
Returns: “system” | “theme”
*/
@function color-token-type($token) {
$type: if(is-system-color-token($token), "system", false); @if not $type { $type: if(is-theme-color-token($token), "theme", false); } @if not $type { @return error-not-token($token, "color"); } @return $type;
}
/*
color-token-family()
Returns the family of a color token.
Returns: color-family
color-token-family(“accent-warm-vivid”) > “accent-warm”
color-token-family(“red-50v”) > “red”
color-token-variant((“red”, 50, “vivid”)) > “red”
*/
@function color-token-family($color-token) {
$split: if( type-of($color-token) == "list", $color-token, decompose-color-token($color-token) ); $family: nth($split, 1); @return $family;
}
/*
color-token-grade()
Returns the grade of a USWDS color token.
Returns: color-grade
color-token-grade(“accent-warm”) > “root”
color-token-grade(“accent-warm-vivid”) > “root”
color-token-grade(“accent-warm-darker”) > “darker”
color-token-grade(“red-50v”) > 50
color-token-variant((“red”, 50, “vivid”)) > 50
*/
@function color-token-grade($color-token) {
$split: if( type-of($color-token) == "list", $color-token, decompose-color-token($color-token) ); $grade: nth($split, 2); @return $grade;
}
/*
color-token-variant()
Returns the variant of color token.
Returns: “vivid” | false
color-token-variant(“accent-warm”) > false
color-token-variant(“accent-warm-vivid”) > “vivid”
color-token-variant(“red-50v”) > “vivid”
color-token-variant((“red”, 50, “vivid”)) > “vivid”
*/
@function color-token-variant($color-token) {
$split: if( type-of($color-token) == "list", $color-token, decompose-color-token($color-token) ); $variant: nth($split, 3); @return $variant;
}
/*
next-token()
Returns next “darker” or “lighter” color token of the same token type and variant.
Returns: color-token | false
next-token(“accent-warm”, “lighter”) > “accent-warm-light”
next-token(“gray-10”, “lighter”) > “gray-5”
next-token(“gray-5”, “lighter”) > “white”
next-token(“white”, “lighter”) > false
next-token(“red-50v”, “darker”) > “red-60v”
next-token(“red-50”, “darker”) > “red-60”
next-token(“red-80v”, “darker”) > “red-90”
next-token(“red-90”, “darker”) > “black”
next-token(“white”, “darker”) > “gray-5”
next-token(“black”, “lighter”) > “gray-90”
*/
@function next-token($token, $direction) {
$next-token: false; $type: color-token-type($token); $token-split: decompose-color-token($token); // 1. System case @if $type == "system" { // transparent tokens return don't have a next token @if $token == "transparent" { @return false; } // black and white tokens use the gray family for next $current-family: if( $token == "white" or $token == "black", "gray", color-token-family($token-split) ); // black- and white-transparent tokens don't have a next @if str-index($current-family, "-transparent") { @return false; } $current-grade: color-token-grade($token-split); // Nothing can be darker than black or lighter than white @if $direction == "darker" and $current-grade == 100 { @return false; } @if $direction == "lighter" and $current-grade == 0 { @return false; } // Grades under 5 should be treated as 5 @if $current-grade > 0 and $current-grade < 5 { $current-grade: 5; } $system-grade-list: map-keys($system-color-grades); $current-grade-index: index($system-grade-list, $current-grade); // Note: System grades go from darkest (100) to lightest (0) $next-grade: if( $direction == "darker", nth($system-grade-list, ($current-grade-index - 1)), nth($system-grade-list, ($current-grade-index + 1)) ); $output-grade: $next-grade; // Keep the same vivid variant as the parent // Note: Grade 90 has no vivid variant @if color-token-variant($token-split) == "vivid" and ($next-grade < 90) { $output-grade: $next-grade + "v"; } // Use black and white tokens for grades 100 and 0... @if $next-grade == 100 { $next-token: "black"; } @else if $next-grade == 0 { $next-token: "white"; // ...Otherwise output token in expected form } @else { $next-token: $current-family + "-" + $output-grade; } // 2. Theme case } @else { $current-grade: color-token-grade($token-split); // Vivid theme token should be considered root for ordering $current-grade: if($current-grade == "vivid", "root", $current-grade); $current-family: color-token-family($token-split); // Ink should be considered base-darkest // TODO: Should it? @if $token == "ink" { $current-family: "base"; $current-grade: "darkest"; } // Black is darker than darkest @if $direction == "darker" and $current-grade == "darkest" { @return "black"; } // White is lighter than lightest @if $direction == "lighter" and $current-grade == "lightest" { @return "white"; } $theme-grade-list: map-keys($theme-color-grades); $current-grade-index: index($theme-grade-list, $current-grade); // Note: Theme grades go from `lightest` to `darkest` $next-grade: if( $direction == "darker", nth($theme-grade-list, ($current-grade-index + 1)), nth($theme-grade-list, ($current-grade-index - 1)) ); // Exclude `root` from token output @if $next-grade == "root" { @return $current-family; } @else { $next-token: $current-family + "-" + $next-grade; } // If the next color is set to false, use black/white instead @if not color-token-assignment($next-token) { @if $direction == "darker" { @return "black"; } @if $direction == "lighter" { @return "white"; } } } @return $next-token;
}
/*
wcag-magic-number()
Returns the magic number of a specific wcag grade:
“AA” “AA-Large” “AAA”
wcag-magic-number(“AA”) > 50
*/
@function wcag-magic-number($wcag-target) {
$wcag-magic-number: map-get($system-wcag-magic-numbers, $wcag-target); @return $wcag-magic-number;
}
/*
is-accessible-magic-number()
Returns whether two grades achieve specified target color contrast
Returns: true | false
is-accessible-magic-number(10, 50, “AA”) > false
is-accessible-magic-number(10, 60, “AA”) > true
*/
@function is-accessible-magic-number($grade-1, $grade-2, $wcag-target) {
$target-magic-number: wcag-magic-number($wcag-target); $magic-number: magic-number($grade-1, $grade-2); @if $magic-number >= $target-magic-number { @return true; } @return false;
}