diff --git a/.gitignore b/.gitignore index 8a3b2d596..94f57dd26 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ Thumbs.db /_site/ /source/assets/dist/ +# Generated file +/source/assets/js/playground/module-metadata.ts + # NPM/Yarn node_modules/ yarn-debug.log* diff --git a/.prettierignore b/.prettierignore index 7c5ade481..c455f7e99 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ /_site/ /source/_data/versionCache.json /source/assets/dist/ +/source/assets/js/playground/module-metadata.ts /source/assets/js/vendor/** /source/assets/sass/vendor/ /source/documentation/js-api diff --git a/package-lock.json b/package-lock.json index 1a811c728..dacf24ee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "prettier": "^3.3.3", "prismjs": "^1.29.0", "rollup": "^4.21.3", - "sass": "^1.78.0", + "sass": "^1.79.2", "semver": "^7.6.3", "stylelint": "^15.11.0", "stylelint-config-standard-scss": "^11.1.0", @@ -9815,12 +9815,12 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz", - "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", + "version": "1.79.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.2.tgz", + "integrity": "sha512-YmT1aoF1MwHsZEu/eXhbAJNsPGAhNP4UixW9ckEwWCvPcVdVF0/C104OGDVEqtoctKq0N+wM20O/rj+sSPsWeg==", "dev": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -9831,6 +9831,34 @@ "node": ">=14.0.0" } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.0.tgz", + "integrity": "sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", + "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/section-matter": { "version": "1.0.0", "dev": true, @@ -18011,14 +18039,31 @@ "dev": true }, "sass": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz", - "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", + "version": "1.79.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.2.tgz", + "integrity": "sha512-YmT1aoF1MwHsZEu/eXhbAJNsPGAhNP4UixW9ckEwWCvPcVdVF0/C104OGDVEqtoctKq0N+wM20O/rj+sSPsWeg==", "dev": true, "requires": { - "chokidar": ">=3.0.0 <4.0.0", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" + }, + "dependencies": { + "chokidar": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.0.tgz", + "integrity": "sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "readdirp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", + "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", + "dev": true + } } }, "section-matter": { diff --git a/package.json b/package.json index 601786500..1f60b2b88 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,20 @@ "clean-version-cache": "rm -f source/_data/versionCache.json", "build:sass": "sass --style=compressed ./source/assets/sass/sass.scss:./source/assets/dist/css/sass.css ./source/assets/sass/noscript.scss:./source/assets/dist/css/noscript.css", "watch:sass": "sass --watch ./source/assets/sass/sass.scss:./source/assets/dist/css/sass.css ./source/assets/sass/noscript.scss:./source/assets/dist/css/noscript.css", - "build-dev:scripts": "rollup -c", - "build-prod:scripts": "NODE_ENV=production BABEL_ENV=production rollup -c", - "watch:scripts": "npm run build-dev:scripts -- -w", + "build-module-metadata": "ts-node tool/generate-module-metadata.ts", + "build-rollup": "rollup -c", + "watch-rollup": "rollup -c -w", + "build-dev:scripts": "run-s build-module-metadata build-rollup", + "build-prod:scripts": "NODE_ENV=production BABEL_ENV=production npm run build-dev:scripts", + "watch:scripts": "run-s build-module-metadata watch-rollup", "build:typedoc": "./tool/typedoc-build.sh", "build:11ty": "NODE_OPTIONS='-r ts-node/register' eleventy", "watch:11ty": "npm run build:11ty -- --serve --incremental", "check": "run-s check:gts check:tsc check:stylelint", "check:gts": "gts check", "check:stylelint": "stylelint 'source/assets/sass/*.{css,scss}'", - "check:tsc": "tsc --noEmit", + "tsc:no-emit": "tsc --noEmit", + "check:tsc": "run-s build-module-metadata tsc:no-emit", "fix": "run-s fix:gts fix:stylelint", "fix:gts": "gts fix", "fix:stylelint": "stylelint 'source/assets/sass/*.{css,scss}' --fix", @@ -98,7 +102,7 @@ "prettier": "^3.3.3", "prismjs": "^1.29.0", "rollup": "^4.21.3", - "sass": "^1.78.0", + "sass": "^1.79.2", "semver": "^7.6.3", "stylelint": "^15.11.0", "stylelint-config-standard-scss": "^11.1.0", diff --git a/source/_data/documentation.yml b/source/_data/documentation.yml index a6d1bcbec..5b4e21dfa 100644 --- a/source/_data/documentation.yml +++ b/source/_data/documentation.yml @@ -76,6 +76,8 @@ toc: - Functions and Mixins Beginning with --: /documentation/breaking-changes/css-function-mixin/ - Mixed Declarations: /documentation/breaking-changes/mixed-decls/ - meta.feature-exists: /documentation/breaking-changes/feature-exists/ + - Color Functions: /documentation/breaking-changes/color-functions/ + - Legacy JS API: /documentation/breaking-changes/legacy-js-api/ - Command Line: /documentation/cli/ :children: - Dart Sass: /documentation/cli/dart-sass/ diff --git a/source/_includes/silencing_deprecations.liquid b/source/_includes/silencing_deprecations.liquid index 2e4d1c1c0..ea34d0e7c 100644 --- a/source/_includes/silencing_deprecations.liquid +++ b/source/_includes/silencing_deprecations.liquid @@ -55,9 +55,3 @@ option] in the JavaScript API. [`--silence-deprecation` flag]: /documentation/cli/dart-sass/#silence-deprecation [`silenceDeprecations` option]: /documentation/js-api/interfaces/Options/#silenceDeprecations - -{% headsUp %} - This option is only available in the [modern JS API]. - - [modern JS API]: /documentation/js-api/#md:usage -{% endheadsUp %} diff --git a/source/assets/img/blog/042-blue-yellow.jpg b/source/assets/img/blog/042-blue-yellow.jpg new file mode 100644 index 000000000..9b1096322 Binary files /dev/null and b/source/assets/img/blog/042-blue-yellow.jpg differ diff --git a/source/assets/img/blog/042-p3-hsl.png b/source/assets/img/blog/042-p3-hsl.png new file mode 100644 index 000000000..dd4b01f14 Binary files /dev/null and b/source/assets/img/blog/042-p3-hsl.png differ diff --git a/source/assets/img/blog/042-p3-oklch.png b/source/assets/img/blog/042-p3-oklch.png new file mode 100644 index 000000000..cd78fe6e7 Binary files /dev/null and b/source/assets/img/blog/042-p3-oklch.png differ diff --git a/source/assets/img/blog/042-p3-srgb.png b/source/assets/img/blog/042-p3-srgb.png new file mode 100644 index 000000000..450b90889 Binary files /dev/null and b/source/assets/img/blog/042-p3-srgb.png differ diff --git a/source/assets/img/blog/042-srgb-hsl.png b/source/assets/img/blog/042-srgb-hsl.png new file mode 100644 index 000000000..54caf1460 Binary files /dev/null and b/source/assets/img/blog/042-srgb-hsl.png differ diff --git a/source/assets/img/blog/042-srgb-hwb.png b/source/assets/img/blog/042-srgb-hwb.png new file mode 100644 index 000000000..e1600051a Binary files /dev/null and b/source/assets/img/blog/042-srgb-hwb.png differ diff --git a/source/assets/img/blog/042-srgb.png b/source/assets/img/blog/042-srgb.png new file mode 100644 index 000000000..72d54d8e9 Binary files /dev/null and b/source/assets/img/blog/042-srgb.png differ diff --git a/source/assets/js/playground.ts b/source/assets/js/playground.ts index 9b8a62875..c215bff9a 100644 --- a/source/assets/js/playground.ts +++ b/source/assets/js/playground.ts @@ -14,6 +14,7 @@ import { } from './playground/editor-setup.js'; import { ParseResult, + PlaygroundSelection, PlaygroundState, customLoader, deserializeState, @@ -111,9 +112,7 @@ function setupPlayground(): void { * Returns a playground state selection for the current single non-empty * selection, or `null` otherwise. */ - function editorSelectionToStateSelection(): - | PlaygroundState['selection'] - | null { + function editorSelectionToStateSelection(): PlaygroundSelection { const sel = editor.state.selection; if (sel.ranges.length !== 1) return null; @@ -130,7 +129,7 @@ function setupPlayground(): void { ]; } - /** Updates the editor's selection based on `playgroundState.selection`. */ + /** Updates the {@link editor}'s selection based on `{@link playgroundState.selection}`. */ function updateSelection(): void { if (playgroundState.selection === null) { const sel = editor.state.selection; @@ -162,6 +161,13 @@ function setupPlayground(): void { } } + /** Highlights {@link selection} and focuses on the {@link editor}. */ + function goToSelection(selection: PlaygroundSelection): void { + playgroundState.selection = selection; + updateSelection(); + editor.focus(); + } + // Apply initial state to dom function applyInitialState(): void { updateButtonState(); @@ -301,8 +307,22 @@ function setupPlayground(): void { '.sl-c-playground__console' ) as HTMLDivElement; console.innerHTML = playgroundState.debugOutput - .map(displayForConsoleLog) + .map(item => displayForConsoleLog(item, playgroundState)) .join('\n'); + console.querySelectorAll('a.console-location').forEach(link => { + (link as HTMLAnchorElement).addEventListener('click', event => { + if (!(event.metaKey || event.altKey || event.shiftKey)) { + event.preventDefault(); + } + const range = (event.currentTarget as HTMLAnchorElement).dataset.range + ?.split(',') + .map(n => parseInt(n)); + if (range && range.length === 4) { + const [fromL, fromC, toL, toC] = range; + goToSelection([fromL, fromC, toL, toC]); + } + }); + }); } function updateDiagnostics(): void { diff --git a/source/assets/js/playground/autocomplete.ts b/source/assets/js/playground/autocomplete.ts new file mode 100644 index 000000000..1f387bb06 --- /dev/null +++ b/source/assets/js/playground/autocomplete.ts @@ -0,0 +1,249 @@ +import { + CompletionContext, + CompletionResult, + CompletionSource, +} from '@codemirror/autocomplete'; +import {sassCompletionSource} from '@codemirror/lang-sass'; +import {syntaxTree} from '@codemirror/language'; +import {EditorState} from '@codemirror/state'; +import moduleMetadata from './module-metadata'; + +// The validFor identifier, from @codemirror/lang-css. After an initial set of +// possible completions are returned from a completion soruce, the matched set +// narrows as the user types as long as it matches this identifier. Once it no +// longer matches, the completion sources are checked again. +// https://codemirror.net/docs/ref/#autocomplete.CompletionResult.validFor +const identifier = /^(\w[\w-]*|-\w[\w-]*|)$/; + +// Sass-specific at rules only. CSS at rules should be added to `@codemirror/lang-css`. +const atRuleKeywords = [ + 'use', + 'forward', + 'import', + 'mixin', + 'include', + 'function', + 'extend', + 'error', + 'warn', + 'debug', + 'at-root', + 'if', + 'else', + 'each', + 'for', + 'while', +]; + +// CompletionResult options for Sass at rules, for example `@use`. +const atRuleOptions = Object.freeze( + atRuleKeywords.map(keyword => ({ + label: `@${keyword} `, + type: 'keyword', + validFor: identifier, + })) +); + +// Completions for Sass at rules +function atRuleCompletion(context: CompletionContext): CompletionResult | null { + const atRule = context.matchBefore(/@\w*/); + if (!atRule) return null; + if (atRule.from === atRule.to && !context.explicit) return null; + return { + from: atRule.from, + to: atRule.to, + options: atRuleOptions, + validFor: identifier, + }; +} + +// A list of all the Sass built in modules. +const moduleNames = moduleMetadata.map(mod => mod.name); +type ModuleName = (typeof moduleNames)[number]; + +// Matches an identifier namespaced within any of the built in Sass modules. +const moduleNameRegExp = new RegExp(`(${moduleNames.join('|')}).\\$?\\w*`); + +// Matches the StringLiteral `"sass:modName"`, capturing `modName`. +const moduleUseRegex = new RegExp(/['"]sass:(?.*)['"]/); + +// Completion results for built in Sass modules following the `sass:` namespace. +const moduleCompletions = Object.freeze( + moduleMetadata.map(mod => ({ + label: `sass:${mod.name}`, + apply: `sass:${mod.name}`, + info: mod.description, + type: 'class', + validFor: identifier, + _moduleName: mod.name as ModuleName, + })) +); + +// Completions for the import of built in modules, for instance "sass:color". +function moduleMetadataCompletion( + context: CompletionContext +): CompletionResult | null { + const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1); + const potentialTypes = [ + 'StringLiteral', // When wrapped in quotes- `"sass:"` + 'ValueName', // No end quote, before semicolon- `"sa` + ':', // No end quote, on semicolon- `"sass:` + 'PseudoClassName', // No end quote, after semicolon- `"sass:m` + ]; + if (!potentialTypes.includes(nodeBefore.type.name)) return null; + const potentialParentTypes = [ + 'UseStatement', // When wrapped in quotes + 'PseudoClassSelector', // No end quote, after the colon + ]; + if (!potentialParentTypes.includes(nodeBefore.parent?.type.name || '')) { + return null; + } + + const moduleMatch = context.matchBefore(/["'](sass:)?\w*/); + + if (!moduleMatch) return null; + if (moduleMatch.from === moduleMatch.to && !context.explicit) return null; + + const included = includedModuleMetadata(context.state); + const notIncludedModuleCompletions = moduleCompletions.filter( + moduleCompletion => !included.includes(moduleCompletion._moduleName) + ); + return { + from: moduleMatch.from + 1, + to: moduleMatch.to, + options: notIncludedModuleCompletions, + validFor: identifier, + }; +} + +/** + * Maps modules into a record with the name as the key and the result of `map` + * as the value. + */ +function mapModulesByName( + map: (module: (typeof moduleMetadata)[number]) => V +): Record { + return moduleMetadata.reduce>((acc, mod) => { + acc[mod.name] = map(mod); + return acc; + }, {}); +} + +// Completion results for module variables. +const moduleVariableCompletions = Object.freeze( + mapModulesByName(mod => + mod.variables.map(variable => ({ + label: `${mod.name}.${variable}`, + type: 'variable', + })) + ) +); + +// Completion results for module functions. +const moduleFunctionsCompletions = Object.freeze( + mapModulesByName(mod => + mod.functions.map(func => ({ + label: `${mod.name}.${func}`, + apply: `${mod.name}.${func}(`, + type: 'method', + boost: 10, + validFor: identifier, + })) + ) +); + +// Type predicate for modules names. +function isModuleName(string?: string | null): string is ModuleName { + return moduleNames.includes(string as ModuleName); +} + +// Returns the list of built in modules that are included in the text. +function includedModuleMetadata(state: EditorState): ModuleName[] { + const tree = syntaxTree(state); + const useNodes = tree.topNode.getChildren('UseStatement'); + const usedModules = useNodes.map(useNode => { + const cursor = useNode.cursor(); + while (cursor.next()) { + if (cursor.node.name === 'StringLiteral') { + const string = state.doc.sliceString(cursor.from, cursor.to); + return string.match(moduleUseRegex)?.groups?.modName; + } + } + return null; + }); + + return usedModules.filter(mod => isModuleName(mod)); +} + +// Completions for the namespaces of included built in modules. +function builtinModuleNameCompletion( + context: CompletionContext +): CompletionResult | null { + const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1); + if (nodeBefore.type.name !== 'ValueName') return null; + // Prevent module name from showing up after `.` + if (nodeBefore.parent?.type.name === 'NamespacedValue') return null; + const includedModules = includedModuleMetadata(context.state); + + const match = context.matchBefore(/\w+/); + if (!match) return null; + + return { + from: match.from, + to: match.to, + options: includedModules.map(mod => ({ + label: mod, + info: moduleMetadata.find(builtin => builtin.name === mod)?.description, + type: 'namespace', + boost: 20, + validFor: identifier, + })), + }; +} + +// Completions for variables and functions for included built in modules. +function builtinModuleItemCompletion( + context: CompletionContext +): CompletionResult | null { + const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1); + if ( + ![nodeBefore.type.name, nodeBefore.parent?.type.name].includes( + 'NamespacedValue' + ) + ) { + return null; + } + const moduleNameMatch = context.matchBefore(moduleNameRegExp); + + if (!moduleNameMatch) return null; + if (moduleNameMatch.from === moduleNameMatch.to && !context.explicit) { + return null; + } + + const includedModules = includedModuleMetadata(context.state); + + const includedModFunctions = includedModules.flatMap( + mod => moduleFunctionsCompletions[mod] + ); + const includedModVariables = includedModules.flatMap( + mod => moduleVariableCompletions[mod] + ); + + return { + from: moduleNameMatch.from, + to: moduleNameMatch.to, + options: [...includedModVariables, ...includedModFunctions], + validFor: identifier, + }; +} + +// Aggregates all custom completions with the CodeMirror sassCompletionSource. +const playgroundCompletions: CompletionSource[] = [ + atRuleCompletion, + moduleMetadataCompletion, + builtinModuleNameCompletion, + sassCompletionSource, + builtinModuleItemCompletion, +]; + +export default playgroundCompletions; diff --git a/source/assets/js/playground/console-utils.ts b/source/assets/js/playground/console-utils.ts index b2a57088b..106f47a4d 100644 --- a/source/assets/js/playground/console-utils.ts +++ b/source/assets/js/playground/console-utils.ts @@ -1,5 +1,7 @@ import {Exception, SourceSpan} from 'sass'; +import {PlaygroundSelection, PlaygroundState, serializeState} from './utils'; + export interface ConsoleLogDebug { options: { span: SourceSpan; @@ -13,6 +15,9 @@ export interface ConsoleLogWarning { deprecation: boolean; span?: SourceSpan | undefined; stack?: string | undefined; + deprecationType?: { + id: string; + }; }; message: string; type: 'warn'; @@ -39,42 +44,81 @@ function encodeHTML(message: string): string { return el.innerHTML; } -function lineNumberFormatter(number?: number): string { - if (number === undefined) return ''; - number = number + 1; - return `${number}`; +// Returns undefined if no range, or a link to the state, including range. +function selectionLink( + playgroundState: PlaygroundState, + range: PlaygroundSelection +): string | undefined { + if (!range) return undefined; + return serializeState({...playgroundState, selection: range}); } -export function displayForConsoleLog(item: ConsoleLog): string { - const data: {type: string; lineNumber?: number; message: string} = { - type: item.type, - lineNumber: undefined, - message: '', - }; +// Returns a safe HTML string for a console item. +export function displayForConsoleLog( + item: ConsoleLog, + playgroundState: PlaygroundState +): string { + let lineNumber: number | undefined; + let message: string; + let range: PlaygroundSelection = null; + if (item.type === 'error') { if (item.error instanceof Exception) { - data.lineNumber = item.error.span.start.line; + const span = item.error.span; + lineNumber = span.start.line; + range = [ + span.start.line + 1, + span.start.column + 1, + span.end.line + 1, + span.end.column + 1, + ]; } - data.message = item.error?.toString() || ''; - } else if (['debug', 'warn'].includes(item.type)) { - data.message = item.message; - let lineNumber = item.options.span?.start?.line; - if (typeof lineNumber === 'undefined') { - const stack = 'stack' in item.options ? item.options.stack : ''; - const needleFromStackRegex = /^- (\d+):/; - const match = stack?.match(needleFromStackRegex); - if (match?.[1]) { + message = encodeHTML(item.error?.toString() ?? ''); + } else { + message = encodeHTML(item.message); + if (item.options.span) { + const span = item.options.span; + lineNumber = span.start.line; + range = [ + span.start.line + 1, + span.start.column + 1, + span.end.line + 1, + span.end.column + 1, + ]; + } else if ('stack' in item.options) { + const match = item.options.stack?.match(/^- (\d+):(\d+) /); + if (match) { // Stack trace starts at 1, all others come from span, which starts at // 0, so adjust before formatting. lineNumber = parseInt(match[1]) - 1; + range = [ + parseInt(match[1]), + parseInt(match[2]), + parseInt(match[1]), + parseInt(match[2]), + ]; } } - data.lineNumber = lineNumber; + + if (item.type === 'warn' && item.options.deprecationType?.id) { + const safeLink = `https://sass-lang.com/d/${item.options.deprecationType.id}`; + message = message.replace( + safeLink, + `${safeLink}` + ); + } } + const link = selectionLink(playgroundState, range); + + const locationStart = link + ? `` + : ''; - return `
@${data.type}:${lineNumberFormatter( - data.lineNumber - )}
${encodeHTML(data.message)}
`; + return `
${locationStart}@${item.type}${ + lineNumber !== undefined ? `:${lineNumber + 1}` : '' + }${locationEnd}
${message}
`; } diff --git a/source/assets/js/playground/editor-setup.ts b/source/assets/js/playground/editor-setup.ts index 2ba7c2228..8aa17bb02 100644 --- a/source/assets/js/playground/editor-setup.ts +++ b/source/assets/js/playground/editor-setup.ts @@ -33,6 +33,7 @@ import { } from '@codemirror/view'; import {playgroundHighlightStyle} from './theme.js'; +import playgroundCompletions from './autocomplete.js'; import {EditorView} from 'codemirror'; const syntax = new Compartment(); @@ -67,7 +68,7 @@ const editorSetup = (() => [ syntaxHighlighting(defaultHighlightStyle, {fallback: true}), bracketMatching(), closeBrackets(), - autocompletion(), + autocompletion({override: playgroundCompletions}), highlightActiveLine(), drawSelection(), keymap.of([ diff --git a/source/assets/js/playground/utils.ts b/source/assets/js/playground/utils.ts index b1072f7da..4f6543f8c 100644 --- a/source/assets/js/playground/utils.ts +++ b/source/assets/js/playground/utils.ts @@ -7,6 +7,12 @@ import {ConsoleLog, ConsoleLogDebug, ConsoleLogWarning} from './console-utils'; const PLAYGROUND_LOAD_ERROR_MESSAGE = 'The Sass Playground does not support loading stylesheets.'; +/** + * `[fromLine, fromColumn, toLine, toColumn]`; all 1-indexed. If this is null, + * the editor has no selection. + */ +export type PlaygroundSelection = [number, number, number, number] | null; + export interface PlaygroundState { inputFormat: Exclude; outputFormat: OutputStyle; @@ -14,12 +20,7 @@ export interface PlaygroundState { compilerHasError: boolean; debugOutput: ConsoleLog[]; outputValue: string; - - /** - * `[fromLine, fromColumn, toLine, toColumn]`; all 1-indexed. If this is null, - * the editor has no selection. - */ - selection: [number, number, number, number] | null; + selection: PlaygroundSelection; } export function serializeState(state: PlaygroundState): string { diff --git a/source/assets/sass/components/_playground.scss b/source/assets/sass/components/_playground.scss index d9ce4f5fb..03cf718c7 100644 --- a/source/assets/sass/components/_playground.scss +++ b/source/assets/sass/components/_playground.scss @@ -2,6 +2,12 @@ @use '../config'; @use '../config/color/brand'; +$playground-base-colors: ( + 'info': var(--sl-color--code-info), + 'warning': var(--sl-color--code-warning), + 'error': var(--sl-color--code-error), +); + .playground { --sl-max-width--container: 100vw; @@ -172,12 +178,13 @@ overflow-y: inherit; .cm-gutters { - background-color: var(--sl-background--editor); + background-color: transparent; border-right: none; } .cm-lineNumbers .cm-gutterElement { min-width: var(--sl-gutter--double); + padding: 0 0.5ch 0 1.5ch; } .cm-content, @@ -195,11 +202,26 @@ .cm-line { padding-left: var(--sl-gutter); + + &::before { + content: '\2022'; + color: var(--sl-color--bullet-line, transparent); + font-size: var(--sl-font-size--x-large); + left: 0; + position: absolute; + transform: translateY(-25%); + } + + @each $name, $color in $playground-base-colors { + &:has(.cm-lintPoint-#{$name}, .cm-lintRange-#{$name}) { + --sl-color--bullet-line: #{$color}; + } + } } .cm-activeLineGutter, .cm-activeLine { - background-color: var(--sl-color--warning-highlight); + background-color: var(--sl-color--code-highlight-light); [data-code='compiled'] & { background-color: var(--sl-color--code-background); @@ -216,21 +238,24 @@ } } - .cm-diagnostic { - color: var(--sl-color--code-text); - background: var(--sl-color--code-background-darker); + .cm-tooltip { + border: none; } - .cm-diagnostic-error { - border-color: var(--sl-color--error); + .cm-diagnostic { + background: var(--sl-color--background-tooltip); + border: 1px solid var(--sl-color--border-tooltip); + color: var(--sl-color--code-text); + padding: var(--sl-gutter--half); } - .cm-diagnostic-warning { - border-color: var(--sl-color--warn); - } + @each $name, $color in $playground-base-colors { + .cm-diagnostic-#{$name} { + --sl-color--border-tooltip: #{$color}; + --sl-color--background-tooltip: var(--sl-color--code-#{$name}-light); - .cm-diagnostic-info { - border-color: var(--sl-color--success); + border-color: $color; + } } .cm-specialChar { @@ -260,26 +285,41 @@ .sl-c-playground__console { font-family: var(--sl-font-family--code); + display: grid; + gap: var(--sl-gutter); + grid-auto-rows: max-content; + grid-template-columns: [location] auto [message] 1fr; height: 100%; line-height: 1; margin: 0; .console-line { + --sl-background--link: transparent; + --sl-border-color--link: transparent; + --sl-border-color--link-state: var(--sl-color--iron); + display: grid; - gap: var(--sl-gutter); - grid-template: 'location message' auto / 10ch 1fr; + grid-column: 1 / -1; + grid-template-columns: subgrid; margin-bottom: var(--sl-gutter--half); + place-items: start; } .console-message { display: grid; line-height: var(--sl-line-height--console); + + a { + justify-self: start; + } } + // Debug panel uses Sass terms "warn" and "debug" + // Code Mirror uses "warning" and "info" $console-type-colors: ( - 'error': var(--sl-color--error), - 'warn': var(--sl-color--warn), - 'debug': var(--sl-color--success), + 'error': var(--sl-color--code-error), + 'warn': var(--sl-color--code-warning), + 'debug': var(--sl-color--code-info), ); @each $name, $color in $console-type-colors { diff --git a/source/assets/sass/config/color/_content.scss b/source/assets/sass/config/color/_content.scss index f3b9332a7..07dc2dbf0 100644 --- a/source/assets/sass/config/color/_content.scss +++ b/source/assets/sass/config/color/_content.scss @@ -1,22 +1,23 @@ @use 'sass:color'; @use 'brand'; -$sl-color--highlight: color.adjust(brand.$sl-color--hopbush, $lightness: -10%); $sl-color--action: color.adjust(brand.$sl-color--bouquet, $lightness: -10%); -$sl-color--shadow: rgba(brand.$sl-color--midnight-blue, 0.125); $sl-color--active: color.adjust(brand.$sl-color--venus, $lightness: -10%); -$sl-color--code-background: #f8f8f8; -$sl-color--code-background-darker: #ebebeb; -$sl-color--code-text: color.adjust(brand.$sl-color--pale-sky, $lightness: -25%); +$sl-color--highlight: color.adjust(brand.$sl-color--hopbush, $lightness: -10%); $sl-color--link-action: rgba(218, 219, 223, 25%); -$sl-color--code-warm: #cf1666; -$sl-color--code-bright-dark: #900; -$sl-color--code-bright: #df1144; -$sl-color--code-muted-dark: #393a34; -$sl-color--code-muted: #656556; -$sl-color--code-base: #066; -$sl-color--code-cool: #458; -$sl-color--code-dark: black; +$sl-color--shadow: rgba(brand.$sl-color--midnight-blue, 0.125); + +// Darker Shades of existing colors for use on a light gray background +$sl-color--text-medium-dark: color.adjust( + brand.$sl-color--pale-sky, + $lightness: -5% +); +$sl-color--action-dark: color.adjust( + brand.$sl-color--bouquet, + $lightness: -11.647% +); + +// Callouts/Info Panels $sl-color--warning-light: color.adjust( brand.$sl-color--hopbush, $lightness: 27% @@ -25,19 +26,33 @@ $sl-color--warning-lighter: color.adjust( brand.$sl-color--hopbush, $lightness: 38% ); -$sl-color--warning-highlight: rgba($sl-color--warning-light, 0.2); $sl-color--info-light: color.adjust(brand.$sl-color--patina, $lightness: 32%); $sl-color--info-lighter: color.adjust(brand.$sl-color--patina, $lightness: 47%); -$sl-color--error: #cf0254; -$sl-color--warn: #c14e00; -$sl-color--success: #168073; -// Darker Shades of existing colors for use on a light gray background -$sl-color--text-medium-dark: color.adjust( - brand.$sl-color--pale-sky, - $lightness: -5% +// Code Colors +$sl-color--code-background: #f8f8f8; +$sl-color--code-background-darker: #ebebeb; +$sl-color--code-text: color.adjust(brand.$sl-color--pale-sky, $lightness: -25%); +$sl-color--code-warm: #cf1666; +$sl-color--code-bright-dark: #900; +$sl-color--code-bright: #df1144; +$sl-color--code-muted-dark: #393a34; +$sl-color--code-muted: #656556; +$sl-color--code-base: #066; +$sl-color--code-cool: #458; +$sl-color--code-dark: black; +$sl-color--code-highlight-light: rgba($sl-color--warning-light, 0.2); + +// Playground Status +$sl-color--code-error: #cf0254; +$sl-color--code-error-light: color.adjust( + $sl-color--code-error, + $lightness: 55% ); -$sl-color--action-dark: color.adjust( - brand.$sl-color--bouquet, - $lightness: -11.647% +$sl-color--code-warning: #c14e00; +$sl-color--code-warning-light: color.adjust( + $sl-color--code-warning, + $lightness: 55% ); +$sl-color--code-info: #168073; +$sl-color--code-info-light: color.adjust($sl-color--code-info, $lightness: 65%); diff --git a/source/assets/sass/visual-design/_theme.scss b/source/assets/sass/visual-design/_theme.scss index 48f893fcc..108ac2b5e 100644 --- a/source/assets/sass/visual-design/_theme.scss +++ b/source/assets/sass/visual-design/_theme.scss @@ -32,6 +32,10 @@ body { color: var(--text, var(--sl-color--pale-sky)); } +.fade { + opacity: 0.7; +} + ::selection { background: var(--sl-color--iron); } diff --git a/source/blog/042-wide-gamut-colors-in-sass.md b/source/blog/042-wide-gamut-colors-in-sass.md new file mode 100644 index 000000000..b4d94bb3c --- /dev/null +++ b/source/blog/042-wide-gamut-colors-in-sass.md @@ -0,0 +1,385 @@ +--- +title: "Sass color spaces & wide gamut colors" +author: Miriam Suzanne +date: 2024-09-11 13:00:00 -8 +--- + +Wide gamut colors are coming to Sass! + +I should clarify. Wide gamut CSS color formats like `oklch(…)` and `color(display-p3 …)` have been available in all major browsers since May, 2023. But even before that, these new color formats were *allowed* in Sass. This is one of my favorite features of Sass: most new CSS *just works*, without any need for "official" support or updates. When Sass encounters unknown CSS, it passes that code along to the browser. Not everything needs to be pre-processed. + +Often, that's all we need. When Cascade Layers and Container Queries rolled out in browsers, there was nothing more for Sass to do. But the new CSS color formats are a bit different. Since colors are a first-class data type in Sass, we don't always want to pass them along *as-is*. We often want to manipulate and manage colors before they go to the browser. + +Already know all about color spaces? [Skip ahead to the new Sass features](#css-color-functions-in-sass)! + +## The color format trade-off + +CSS has historically been limited to `sRGB` color formats, which share two main features: + +- They use an underlying [RGB color model](https://en.wikipedia.org/wiki/RGB_color_model) for representing & manipulating colors mathematically by controlling the relative amounts of `red`, `green`, and `blue` light. +- They can only represent colors in the [`sRGB` color gamut](https://en.wikipedia.org/wiki/SRGB) -- the default range of color that can be displayed on color monitors since the mid 1990s. + +### Clear gamut boundaries + +The previously available formats in CSS -- named colors (e.g. `red`), `hex` colors (e.g. `#f00`), and color functions (e.g. `rgb()`/`rgba()`, `hsl()`/`hsla()`, and more recently `hwb()`) -- are all ways of describing `sRGB` colors. Named colors are special, but the other formats use a 'coordinate' system, as though the colors of the gamut were projected into 3d space: + +
+
+sRGB gamut rendered in sRGB space forms a rainbow colored cube +sRGB gamut rendered in hsl space forms a rainbow-edged cylinder with black at the bottom and white at the top +sRGB gamut rendered in hwb space forms a rainbow-core top surface with a black-to-gray bottom and gray-to-white outside edge +
+
+Images generated using +ColorAide +by Isaac Muse. +
+
+ +Look at those nice, geometric shapes! RGB gives us a rainbow cube, while HSL and HWB (with their "polar" `hue` channels) arrange those same colors into cylinders. The clean boundaries make it easy for us to know (mathematically) what colors are *in gamut* or *out of gamut*. In `rgb()` we use values of `0-255`. Anything inside that range will be inside the cube, but if a channel goes below `0` or above `255`, we're no longer inside the `sRGB` gamut. In `hsl()` and `hwb()` the `hue` coordinates can keep going around the circle without ever reaching escape velocity, but the `saturation`, `lightness`, `whiteness`, and `blackness` channels go cleanly from `0-1` or `0%-100%`. Again, anything outside that range is outside the color space. + +### Matching human perception + +But that simplicity comes with limitations. The most obvious is that monitors keep getting better. These days, many monitors can display colors beyond `sRGB`, especially extending the range of bright greens available. If we simply extend our shapes with the new colors available, we're no longer dealing with clean geometry! + +
+display-p3 gamut rendered in sRGB space adds unequal red and green horns to the sRGB cube +display-p3 gamut rendered in hsl space creates a boot-like bulge of green near the base of the hsl cylinder +
+ +The crisp edges and clean math of `sRGB` formats were only possible because we knew exactly what colors could be displayed, and we arranged those colors to fit perfectly into a box. But human color perception is not so clear-cut, and it doesn't align perfectly with the gamut of any monitors on the market. When we attempt to space all the same colors *evenly* based on human perception rather than simple math, we get an entirely different shape with swooping edges. This is the `display-p3` gamut in `oklch` space: + +display-p3 gamut rendered in oklch space forms a skewed cube with a conic black base + +The practical difference is particularly noticeable when we compare colors of the same 'lightness' in `hsl` vs `oklch`. Humans perceive yellow hues as lighter than blues. By scaling them to fit in the same range, `hsl` gives us a yellow that is much brighter than the blue: + +on the left a blue and much brighter yellow, on the right our yellow is much darker to match the blue tone + +## New CSS formats give us the choice + +Moving forward, there are two directions we could go with wide gamut colors: + +- Color formats that re-fit larger and larger gamuts into simple coordinates, stretching the colors to preserve clean, geometric boundaries. +- Color formats that maintain their *perceptually uniform* spacing, without any regard for specific gamuts. + +On the one hand, clean boundaries allow us to easily stay inside the range of available colors. Without those boundaries, it would be easy to *accidentally* request colors that aren't even physically possible. On the other hand, we expect these colors to be *perceived* by *other humans* -- and we need to make things *look* consistent, with enough contrast to be readable. + +The [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/) defines a number of new CSS color formats. Some of them maintain geometric access to specific color spaces. Like the more familiar `rgb()` and `hsl()` functions, the newer `hwb()` function still describes colors in the `sRGB` gamut, using `hue`, `whiteness`, and `blackness` channels. It's an interesting format, and [I've written about it before](https://www.miriamsuzanne.com/2022/06/29/hwb-clamping/). + +The rest of the gamut-bounded spaces are available using the `color( <3-channels> / )` function. Using that syntax we can define colors in `sRGB`, `srbg-linear`, `display-p3` (common for modern monitors), `a98-rgb`, `prophoto-rgb`, and `rec2020`. Each of these maps the specified gamut onto a range of (cubic) coordinates from `0-1` or `0%-100%`. Nice and clean. + +In the same `color()` function, we can also access the 'device independent' (and gamut-less) `xyz` color spaces -- often used as an international baseline for converting between different color models. I won't get into [white points](https://www.w3.org/TR/css-color-4/#white-point) here, but we can specify `xyz-d65` (the default) explicitly, or use `xyz-d50` instead. + +Working outwards from `xyz`, we get a number of new *theoretically unbounded* color formats -- prioritizing *perceptually uniform* distribution over clean geometry. These are available in functions of their own, including `lab()` (`lightness`, `a`, and `b`) and `lch()` (`lightness`, `chroma`, and `hue`) along with the newer 'ok' versions of each -- `oklab()` and `oklch()`. If you want the full history of these formats, [Eric Portis has written a great explainer](https://ericportis.com/posts/2024/okay-color-spaces/). + +## TL;DR top priority new formats + +For the color experts, it's great to have all this flexibility. For the rest of us, there are a few stand-out formats: + +- `color(display-p3 …)` provides access to a wider gamut of colors, which are available on many modern displays, while maintaining a clear set of gamut boundaries. +- `oklch(…)` is the most intuitive and perceptually uniform space to work in, a newer alternative to `hsl(…)` -- `chroma` is very similar to `saturation`. But there are few guard rails here, and it's easy to end up outside the gamuts that any screen can possibly display. The coordinate system is still describing a cylinder, but the edges of human perception and display technology don't map neatly into that space. +- For transitions and gradients, if we want to go directly between hues (instead of going around the color wheel), `oklab(…)` is a good linear option. Usually, a transition or gradient between two in-gamut colors will stay in gamut -- but we can't always rely on that when we're dealing with extremes of saturation or lightness. + +## CSS color functions in Sass + +Sass now accepts all the new CSS formats, and treats them as first-class *colors* that we can manipulate, mix, convert, and inspect. These functions are all available globally: + +- `lab()`, `oklab()`, `lch()`, and `oklch()` +- `color()` using the `sRGB`, `srgb-linear`, `display-p3`, `a98-rgb`, `prophoto-rgb`, `rec2020`, `xyz`, `xyz-d65`, and `xyz-d50` color spaces +- `hwb()` (Sass previously had a `color.hwb()` function, which is now deprecated in favor of the global function) + +The Sass color functions use the same syntax as the CSS functions, which means that a given color can be represented in a variety of different spaces. For example, these are all the same color: + +{% codeExample 'color-fns', false %} + @debug MediumVioletRed; + @debug #C71585; + @debug hsl(322.2 80.91% 43.14%); + @debug oklch(55.34% 0.2217 349.7); + @debug color(display-p3 0.716 0.1763 0.5105); + === + @debug MediumVioletRed + @debug #C71585 + @debug hsl(322.2 80.91% 43.14%) + @debug oklch(55.34% 0.2217 349.7) + @debug color(display-p3 0.716 0.1763 0.5105) +{% endcodeExample %} + +## Sass colors hold their space + +Historically, both CSS and Sass would treat the different color-spaces as *interchangeable*. When all the color formats describe the same color gamut using the same underlying model, you can provide a color using `hsl()` syntax, and the parser can eagerly convert it to `rgb()` without risking any data loss. That's no longer the case for modern color spaces. + +In general, any color defined in a given space will remain in that space, and be emitted in that space. The space is defined by the function used, either one of the named spaced passed to `color()`, or the function name (e.g. `lab` for colors defined using the `lab()` function). + +However, the `rgb`, `hsl`, and `hwb` spaces are considered "legacy spaces", and often get special handling for the sake of backwards compatibility. Legacy colors are still emitted in the most backwards-compatible format available. This matches CSS’s own backwards-compatibility behavior. Colors defined using hex notation or CSS color names are also considered part of the legacy `rgb` color space. + +Sass provides a variety of tools for inspecting and working with these color spaces: + +- We can inspect the space of a color using `color.space($color)` +- We can ask if the color is in a legacy space with `color.is-legacy($color)` +- We can *convert* a color from one space to another using `color.to-space($color, $space)` + +All of these functions are provided by the built-in [Sass Color Module](https://sass-lang.com/documentation/modules/color/): + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $brand: MediumVioletRed; + + // results: rgb, true + @debug color.space($brand); + @debug color.is-legacy($brand); + + // result: oklch(55.34% 0.2217 349.7) + @debug color.to-space($brand, 'oklch'); + + // results: oklch, false + @debug color.space($brand); + @debug color.is-legacy($brand); + === + @use 'sass:color' + $brand: MediumVioletRed + + // results: rgb, true + @debug color.space($brand) + @debug color.is-legacy($brand) + + // result: oklch(55.34% 0.2217 349.7) + @debug color.to-space($brand, 'oklch') + + // results: oklch, false + @debug color.space($brand) + @debug color.is-legacy($brand) +{% endcodeExample %} + +Once we convert a color between spaces, we no longer consider those colors to be *equal*. But we can ask if they would render as 'the same' color, using the `color.same()` function: + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $orange-rgb: #ff5f00; + $orange-oklch: oklch(68.72% 20.966858279% 41.4189852913deg); + + // result: false + @debug $orange-rgb == $orange-oklch; + + // result: true + @debug color.same($orange-rgb, $orange-oklch); + === + @use 'sass:color' + $orange-rgb: #ff5f00 + $orange-oklch: oklch(68.72% 20.966858279% 41.4189852913deg) + + // result: false + @debug $orange-rgb == $orange-oklch + + // result: true + @debug color.same($orange-rgb, $orange-oklch) +{% endcodeExample %} + +We can inspect the individual channels of a color using `color.channel()`. By default, it only supports channels that are available in the color's own space, but we can pass the `$space` parameter to return the value of the channel value after converting to the given space: + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $brand: hsl(0 100% 25.1%); + + // result: 25.1% + @debug color.channel($brand, "lightness"); + + // result: 37.67% + @debug color.channel($brand, "lightness", $space: oklch); + === + @use 'sass:color' + $brand: hsl(0 100% 25.1%) + + // result: 25.1% + @debug color.channel($brand, "lightness") + + // result: 37.67% + @debug color.channel($brand, "lightness", $space: oklch) +{% endcodeExample %} + +CSS has also introduced the concept of 'powerless' and 'missing' color channels. For example, an `hsl` color with `0%` saturation will *always be grayscale*. In that case, we can consider the `hue` channel to be powerless. Changing its value won't have any impact on the resulting color. Sass allows us to ask if a channel is powerless using the `color.is-powerless()` function: + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $gray: hsl(0 0% 60%); + + // result: true, because saturation is 0 + @debug color.is-powerless($gray, "hue"); + + // result: false + @debug color.is-powerless($gray, "lightness"); + === + @use 'sass:color' + $gray: hsl(0 0% 60%) + + // result: true, because saturation is 0 + @debug color.is-powerless($gray, "hue") + + // result: false + @debug color.is-powerless($gray, "lightness") +{% endcodeExample %} + +Taking that a step farther, CSS also allows us to explicitly mark a channel as 'missing' or unknown. That can happen automatically if we convert a color like `gray` into a color space like `oklch` -- we don't have any information about the `hue`. We can also create colors with missing channels explicitly by using the `none` keyword, and inspect if a color channel is missing with the `color.is-missing()` function: + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $brand: hsl(none 100% 25.1%); + + // result: false + @debug color.is-missing($brand, "lightness"); + + // result: true + @debug color.is-missing($brand, "hue"); + === + @use 'sass:color' + $brand: hsl(none 100% 25.1%) + + // result: false + @debug color.is-missing($brand, "lightness") + + // result: true + @debug color.is-missing($brand, "hue") +{% endcodeExample %} + +Like CSS, Sass maintains missing channels where they can be meaningful, but treats them as a value of `0` when a channel value is required. + +## Manipulating Sass colors + +The existing `color.scale()`, `color.adjust()`, and `color.change()` functions will continue to work as expected. By default, all color manipulations are performed *in the space provided by the color*. But we can now also specify an explicit color space for transformations: + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $brand: hsl(0 100% 25.1%); + + // result: hsl(0 100% 43.8%) + @debug color.scale($brand, $lightness: 25%); + + // result: hsl(5.76 56% 45.4%) + @debug color.scale($brand, $lightness: 25%, $space: oklch); + === + @use 'sass:color' + $brand: hsl(0 100% 25.1%) + + // result: hsl(0 100% 43.8%) + @debug color.scale($brand, $lightness: 25%) + + // result: hsl(5.76 56% 45.4%) + @debug color.scale($brand, $lightness: 25%, $space: oklch) +{% endcodeExample %} + +Note that the returned color is still returned in the original color space, even when the adjustment is performed in a different space. That way we can start to use more advanced color spaces like `oklch` where they are useful, without necessarily relying on browsers to support those formats. + +The existing `color.mix()` function will also maintain existing behavior *when both colors are in legacy color spaces*. Legacy mixing is always done in `rgb` space. We can opt into other mixing techniques using the new `$method` parameter, which is designed to match the CSS specification for describing [interpolation methods](https://www.w3.org/TR/css-color-4/#interpolation-space) – used in CSS gradients, filters, animations, and transitions as well as the new CSS `color-mix()` function. + +For legacy colors, the method is optional. But for non-legacy colors, a method is required. In most cases, the method can simply be a color space name. But when we're using a color space with "polar hue" channel (such as `hsl`, `hwb`, `lch`, or `oklch`) we can also specify the *direction* we want to move around the color wheel: `shorter hue`, `longer hue`, `increasing hue`, or `decreasing hue`: + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + + // result: #660099 + @debug color.mix(red, blue, 40%); + + // result: rgb(176.2950613593, -28.8924497904, 159.1757183525) + @debug color.mix(red, blue, 40%, $method: lab); + + // result: rgb(-129.55249236, 149.0291922672, 77.9649510422) + @debug color.mix(red, blue, 40%, $method: oklch longer hue); + === + @use 'sass:color' + + // result: #660099 + @debug color.mix(red, blue, 40%) + + // result: rgb(176.2950613593, -28.8924497904, 159.1757183525) + @debug color.mix(red, blue, 40%, $method: lab) + + // result: rgb(-129.55249236, 149.0291922672, 77.9649510422) + @debug color.mix(red, blue, 40%, $method: oklch longer hue) +{% endcodeExample %} + + +In this case, the first color in the mix is considered the "origin" color. Like the other functions above, we can use different spaces for mixing, but the result will always be returned in that origin color space. + +## Working with gamut boundaries + +So what happens when you go outside the gamut of a given display? Browsers are still debating the details, but everyone agrees we have to display *something*: + +- Currently, browsers convert every color into `red`, `green`, and `blue` channels for display. If any of those channels are too high or two low for a given screen, they get *clamped* at the highest or lowest value allowed. This is often referred to as 'channel clipping'. It keeps the math simple, but it can have a weird effect on both the `hue` and `lightness` if some channels are clipped more than others. +- The CSS specification says that preserving `lightness` should be the highest priority, and provides an algorithm for reducing `chroma` until the color is in gamut. That's great for maintaining readable text, but it's more work for browsers, and it can be surprising when colors suddenly lose their vibrance. +- There's been some progress on a compromise approach, reducing `chroma` to get colors inside the `rec2020` gamut, and clipping from there. + +Since browser behavior is still unreliable, and some color spaces (*cough* `oklch`) can easily launch us out of any available gamut, it can be helpful to do some gamut management in Sass. + +We can use `color.is-in-gamut()` to test if a particular color is in a given gamut. Like our other color functions, this will default to the space the color is defined in, but we can provide a `$space` parameter to test it against a different gamut: + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $extra-pink: color(display-p3 0.951 0.457 0.7569); + + // result: true, for display-p3 gamut + @debug color.is-in-gamut($extra-pink); + + // result: false, for srgb gamut + @debug color.is-in-gamut($extra-pink, $space: srgb); + === + @use 'sass:color' + $extra-pink: color(display-p3 0.951 0.457 0.7569) + + // result: true, for display-p3 gamut + @debug color.is-in-gamut($extra-pink) + + // result: false, for srgb gamut + @debug color.is-in-gamut($extra-pink, $space: srgb) +{% endcodeExample %} + +We can also use the `color.to-gamut()` function to explicitly move a color so that it is in a particular gamut. Since there are several options on the table, and no clear sense what default CSS will use long-term, this function currently requires an explicit `$method` parameter. The current options are `clip` (as is currently applied by browsers) or `local-minde` (as is currently specified): + +{% codeExample 'color-fns', false %} + @use 'sass:color'; + $extra-pink: oklch(90% 90% 0deg); + + // result: oklch(68.3601568298% 0.290089749 338.3604392249deg) + @debug color.to-gamut($extra-pink, srgb, clip); + + // result: oklch(88.7173946522% 0.0667320674 355.3282956627deg) + @debug color.to-gamut($extra-pink, srgb, local-minde); + === + @use 'sass:color' + $extra-pink: oklch(90% 90% 0deg) + + // result: oklch(68.3601568298% 0.290089749 338.3604392249deg) + @debug color.to-gamut($extra-pink, srgb, clip) + + // result: oklch(88.7173946522% 0.0667320674 355.3282956627deg) + @debug color.to-gamut($extra-pink, srgb, local-minde) +{% endcodeExample %} + +All legacy and RGB-style spaces represent bounded gamuts of color. Since mapping colors into gamut is a lossy process, it should generally be left to browsers or done with caution. For that reason, out-of-gamut channel values are maintained by Sass, even when converting into gamut-bounded color spaces. + +Legacy browsers require colors in the `srgb` gamut. However, most modern displays support the wider `display-p3` gamut. + + +## Deprecated functions + +A number of existing functions only make sense for legacy colors, and so are being deprecated in favor of color-space-friendly functions like `color.channel()` and `color.adjust()`. Eventually these will be removed from Sass entirely, but all the same functionality is still available in the updated functions: + +- `color.red()` +- `color.green()` +- `color.blue()` +- `color.hue()` +- `color.saturation()` +- `color.lightness()` +- `color.whiteness()` +- `color.blackness()` +- `adjust-hue()` +- `saturate()` +- `desaturate()` +- `transparentize()`/`fade-out()` +- `opacify()`/`fade-in()` +- `lighten()`/`darken()` + +We've added [a migrator](/documentation/cli/migrator#color) to automatically +convert these legacy functions to the color-space-friendly ones. + +```shellsession +$ sass-migrator color --migrate-deps +``` diff --git a/source/documentation/breaking-changes/color-functions.md b/source/documentation/breaking-changes/color-functions.md new file mode 100644 index 000000000..b1ed27bc2 --- /dev/null +++ b/source/documentation/breaking-changes/color-functions.md @@ -0,0 +1,123 @@ +--- +title: 'Breaking Change: Color Functions' +introduction: > + Certain color functions that were designed with the assumption that all colors + were mutually compatible no longer make sense now that Sass supports all the + color spaces of CSS Color 4. +--- + +Historically, all Sass color values covered the same gamut: whether the colors +were defined as RGB, HSL, or HWB, they only covered [the `sRGB` gamut] and could +only represent the colors that monitors could display since the mid-1990s. When +Sass added its original set of color functions, they assumed that all colors +could be freely converted between any of these representations and that there +was a single unambiguous meaning for each channel name like "red" or "hue". + +[the `sRGB` gamut]: https://en.wikipedia.org/wiki/SRGB + +The release of [CSS Color 4] changed all that. It added support for many new +color spaces with different (wider) gamuts than `sRGB`. In order to support +these colors, Sass had to rethink the way color functions worked. In addition to +adding new functions like [`color.channel()`] and [`color.to-space()`], a number +of older functions were deprecated when they were based on assumptions that no +longer held true. + +[CSS Color 4]: https://developer.mozilla.org/en-US/blog/css-color-module-level-4/ + +[`color.channel()`]: /documentation/modules/color/#channel +[`color.to-space()`]: /documentation/modules/color/#to-space + +### Old Channel Functions + +Channel names are now ambiguous across color spaces. The legacy RGB space has a +`red` channel, but so do `display-p3`, `rec2020`, and many more. This means that +[`color.red()`], [`color.green()`], [`color.blue()`], [`color.hue()`], +[`color.saturation()`], [`color.lightness()`], [`color.whiteness()`], +[`color.blackness()`], [`color.alpha()`], and [`color.opacity()`] will be +removed. Instead, you can use the [`color.channel()`] function to get the value +of a specific channel, usually with an explicit `$space` argument to indicate +which color space you're working with. + +[`color.red()`]: /documentation/modules/color/#red +[`color.green()`]: /documentation/modules/color/#green +[`color.blue()`]: /documentation/modules/color/#blue +[`color.hue()`]: /documentation/modules/color/#hue +[`color.saturation()`]: /documentation/modules/color/#saturation +[`color.lightness()`]: /documentation/modules/color/#lightness +[`color.whiteness()`]: /documentation/modules/color/#whiteness +[`color.blackness()`]: /documentation/modules/color/#blackness +[`color.alpha()`]: /documentation/modules/color/#alpha +[`color.opacity()`]: /documentation/modules/color/#opacity + +{% codeExample 'channel', false %} + @use "sass:color"; + + $color: #c71585; + @debug color.channel($color, "red", $space: rgb); + @debug color.channel($color, "red", $space: display-p3); + @debug color.channel($color, "hue", $space: oklch); + === + @use "sass:color" + + $color: #c71585 + @debug color.channel($color, "red", $space: rgb) + @debug color.channel($color, "red", $space: display-p3) + @debug color.channel($color, "hue", $space: oklch) +{% endcodeExample %} + +### Single-Channel Adjustment Functions + +These have the same ambiguity problem as the old channel functions, while _also_ +already being redundant with [`color.adjust()`] even before Color 4 support was +added. Not only that, it's often better to use [`color.scale()`] anyway, because +it's better suited for making changes relative to the existing color rather than +in absolute terms. This means that [`adjust-hue()`], [`saturate()`], +[`desaturate()`], [`lighten()`], [`darken()`], [`opacify()`], [`fade-in()`], +[`transparentize()`], and [`fade-out()`] will be removed. Note that these +functions never had module-scoped counterparts because their use was already +discouraged. + +[`color.adjust()`]: /documentation/modules/color/#adjust +[`color.scale()`]: /documentation/modules/color/#scale +[`adjust-hue()`]: /documentation/modules/color/#adjust-hue +[`saturate()`]: /documentation/modules/color/#saturate +[`desaturate()`]: /documentation/modules/color/#desaturate +[`lighten()`]: /documentation/modules/color/#lighten +[`darken()`]: /documentation/modules/color/#darken +[`opacify()`]: /documentation/modules/color/#opacify +[`fade-in()`]: /documentation/modules/color/#fade-in +[`transparentize()`]: /documentation/modules/color/#transparentize +[`fade-out()`]: /documentation/modules/color/#fade-out + +{% codeExample 'adjust', false %} + @use "sass:color"; + + $color: #c71585; + @debug color.adjust($color, $lightness: 15%, $space: hsl); + @debug color.adjust($color, $lightness: 15%, $space: oklch); + @debug color.scale($color, $lightness: 15%, $space: oklch); + === + @use "sass:color" + + $color: #c71585 + @debug color.adjust($color, $lightness: 15%, $space: hsl) + @debug color.adjust($color, $lightness: 15%, $space: oklch) + @debug color.scale($color, $lightness: 15%, $space: oklch) +{% endcodeExample %} + +## Transition Period + +{% compatibility 'dart: "1.79.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + +First, we'll emit deprecation warnings for all uses of the functions that are +slated to be removed. In Dart Sass 2.0.0, these functions will be removed +entirely. Attempts to call the module-scoped versions will throw an error, while +the global functions will be treated as plain CSS functions and emitted as plain +strings. + +You can use [the Sass migrator] to automatically migrate from the deprecated +APIs to their new replacements. + +[the Sass migrator]: https://sass-lang.com/documentation/cli/migrator/#color + +{% render 'silencing_deprecations' %} diff --git a/source/documentation/breaking-changes/legacy-js-api.md b/source/documentation/breaking-changes/legacy-js-api.md new file mode 100644 index 000000000..1bfd64c05 --- /dev/null +++ b/source/documentation/breaking-changes/legacy-js-api.md @@ -0,0 +1,99 @@ +--- +title: "Breaking Change: Legacy JS API" +introduction: | + Dart Sass originally used an API based on the one used by Node Sass, but + replaced it with a new, modern API in Dart Sass 1.45.0. The legacy JS API is + now deprecated and will be removed in Dart Sass 2.0.0. +--- + +## Migrating Usage + +### Entrypoints + +The legacy JS API had two entrypoints for compiling Sass: `render` and +`renderSync`, which took in an options object that included either `file` (to +compile a file) or `data` (to compile a string). The modern API has four: +`compile` and `compileAsync` for compiling a file and `compileString` and +`compileStringAsync` for compiling a string. These functions take a path or +source string as their first argument and then an object of all other options +as their second argument. Unlike `render`, which used a callback, `compileAsync` +and `compileStringAsync` return a promise instead. + +See the [usage documentation] for more details. + +[usage documentation]: /documentation/js-api/#md:usage + +### Importers + +Importers in the legacy API consisted of a single function that took in the +dependency rule URL and the URL of the containing stylesheet (as well as a +`done` callback for asynchronous importers) and return an object with either +a `file` path on disk or the `contents` of the stylesheet to be loaded. + +Modern API [`Importer`]s instead contain two methods: `canonicalize`, which takes +in a rule URL and returns the canonical form of that URL; and `load`, which +takes in a canonical URL and returns an object with the contents +of the loaded stylesheet. This split ensures that the same module is only +loaded once and that relative URLs work consistently. Asynchronous importers +have both of these methods return promises. + +There's also a special [`FileImporter`] that redirects all loads to existing +files on disk, which should be used when migrating from legacy importers that +returned a `file` instead of `contents`. + +[`Importer`]: /documentation/js-api/interfaces/Importer/ +[`ImporterResult`]: /documentation/js-api/interfaces/ImporterResult/ +[`FileImporter`]: /documentation/js-api/interfaces/FileImporter/ + +### Custom Functions + +In the legacy JS API, custom functions took a separate JS argument for each +Sass argument, with an additional `done` callback for asynchronous custom +functions. In the modern API, custom functions instead take a single JS argument +that contains a list of all Sass arguments, with asynchronous custom functions +returning a promise. + +The modern API also uses a much more robust [`Value`] class that supports all +Sass value stypes, type assertions, and easy map and list lookups. + +[`Value`]: /documentation/js-api/classes/Value/ + +### Bundlers + +If you're using a bundler or other tool that calls the Sass API rather than +using it directly, you may need to change the configuration you pass to that +tool to tell it to use the modern API. + +Webpack should already use the modern API by default, but if you're getting +warnings, set `api` to `"modern"` or `"modern-compiler"`. +See [Webpack's documentation] for more details. + +Vite still defaults to the legacy API, but you can similarly switch it by +setting `api` to `"modern"` or `"modern-compiler"`. See [Vite's documentation] +for more details. + +For other tools, check their documentation or issue tracker for information +about supporting the modern Sass API. + +[Webpack's documentation]: https://webpack.js.org/loaders/sass-loader/#api +[Vite's documentation]: https://vitejs.dev/config/shared-options.html#css-preprocessoroptions + +## Silencing Warnings + +While the legacy JS API was marked as deprecated in Dart Sass 1.45.0 alongside +the release of the modern API, we began emitting warnings for using it starting +in Dart Sass 1.79.0. If you're not yet able to migrate to the modern API but +want to silence the warnings for now, you can pass `legacy-js-api` in the +`silenceDeprecations` option: + +```js +const sass = require('sass'); + +const result = sass.renderSync({ + silenceDeprecations: ['legacy-js-api'], + ... +}); +``` + +This will silence the warnings for now, but the legacy API will be removed +entirely in Dart Sass 2.0.0, so you should still plan to migrate off of it soon. diff --git a/source/documentation/cli/migrator.md b/source/documentation/cli/migrator.md index fc783fc77..712f75388 100644 --- a/source/documentation/cli/migrator.md +++ b/source/documentation/cli/migrator.md @@ -198,6 +198,11 @@ Migrating style.scss ## Migrations +### Color + +This migration converts legacy color functions to the new color-space-compatible +functions. + ### Division This migration converts stylesheets that use [`/` as division] to use the diff --git a/source/documentation/modules/color.md b/source/documentation/modules/color.md index 29c7a6f8b..7b374c532 100644 --- a/source/documentation/modules/color.md +++ b/source/documentation/modules/color.md @@ -9,68 +9,666 @@ title: sass:color $red: null, $green: null, $blue: null, $hue: null, $saturation: null, $lightness: null, $whiteness: null, $blackness: null, - $alpha: null) + $x: null, $y: null, $z: null, + $chroma: null, + $alpha: null, + $space: null) {% endcapture %} {% function color_adjust, 'adjust-color(...)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$x, $y, $z, $chroma, and $space"' %}{% endcompatibility %} {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false', 'feature: "$whiteness and $blackness"' %}{% endcompatibility %} - Increases or decreases one or more properties of `$color` by fixed amounts. + Increases or decreases one or more channels of `$color` by fixed amounts. - Adds the value passed for each keyword argument to the corresponding property - of the color, and returns the adjusted color. It's an error to specify an RGB - property (`$red`, `$green`, and/or `$blue`) at the same time as an HSL - property (`$hue`, `$saturation`, and/or `$lightness`), or either of those at - the same time as an [HWB][] property (`$hue`, `$whiteness`, and/or - `$blackness`). + Adds the value passed for each keyword argument to the corresponding channel + of the color, and returns the adjusted color. By default, this can only adjust + channels in `$color`'s space, but a different color space can be passed as + `$space` to adjust channels there instead. This always returns a color in the + same space as `$color`. - [HWB]: https://en.wikipedia.org/wiki/HWB_color_model + {% headsUp %} + For historical reasons, if `$color` is in a [legacy color space], _any_ + legacy color space channels can be adjusted. However, it's an error to + specify an RGB channel (`$red`, `$green`, and/or `$blue`) at the same time + as an HSL channel (`$hue`, `$saturation`, and/or `$lightness`), or either of + those at the same time as an [HWB] channel (`$hue`, `$whiteness`, and/or + `$blackness`). - All optional arguments must be numbers. The `$red`, `$green`, and `$blue` - arguments must be [unitless][] and between -255 and 255 (inclusive). The - `$hue` argument must have either the unit `deg` or no unit. The `$saturation`, - `$lightness`, `$whiteness`, and `$blackness` arguments must be between `-100%` - and `100%` (inclusive), and may not be unitless. The `$alpha` argument must be - unitless and between -1 and 1 (inclusive). + [legacy color space]: /documentation/values/colors#legacy-color-spaces + [HWB]: https://en.wikipedia.org/wiki/HWB_color_model - [unitless]: /documentation/values/numbers#units + Even so, it's a good idea to pass `$space` explicitly even for legacy colors. + {% endheadsUp %} + + All channel arguments must be numbers, and must be units that could be passed + for those channels in the color space's constructor. If the existing channel + value plus the adjustment value is outside the channel's native range, it's + clamped for: + + * red, green, and blue channels for the `rgb` space; + * lightness channel for the `lab`, `lch`, `oklab`, and `oklch` spaces; + * the lower bound of the saturation and chroma channels for the `hsl`, `lch`, + and `oklch` spaces; + * and the alpha channel for all spaces. See also: * [`color.scale()`](#scale) for fluidly scaling a color's properties. * [`color.change()`](#change) for setting a color's properties. - {% codeExample 'adjust-color' %} + {% codeExample 'adjust-color', false %} @use 'sass:color'; @debug color.adjust(#6b717f, $red: 15); // #7a717f - @debug color.adjust(#d2e1dd, $red: -10, $blue: 10); // #c8e1e7 - @debug color.adjust(#998099, $lightness: -30%, $alpha: -0.4); // rgba(71, 57, 71, 0.6) + @debug color.adjust(lab(40% 30 40), $lightness: 10%, $a: -20); // lab(50% 10 40) + @debug color.adjust(#d2e1dd, $hue: 45deg, $space: oklch); + // rgb(209.7987626149, 223.8632000471, 229.3988769575) === @use 'sass:color' @debug color.adjust(#6b717f, $red: 15) // #7a717f - @debug color.adjust(#d2e1dd, $red: -10, $blue: 10) // #c8e1e7 - @debug color.adjust(#998099, $lightness: -30%, $alpha: -0.4) // rgba(71, 57, 71, 0.6) + @debug color.adjust(lab(40% 30 40), $lightness: 10%, $a: -20) // lab(50% 10 40) + @debug color.adjust(#d2e1dd, $hue: 45deg, $space: oklch) + // rgb(209.7987626149, 223.8632000471, 229.3988769575) + {% endcodeExample %} +{% endfunction %} + +{% capture color_change %} + color.change($color, + $red: null, $green: null, $blue: null, + $hue: null, $saturation: null, $lightness: null, + $whiteness: null, $blackness: null, + $x: null, $y: null, $z: null, + $chroma: null, + $alpha: null, + $space: null) +{% endcapture %} + +{% function color_change, 'change-color(...)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$x, $y, $z, $chroma, and $space"' %}{% endcompatibility %} + {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false', 'feature: "$whiteness and $blackness"' %}{% endcompatibility %} + + Sets one or more channels of a color to new values. + + Uses the value passed for each keyword argument in place of the corresponding + color channel, and returns the changed color. By default, this can only change + channels in `$color`'s space, but a different color space can be passed as + `$space` to adjust channels there instead. This always returns a color in the + same space as `$color`. + + {% headsUp %} + + For historical reasons, if `$color` is in a [legacy color space], _any_ + legacy color space channels can be changed. However, it's an error to + specify an RGB channel (`$red`, `$green`, and/or `$blue`) at the same time + as an HSL channel (`$hue`, `$saturation`, and/or `$lightness`), or either + of those at the same time as an [HWB] channel (`$hue`, `$whiteness`, and/or + `$blackness`). + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + [HWB]: https://en.wikipedia.org/wiki/HWB_color_model + + Even so, it's a good idea to pass `$space` explicitly even for legacy colors. + {% endheadsUp %} + + All channel arguments must be numbers, and must be units that could be passed + for those channels in the color space's constructor. Channels are never + clamped for `color.change()`. + + See also: + + * [`color.scale()`](#scale) for fluidly scaling a color's properties. + * [`color.adjust()`](#adjust) for adjusting a color's properties by fixed + amounts. + + {% codeExample 'color-change', false %} + @use 'sass:color'; + + @debug color.change(#6b717f, $red: 100); // #64717f + @debug color.change(color(srgb 0 0.2 0.4), $red: 0.8, $blue: 0.1); + // color(srgb 0.8 0.1 0.4) + @debug color.change(#998099, $lightness: 30%, $space: oklch); + // rgb(58.0719961509, 37.2631531594, 58.4201613409) + === + @use 'sass:color' + + @debug color.change(#6b717f, $red: 100) // #64717f + @debug color.change(color(srgb 0 0.2 0.4), $red: 0.8, $blue: 0.1) + // color(srgb 0.8 0.1 0.4) + @debug color.change(#998099, $lightness: 30%, $space: oklch) + // rgb(58.0719961509, 37.2631531594, 58.4201613409) + {% endcodeExample %} +{% endfunction %} + +{% function 'color.channel($color, $channel, $space: null)', 'returns:number' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$space"' %}{% endcompatibility %} + + Returns the value of `$channel` in `$space`, which defaults to `$color`'s + space. The `$channel` must be a quoted string, and the `$space` must be an + unquoted string. + + This returns a number with unit `deg` for the `hue` channel of the `hsl`, + `hwb`, `lch`, and `oklch` spaces. It returns a number with unit `%` for the + `saturation`, `lightness`, `whiteness`, and `blackness` channels of the `hsl`, + `hwb`, `lab`, `lch`, `oklab`, and `oklch` spaces. For all other channels, it + returns a unitless number. + + This will return `0` (possibly with an appropriate unit) if the `$channel` is + missing in `$color`. You can use [`color.is-missing()`] to check explicitly + for missing channels. + + [`color.is-missing()`]: #is-missing + + {% codeExample 'color-channel', false %} + @use 'sass:color'; + + @debug color.channel(hsl(80deg 30% 50%), "hue"); // 80deg + @debug color.channel(hsl(80deg 30% 50%), "hue", $space: oklch); // 124.279238779deg + @debug color.channel(hsl(80deg 30% 50%), "red", $space: rgb); // 140.25 + === + @use 'sass:color' + + @debug color.channel(hsl(80deg 30% 50%), "hue") // 80deg + @debug color.channel(hsl(80deg 30% 50%), "hue", $space: oklch) // 124.279238779deg + @debug color.channel(hsl(80deg 30% 50%), "red", $space: rgb) // 140.25 + {% endcodeExample %} +{% endfunction %} + +{% function 'color.complement($color, $space: null)', 'complement($color, $space: null)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$space"' %}{% endcompatibility %} + + Returns the [complement] of `$color` in `$space`. + + [complement]: https://en.wikipedia.org/wiki/Complementary_colors + + This rotates `$color`'s hue by `180deg` in `$space`. This means that `$space` + has to be a polar color space: `hsl`, `hwb`, `lch`, or `oklch`. It always + returns a color in the same space as `$color`. + + {% headsUp %} + For historical reasons, `$space` is optional if `$color` is in a [legacy + color space]. In that case, `$space` defaults to `hsl`. It's always a good + idea to pass `$space` explicitly regardless. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + {% endheadsUp %} + + {% codeExample 'color-complement', false %} + @use 'sass:color'; + + // HSL hue 222deg becomes 42deg. + @debug color.complement(#6b717f); // #7f796b + + // Oklch hue 267.1262408996deg becomes 87.1262408996deg + @debug color.complement(#6b717f, oklch); + // rgb(118.8110604298, 112.5123650034, 98.1616586336) + + // Hue 70deg becomes 250deg. + @debug color.complement(oklch(50% 0.12 70deg), oklch); // oklch(50% 0.12 250deg) + === + @use 'sass:color' + + // HSL hue 222deg becomes 42deg. + @debug color.complement(#6b717f) // #7f796b + + // Oklch hue 267.1262408996deg becomes 87.1262408996deg + @debug color.complement(#6b717f, oklch) + // rgb(118.8110604298, 112.5123650034, 98.1616586336) + + // Hue 70deg becomes 250deg. + @debug color.complement(oklch(50% 0.12 70deg), oklch) // oklch(50% 0.12 250deg) + {% endcodeExample %} +{% endfunction %} + +{% function 'color.grayscale($color)', 'grayscale($color)', 'returns:color' %} + Returns a gray color with the same lightness as `$color`. + + If `$color` is in a [legacy color space], this sets the HSL saturation to 0%. + Otherwise, it sets the Oklch chroma to 0%. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + + {% codeExample 'color-grayscale', false %} + @use 'sass:color'; + + @debug color.grayscale(#6b717f); // #757575 + @debug color.grayscale(color(srgb 0.4 0.2 0.6)); // color(srgb 0.3233585271 0.3233585411 0.3233585792) + @debug color.grayscale(oklch(50% 80% 270deg)); // oklch(50% 0% 270deg) + === + @use 'sass:color' + + @debug color.grayscale(#6b717f) // #757575 + @debug color.grayscale(color(srgb 0.4 0.2 0.6)) // color(srgb 0.3233585271 0.3233585411 0.3233585792) + @debug color.grayscale(oklch(50% 80% 270deg)) // oklch(50% 0% 270deg) + {% endcodeExample %} +{% endfunction %} + +{% function 'color.ie-hex-str($color)', 'ie-hex-str($color)', 'returns:unquoted string' %} + Returns an unquoted string that represents `$color` in the `#AARRGGBB` format + expected by Internet Explorer's [`-ms-filter`] property. + + [`-ms-filter`]: https://learn.microsoft.com/en-us/previous-versions/ms530752(v=vs.85) + + If `$color` isn't already in the `rgb` color space, it's converted to `rgb` + and gamut-mapped if necessary. The specific gamut-mapping algorithm may change + in future Sass versions as the state of the art improves; currently, + [`local-minde`] is used. + + [`local-minde`]: #to-gamut + + {% codeExample 'color-ie-hex-str', false %} + @use 'sass:color'; + + @debug color.ie-hex-str(#b37399); // #FFB37399 + @debug color.ie-hex-str(rgba(242, 236, 228, 0.6)); // #99F2ECE4 + @debug color.ie-hex-str(oklch(70% 10% 120deg)); // #FF9BA287 + === + @use 'sass:color' + + @debug color.ie-hex-str(#b37399) // #FFB37399 + @debug color.ie-hex-str(rgba(242, 236, 228, 0.6)) // #99F2ECE4 + @debug color.ie-hex-str(oklch(70% 10% 120deg)) // #FF9BA287 + {% endcodeExample %} +{% endfunction %} + +{% function 'color.invert($color, $weight: 100%, $space: null)', 'invert($color, $weight: 100%, $space: null)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$space"' %}{% endcompatibility %} + + Returns the inverse or [negative] of `$color` in `$space`. + + [negative]: https://en.wikipedia.org/wiki/Negative_(photography) + + The `$weight` must be a number between `0%` and `100%` (inclusive). A higher + weight means the result will be closer to the negative, and a lower weight + means it will be closer to `$color`. Weight `50%` will always produce a + medium-lightness gray in `$space`. + + {% headsUp %} + For historical reasons, `$space` is optional if `$color` is in a [legacy + color space]. In that case, `$space` defaults to `$color`'s own space. It's + always a good idea to pass `$space` explicitly regardless. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + {% endheadsUp %} + + {% codeExample 'color-invert', false %} + @use 'sass:color'; + + @debug color.invert(#b37399, $space: rgb); // #4c8c66 + @debug color.invert(#550e0c, 20%, $space: display-p3); // rgb(103.4937692017, 61.3720912206, 59.430641338) + === + @use 'sass:color'; + + @debug color.invert(#b37399, $space: rgb) // #4c8c66 + @debug color.invert(#550e0c, 20%, $space: display-p3) // rgb(103.4937692017, 61.3720912206, 59.430641338) {% endcodeExample %} {% endfunction %} +{% function 'color.is-legacy($color)', 'returns:boolean' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$space"' %}{% endcompatibility %} + +Returns whether `$color` is in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + + {% codeExample 'color-is-legacy', false %} + @use 'sass:color'; + + @debug color.is-legacy(#b37399); // true + @debug color.is-legacy(hsl(90deg 30% 90%)); // true + @debug color.is-legacy(oklch(70% 10% 120deg)); // false + === + @use 'sass:color' + + @debug color.is-legacy(#b37399) // true + @debug color.is-legacy(hsl(90deg 30% 90%)) // true + @debug color.is-legacy(oklch(70% 10% 120deg)) // false + {% endcodeExample %} +{% endfunction %} + +{% function 'color.is-missing($color, $channel)', 'returns:boolean' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$space"' %}{% endcompatibility %} + +Returns whether `$channel` is [missing] in `$color`. The `$channel` must be a + quoted string. + + [missing channel]: /documentation/values/colors#missing-channels + + {% codeExample 'color-is-missing', false %} + @use 'sass:color'; + + @debug color.is-missing(#b37399, "green"); // false + @debug color.is-missing(rgb(100 none 200), "green"); // true + @debug color.is-missing(color.to-space(grey, lch), "hue"); // true + === + @use 'sass:color' + + @debug color.is-legacy(#b37399) // true + @debug color.is-legacy(hsl(90deg 30% 90%)) // true + @debug color.is-legacy(oklch(70% 10% 120deg)) // false + {% endcodeExample %} +{% endfunction %} + +{% function 'color.is-powerless($color, $channel, $space: null)', 'returns:boolean' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$space"' %}{% endcompatibility %} + +Returns whether `$color`'s `$channel` is [powerless] in `$space`, which + defaults to `$color`'s space. The `$channel` must be a quoted string and the + `$space` must be an unquoted string. + + [powerless]: /documentation/values/colors#powerless-channels + + Channels are considered powerless in the following circumstances: + + * In the `hsl` space, the `hue` is powerless if the `saturation` is 0%. + * In the `hwb` space, the `hue` is powerless if the `whiteness` plus the + `blackness` is greater than 100%. + * In the `lch` and `oklch` spaces, the `hue` is powerless if the `chroma` is + 0%. + + {% codeExample 'color-is-powerless', false %} + @use 'sass:color'; + + @debug color.is-powerless(hsl(180deg 0% 40%), "hue"); // true + @debug color.is-powerless(hsl(180deg 0% 40%), "saturation"); // false + @debug color.is-powerless(#999, "hue", $space: hsl); // true + === + @use 'sass:color' + + @debug color.is-powerless(hsl(180deg 0% 40%), "hue") // true + @debug color.is-powerless(hsl(180deg 0% 40%), "saturation") // false + @debug color.is-powerless(#999, "hue", $space: hsl) // true + {% endcodeExample %} +{% endfunction %} + +{% function 'color.mix($color1, $color2, $weight: 50%, $method: null)', 'mix($color1, $color2, $weight: 50%, $method: null)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$method"' %}{% endcompatibility %} + + Returns a color that's a mixture of `$color1` and `$color2` using `$method`, + which is the name of a color space, optionally followed by a [hue + interpolation method] if it's a polar color space (`hsl`, `hwb`, `lch`, or + `oklch`). + + [hue interpolation method]: https://developer.mozilla.org/en-US/docs/Web/CSS/hue-interpolation-method + + This uses the same algorithm to mix colors as [the CSS `color-mix()` + function]. This also means that if either color has a [missing channel] in the + interpolation space, it will take on the corresponding channel value from the + other color. This always returns a color in `$color1`'s space. + + [the CSS `color-mix()` function]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix + [missing channel]: /documentation/values/colors#missing-channels + + The `$weight` must be a number between `0%` and `100%` (inclusive). A larger + weight indicates that more of `$color1` should be used, and a smaller weight + indicates that more of `$color2` should be used. + + {% headsUp %} + For historical reasons, `$method` is optional if `$color1` and `$color2` are + both in [legacy color spaces]. In this case, color mixing is done using the + same algorithm that Sass used historically, in which both the `$weight` and + the relative opacity of each color determines how much of each color is in + the result. + + [legacy color spaces]: /documentation/values/colors#legacy-color-spaces + {% endheadsUp %} + + {% codeExample 'color-mix', false %} + @use 'sass:color'; + + @debug color.mix(#036, #d2e1dd, $method: rgb); // #698aa2 + @debug color.mix(#036, #d2e1dd, $method: oklch); // rgb(87.864037264, 140.601918773, 154.2876826946) + @debug color.mix( + color(rec2020 1 0.7 0.1), + color(rec2020 0.8 none 0.3), + $weight: 75%, + $method: rec2020 + ); // color(rec2020 0.95 0.7 0.15) + @debug color.mix( + oklch(80% 20% 0deg), + oklch(50% 10% 120deg), + $method: oklch longer hue + ); // oklch(65% 0.06 240deg) + === + @use 'sass:color'; + + @debug color.mix(#036, #d2e1dd, $method: rgb) // #698aa2 + @debug color.mix(#036, #d2e1dd, $method: oklch) // rgb(87.864037264, 140.601918773, 154.2876826946) + @debug color.mix(color(rec2020 1 0.7 0.1), color(rec2020 0.8 none 0.3), $weight: 75%, $method: rec2020) // color(rec2020 0.95 0.7 0.15) + + + + + + @debug color.mix(oklch(80% 20% 0deg), oklch(50% 10% 120deg), $method: oklch longer hue) // oklch(65% 0.06 240deg) + {% endcodeExample %} +{% endfunction %} + +{% function 'color.same($color1, $color2)', 'returns:boolean' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns whether `$color1` and `$color2` visually render as the same color. + Unlike `==`, this considers colors to be equivalent even if they're in + different color spaces as long as they represent the same color value in the + `xyz` color space. This treats [missing channels] as equivalent to zero. + + [missing channels]: /documentation/values/colors#missing-channels + + {% codeExample 'color-same', false %} + @use 'sass:color'; + + @debug color.same(#036, #036); // true + @debug color.same(#036, #037); // false + @debug color.same(#036, color.to-space(#036, oklch)); // true + @debug color.same(hsl(none 50% 50%), hsl(0deg 50% 50%)); // true + === + @use 'sass:color' + + @debug color.same(#036, #036) // true + @debug color.same(#036, #037) // false + @debug color.same(#036, color.to-space(#036, oklch)) // true + @debug color.same(hsl(none 50% 50%), hsl(0deg 50% 50%)) // true + {% endcodeExample %} +{% endfunction %} + +{% capture color_scale %} + color.scale($color, + $red: null, $green: null, $blue: null, + $saturation: null, $lightness: null, + $whiteness: null, $blackness: null, + $x: null, $y: null, $z: null, + $chroma: null, + $alpha: null, + $space: null) +{% endcapture %} + +{% function color_scale, 'scale-color(...)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "$x, $y, $z, $chroma, and $space"' %}{% endcompatibility %} + {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false', 'feature: "$whiteness and $blackness"' %}{% endcompatibility %} + + Fluidly scales one or more properties of `$color`. + + Each keyword argument must be a number between `-100%` and `100%` (inclusive). + This indicates how far the corresponding property should be moved from its + original position towards the maximum (if the argument is positive) or the + minimum (if the argument is negative). This means that, for example, + `$lightness: 50%` will make all colors `50%` closer to maximum lightness + without making them fully white. By default, this can only scale colors in + `$color`'s space, but a different color space can be passed as `$space` to + scale channels there instead. This always returns a color in the same space as + `$color`. + + {% headsUp %} + For historical reasons, if `$color` is in a [legacy color space], _any_ + legacy color space channels can be scaled. However, it's an error to specify + an RGB channel (`$red`, `$green`, and/or `$blue`) at the same time as an HSL + channel (`$saturation`, and/or `$lightness`), or either of those at the same + time as an [HWB] channel (`$hue`, `$whiteness`, and/or `$blackness`). + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + [HWB]: https://en.wikipedia.org/wiki/HWB_color_model + + Even so, it's a good idea to pass `$space` explicitly even for legacy colors. + {% endheadsUp %} + + [HWB]: https://en.wikipedia.org/wiki/HWB_color_model + + See also: + + * [`color.adjust()`](#adjust) for changing a color's properties by fixed + amounts. + * [`color.change()`](#change) for setting a color's properties. + + {% codeExample 'color-scale', false %} + @use 'sass:color'; + + @debug color.scale(#6b717f, $red: 15%); // rgb(129.2, 113, 127) + @debug color.scale(#d2e1dd, $lightness: -10%, $space: oklch); + // rgb(181.2580722731, 195.8949200496, 192.0059024063) + @debug color.scale(oklch(80% 20% 120deg), $chroma: 50%, $alpha: -40%); + // oklch(80% 0.24 120deg / 0.6) + === + @use 'sass:color' + + @debug color.scale(#6b717f, $red: 15%) // rgb(129.2, 113, 127) + @debug color.scale(#d2e1dd, $lightness: -10%, $space: oklch) + // rgb(181.2580722731, 195.8949200496, 192.0059024063) + @debug color.scale(oklch(80% 20% 120deg), $chroma: 50%, $alpha: -40%) + // oklch(80% 0.24 120deg / 0.6) + {% endcodeExample %} +{% endfunction %} + +{% function 'color.space($color)', 'returns:unquoted string' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns the name of `$color`'s space as an unquoted string. + + {% codeExample 'color-space', false %} + @use 'sass:color'; + + @debug color.space(#036); // rgb + @debug color.space(hsl(120deg 40% 50%)); // hsl + @debug color.space(color(xyz-d65 0.1 0.2 0.3)); // xyz + === + @use 'sass:color' + + @debug color.space(#036) // rgb + @debug color.space(hsl(120deg 40% 50%)) // hsl + @debug color.space(color(xyz-d65 0.1 0.2 0.3)) // xyz + {% endcodeExample %} +{% endfunction %} + +{% function 'color.to-gamut($color, $space: null, $method: null)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns a visually similar color to `$color` in the gamut of `$space`, which + defaults to `$color`'s space. If `$color` is already in-gamut for `$space`, + it's returned as-is. This always returns a color in` $color`'s original space. + The `$space` must be an unquoted string. + + The `$method` indicates how Sass should choose a "similar" color: + + * `local-minde`: This is the method currently recommended by the CSS Colors 4 + specification. It binary searches the Oklch chroma space of the color until + it finds a color whose clipped-to-gamut value is as close as possible to the + reduced-chroma variant. + + * `clip`: This simply clips all channels to within `$space`'s gamut, setting + them to the minimum or maximum gamut values if they're out-of-gamut. + + {% headsUp %} + The CSS working group and browser vendors are still actively discussing + alternative options for a recommended gamut-mapping algorithm. Until they + settle on a recommendation, the `$method` parameter is mandatory in + `color.to-gamut()` so that we can eventually make its default value the same + as the CSS default. + {% endheadsUp %} + + {% codeExample 'color-to-gamut', false %} + @use 'sass:color'; + + @debug color.to-gamut(#036, $method: local-minde); // #036 + @debug color.to-gamut(oklch(60% 70% 20deg), $space: rgb, $method: local-minde); + // oklch(61.2058838235% 0.2466052584 22.0773325274deg) + @debug color.to-gamut(oklch(60% 70% 20deg), $space: rgb, $method: clip); + // oklch(62.5026609544% 0.2528579741 24.1000466758deg) + === + @use 'sass:color' + + @debug color.to-gamut(#036, $method: local-minde) // #036 + @debug color.to-gamut(oklch(60% 70% 20deg), $space: rgb, $method: local-minde) + // oklch(61.2058838235% 0.2466052584 22.0773325274deg) + @debug color.to-gamut(oklch(60% 70% 20deg), $space: rgb, $method: clip) + // oklch(62.5026609544% 0.2528579741 24.1000466758deg) + {% endcodeExample %} +{% endfunction %} + +{% function 'color.to-space($color, $space)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Converts `$color` into the given `$space`, which must be an unquoted string. + + If the gamut of `$color`'s original space is wider than `$space`'s gamut, this + may return a color that's out-of-gamut for the `$space`. You can convert it to + a similar in-gamut color using [`color.to-gamut()`]. + + [`color.to-gamut()`]: #to-gamut + + This can produce colors with [missing channels], either if `$color` has an + [analogous channel] that's missing, or if the channel is [powerless] in the + destination space. In order to ensure that converting to legacy color spaces + always produces a color that's compatible with older browsers, if `$space` is + legacy this will never return a new missing channel. + + [missing channels]: /documentation/values/colors#missing-channels + [analogous channel]: https://www.w3.org/TR/css-color-4/#analogous-components + [powerless]: /documentation/values/colors#powerless-channels + + {% funFact %} + This is the only Sass function that returns a color in a different space + than the one passed in. + {% endfunFact %} + + {% codeExample 'color-to-space', false %} + @use 'sass:color'; + + @debug color.to-space(#036, display-p3); // lch(20.7457453073% 35.0389733355 273.0881809283deg) + @debug color.to-space(oklab(44% 0.09 -0.13)); // rgb(103.1328911972, 50.9728091281, 150.8382311692) + @debug color.to-space(xyz(0.8 0.1 0.1)); // color(a98-rgb 1.2177586808 -0.7828263424 0.3516847577) + @debug color.to-space(grey, lch); // lch(53.5850134522% 0 none) + @debug color.to-space(lch(none 10% 30deg), oklch); // oklch(none 0.3782382429 11.1889160032deg) + === + @use 'sass:color' + + @debug color.to-space(#036, display-p3) // lch(20.7457453073% 35.0389733355 273.0881809283deg) + @debug color.to-space(oklab(44% 0.09 -0.13)) // rgb(103.1328911972, 50.9728091281, 150.8382311692) + @debug color.to-space(xyz(0.8 0.1 0.1)) // color(a98-rgb 1.2177586808 -0.7828263424 0.3516847577) + @debug color.to-space(grey, lch) // lch(53.5850134522% 0 none) + @debug color.to-space(lch(none 10% 30deg), oklch) // oklch(none 0.3782382429 11.1889160032deg) + {% endcodeExample %} +{% endfunction %} + +## Deprecated Functions + {% function 'adjust-hue($color, $degrees)', 'returns:color' %} - Increases or decreases `$color`'s hue. + Increases or decreases `$color`'s HSL hue. The `$hue` must be a number between `-360deg` and `360deg` (inclusive) to add - to `$color`'s hue. It may be [unitless][] but it may not have any unit other - than `deg`. + to `$color`'s hue. It may be [unitless] or have any angle unit. The `$color` + must be in a [legacy color space]. [unitless]: /documentation/values/numbers#units + [legacy color space]: /documentation/values/colors#legacy-color-spaces See also [`color.adjust()`](#adjust), which can adjust any property of a color. {% headsUp %} - Because `adjust-hue()` is redundant with [`adjust()`](#adjust), it's not + Because `adjust-hue()` is redundant with [`color.adjust()`](#adjust), it's not included directly in the new module system. Instead of `adjust-hue($color, - $amount)`, you can write [`color.adjust($color, $hue: $amount)`](#adjust). + $amount)`, you can write [`color.adjust($color, $hue: $amount, $space: + hsl)`](#adjust). {% endheadsUp %} {% codeExample 'adjust-hue' %} @@ -96,20 +694,21 @@ title: sass:color {% function 'color.alpha($color)', 'alpha($color)', 'opacity($color)', 'returns:number' %} Returns the alpha channel of `$color` as a number between 0 and 1. + + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces As a special case, this supports the Internet Explorer syntax - `alpha(opacity=20)`, for which it returns an [unquoted string][]. + `alpha(opacity=20)`, for which it returns an [unquoted string]. [unquoted string]: /documentation/values/strings#unquoted - See also: - - * [`color.red()`](#red) for getting a color's red channel. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.blue()`](#blue) for getting a color's blue channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.lightness()`](#lightness) for getting a color's lightness. + {% headsUp %} + Because `color.alpha()` is redundant with [`color.channel()`](#channel), + it's no longer recommended. Instead of `color.alpha($color)`, you can write + [`color.channel($color, "alpha")`](#channel). + {% endheadsUp %} {% codeExample 'color-alpha' %} @use 'sass:color'; @@ -126,22 +725,22 @@ title: sass:color {% endcodeExample %} {% endfunction %} -{% function 'color.blackness($color)', 'returns:number' %} +{% function 'color.blackness($color)', 'blackness($color)', 'returns:number' %} {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} - Returns the [HWB][] blackness of `$color` as a number between `0%` and `100%`. + Returns the [HWB] blackness of `$color` as a number between `0%` and `100%`. [HWB]: https://en.wikipedia.org/wiki/HWB_color_model - See also: + The `$color` must be in a [legacy color space]. - * [`color.red()`](#red) for getting a color's red channel. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.lightness()`](#lightness) for getting a color's lightness. - * [`color.whiteness()`](#whiteness) for getting a color's whiteness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + [legacy color space]: /documentation/values/colors#legacy-color-spaces + + {% headsUp %} + Because `color.blackness()` is redundant with [`color.channel()`](#channel), + it's no longer recommended. Instead of `color.blackness($color)`, you can + write [`color.channel($color, "blackness")`](#channel). + {% endheadsUp %} {% codeExample 'color-blackness' %} @use 'sass:color'; @@ -161,16 +760,15 @@ title: sass:color {% function 'color.blue($color)', 'blue($color)', 'returns:number' %} Returns the blue channel of `$color` as a number between 0 and 255. - See also: + The `$color` must be in a [legacy color space]. - * [`color.red()`](#red) for getting a color's red channel. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.lightness()`](#lightness) for getting a color's lightness. - * [`color.whiteness()`](#whiteness) for getting a color's whiteness. - * [`color.blackness()`](#blackness) for getting a color's blackness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + [legacy color space]: /documentation/values/colors#legacy-color-spaces + + {% headsUp %} + Because `color.blue()` is redundant with [`color.channel()`](#channel), it's + no longer recommended. Instead of `color.blue($color)`, you can write + [`color.channel($color, "blue")`](#channel). + {% endheadsUp %} {% codeExample 'color-blue' %} @use 'sass:color'; @@ -187,93 +785,13 @@ title: sass:color {% endcodeExample %} {% endfunction %} -{% capture color_change %} - color.change($color, - $red: null, $green: null, $blue: null, - $hue: null, $saturation: null, $lightness: null, - $whiteness: null, $blackness: null, - $alpha: null) -{% endcapture %} - -{% function color_change, 'change-color(...)', 'returns:color' %} - {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false', 'feature: "$whiteness and $blackness"' %}{% endcompatibility %} - - Sets one or more properties of a color to new values. - - Uses the value passed for each keyword argument in place of the corresponding - property of the color, and returns the changed color. It's an error to specify - an RGB property (`$red`, `$green`, and/or `$blue`) at the same time as an HSL - property (`$hue`, `$saturation`, and/or `$lightness`), or either of those at - the same time as an [HWB][] property (`$hue`, `$whiteness`, and/or - `$blackness`). - - [HWB]: https://en.wikipedia.org/wiki/HWB_color_model - - All optional arguments must be numbers. The `$red`, `$green`, and `$blue` - arguments must be [unitless][] and between 0 and 255 (inclusive). The `$hue` - argument must have either the unit `deg` or no unit. The `$saturation`, - `$lightness`, `$whiteness`, and `$blackness` arguments must be between `0%` - and `100%` (inclusive), and may not be unitless. The `$alpha` argument must be - unitless and between 0 and 1 (inclusive). - - [unitless]: /documentation/values/numbers#units - - See also: - - * [`color.scale()`](#scale) for fluidly scaling a color's properties. - * [`color.adjust()`](#adjust) for adjusting a color's properties by fixed - amounts. - - {% codeExample 'color-change' %} - @use 'sass:color'; - - @debug color.change(#6b717f, $red: 100); // #64717f - @debug color.change(#d2e1dd, $red: 100, $blue: 50); // #64e132 - @debug color.change(#998099, $lightness: 30%, $alpha: 0.5); // rgba(85, 68, 85, 0.5) - === - @use 'sass:color' - - @debug color.change(#6b717f, $red: 100) // #64717f - @debug color.change(#d2e1dd, $red: 100, $blue: 50) // #64e132 - @debug color.change(#998099, $lightness: 30%, $alpha: 0.5) // rgba(85, 68, 85, 0.5) - {% endcodeExample %} -{% endfunction %} - -{% function 'color.complement($color)', 'complement($color)', 'returns:color' %} - Returns the RGB [complement][] of `$color`. - - This is identical to [`color.adjust($color, $hue: 180deg)`](#adjust). - - [complement]: https://en.wikipedia.org/wiki/Complementary_colors - - {% codeExample 'color-complement' %} - @use 'sass:color'; - - // Hue 222deg becomes 42deg. - @debug color.complement(#6b717f); // #7f796b - - // Hue 164deg becomes 344deg. - @debug color.complement(#d2e1dd); // #e1d2d6 - - // Hue 210deg becomes 30deg. - @debug color.complement(#036); // #663300 - === - @use 'sass:color' - - // Hue 222deg becomes 42deg. - @debug color.complement(#6b717f) // #7f796b - - // Hue 164deg becomes 344deg. - @debug color.complement(#d2e1dd) // #e1d2d6 - - // Hue 210deg becomes 30deg. - @debug color.complement(#036) // #663300 - {% endcodeExample %} -{% endfunction %} - {% function 'darken($color, $amount)', 'returns:color' %} Makes `$color` darker. + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + The `$amount` must be a number between `0%` and `100%` (inclusive). Decreases the HSL lightness of `$color` by that amount. @@ -285,7 +803,7 @@ title: sass:color Because `darken()` is usually not the best way to make a color darker, it's not included directly in the new module system. However, if you have to preserve the existing behavior, `darken($color, $amount)` can be written - [`color.adjust($color, $lightness: -$amount)`](#adjust). + [`color.adjust($color, $lightness: -$amount, $space: hsl)`](#adjust). {% codeExample 'color-darken' %} @use 'sass:color'; @@ -330,6 +848,10 @@ title: sass:color {% function 'desaturate($color, $amount)', 'returns:color' %} Makes `$color` less saturated. + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + The `$amount` must be a number between `0%` and `100%` (inclusive). Decreases the HSL saturation of `$color` by that amount. @@ -341,7 +863,8 @@ title: sass:color Because `desaturate()` is usually not the best way to make a color less saturated, it's not included directly in the new module system. However, if you have to preserve the existing behavior, `desaturate($color, $amount)` - can be written [`color.adjust($color, $saturation: -$amount)`](#adjust). + can be written [`color.adjust($color, $saturation: -$amount, $space: + hsl)`](#adjust). {% codeExample 'color-desaturate' %} @use 'sass:color'; @@ -385,39 +908,18 @@ title: sass:color {% endcodeExample %} {% endfunction %} -{% function 'color.grayscale($color)', 'grayscale($color)', 'returns:color' %} - Returns a gray color with the same lightness as `$color`. - - This is identical to [`color.change($color, $saturation: 0%)`](#change). - - {% codeExample 'color-grayscale' %} - @use 'sass:color'; - - @debug color.grayscale(#6b717f); // #757575 - @debug color.grayscale(#d2e1dd); // #dadada - @debug color.grayscale(#036); // #333333 - === - @use 'sass:color' - - @debug color.grayscale(#6b717f) // #757575 - @debug color.grayscale(#d2e1dd) // #dadada - @debug color.grayscale(#036) // #333333 - {% endcodeExample %} -{% endfunction %} - {% function 'color.green($color)', 'green($color)', 'returns:number' %} Returns the green channel of `$color` as a number between 0 and 255. - See also: + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces - * [`color.red()`](#red) for getting a color's red channel. - * [`color.blue()`](#blue) for getting a color's blue channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.lightness()`](#lightness) for getting a color's lightness. - * [`color.whiteness()`](#whiteness) for getting a color's whiteness. - * [`color.blackness()`](#blackness) for getting a color's blackness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + {% headsUp %} + Because `color.green()` is redundant with [`color.channel()`](#channel), + it's no longer recommended. Instead of `color.green($color)`, you can write + [`color.channel($color, "green")`](#channel). + {% endheadsUp %} {% codeExample 'color-green' %} @use 'sass:color'; @@ -437,16 +939,15 @@ title: sass:color {% function 'color.hue($color)', 'hue($color)', 'returns:number' %} Returns the hue of `$color` as a number between `0deg` and `360deg`. - See also: + The `$color` must be in a [legacy color space]. - * [`color.red()`](#red) for getting a color's red channel. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.blue()`](#blue) for getting a color's blue channel. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.lightness()`](#lightness) for getting a color's lightness. - * [`color.whiteness()`](#whiteness) for getting a color's whiteness. - * [`color.blackness()`](#blackness) for getting a color's blackness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + [legacy color space]: /documentation/values/colors#legacy-color-spaces + + {% headsUp %} + Because `color.hue()` is redundant with [`color.channel()`](#channel), it's + no longer recommended. Instead of `color.hue($color)`, you can write + [`color.channel($color, "hue")`](#channel). + {% endheadsUp %} {% codeExample 'color-hue' %} @use 'sass:color'; @@ -463,95 +964,13 @@ title: sass:color {% endcodeExample %} {% endfunction %} -{% function 'color.hwb($hue $whiteness $blackness)', 'color.hwb($hue $whiteness $blackness / $alpha)', 'color.hwb($hue, $whiteness, $blackness, $alpha: 1)', 'returns:color' %} - {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} - - Returns a color with the given [hue, whiteness, and blackness][] and the given - alpha channel. - - [hue, whiteness, and blackness]: https://en.wikipedia.org/wiki/HWB_color_model - - The hue is a number between `0deg` and `360deg` (inclusive). The whiteness and - blackness are numbers between `0%` and `100%` (inclusive). The hue may be - [unitless][], but the whiteness and blackness must have unit `%`. The alpha - channel can be specified as either a unitless number between 0 and 1 - (inclusive), or a percentage between `0%` and `100%` (inclusive). - - [unitless]: /documentation/values/numbers#units - - {% headsUp %} - Sass's [special parsing rules][] for slash-separated values make it - difficult to pass variables for `$blackness` or `$alpha` when using the - `color.hwb($hue $whiteness $blackness / $alpha)` signature. Consider using - `color.hwb($hue, $whiteness, $blackness, $alpha)` instead. - - [special parsing rules]: /documentation/operators/numeric#slash-separated-values - {% endheadsUp %} - - {% codeExample 'color-hwb' %} - @use 'sass:color'; - - @debug color.hwb(210, 0%, 60%); // #036 - @debug color.hwb(34, 89%, 5%); // #f2ece4 - @debug color.hwb(210 0% 60% / 0.5); // rgba(0, 51, 102, 0.5) - === - @use 'sass:color' - - @debug color.hwb(210, 0%, 60%) // #036 - @debug color.hwb(34, 89%, 5%) // #f2ece4 - @debug color.hwb(210 0% 60% / 0.5) // rgba(0, 51, 102, 0.5) - {% endcodeExample %} -{% endfunction %} - -{% function 'color.ie-hex-str($color)', 'ie-hex-str($color)', 'returns:unquoted string' %} - Returns an unquoted string that represents `$color` in the `#AARRGGBB` format - expected by Internet Explorer's [`-ms-filter`][] property. - - [`-ms-filter`]: https://learn.microsoft.com/en-us/previous-versions/ms530752(v=vs.85) - - {% codeExample 'color-ie-hex-str' %} - @use 'sass:color'; - - @debug color.ie-hex-str(#b37399); // #FFB37399 - @debug color.ie-hex-str(#808c99); // #FF808C99 - @debug color.ie-hex-str(rgba(242, 236, 228, 0.6)); // #99F2ECE4 - === - @use 'sass:color' - - @debug color.ie-hex-str(#b37399); // #FFB37399 - @debug color.ie-hex-str(#808c99); // #FF808C99 - @debug color.ie-hex-str(rgba(242, 236, 228, 0.6)); // #99F2ECE4 - {% endcodeExample %} -{% endfunction %} - -{% function 'color.invert($color, $weight: 100%)', 'invert($color, $weight: 100%)', 'returns:color' %} - Returns the inverse or [negative][] of `$color`. - - [negative]: https://en.wikipedia.org/wiki/Negative_(photography) - - The `$weight` must be a number between `0%` and `100%` (inclusive). A higher - weight means the result will be closer to the negative, and a lower weight - means it will be closer to `$color`. Weight `50%` will always produce - `#808080`. - - {% codeExample 'color-invert' %} - @use 'sass:color'; - - @debug color.invert(#b37399); // #4c8c66 - @debug color.invert(black); // white - @debug color.invert(#550e0c, 20%); // #663b3a - === - @use 'sass:color' - - @debug color.invert(#b37399) // #4c8c66 - @debug color.invert(black) // white - @debug color.invert(#550e0c, 20%) // #663b3a - {% endcodeExample %} -{% endfunction %} - {% function 'lighten($color, $amount)', 'returns:color' %} Makes `$color` lighter. + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + The `$amount` must be a number between `0%` and `100%` (inclusive). Increases the HSL lightness of `$color` by that amount. @@ -563,7 +982,7 @@ title: sass:color Because `lighten()` is usually not the best way to make a color lighter, it's not included directly in the new module system. However, if you have to preserve the existing behavior, `lighten($color, $amount)` can be written - [`adjust($color, $lightness: $amount)`](#adjust). + [`color.adjust($color, $lightness: $amount, $space: hsl)`](#adjust). {% codeExample 'color-lighten' %} @use 'sass:color'; @@ -608,16 +1027,15 @@ title: sass:color {% function 'color.lightness($color)', 'lightness($color)', 'returns:number' %} Returns the HSL lightness of `$color` as a number between `0%` and `100%`. - See also: + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces - * [`color.red()`](#red) for getting a color's red channel. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.blue()`](#blue) for getting a color's blue channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.whiteness()`](#whiteness) for getting a color's whiteness. - * [`color.blackness()`](#blackness) for getting a color's blackness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + {% headsUp %} + Because `color.lightness()` is redundant with [`color.channel()`](#channel), + it's no longer recommended. Instead of `color.lightness($color)`, you can write + [`color.channel($color, "lightness")`](#channel). + {% endheadsUp %} {% codeExample 'color-lightness' %} @use 'sass:color'; @@ -634,35 +1052,13 @@ title: sass:color {% endcodeExample %} {% endfunction %} -{% function 'color.mix($color1, $color2, $weight: 50%)', 'mix($color1, $color2, $weight: 50%)', 'returns:color' %} - Returns a color that's a mixture of `$color1` and `$color2`. - - Both the `$weight` and the relative opacity of each color determines how much - of each color is in the result. The `$weight` must be a number between `0%` - and `100%` (inclusive). A larger weight indicates that more of `$color1` - should be used, and a smaller weight indicates that more of `$color2` should - be used. - - {% codeExample 'color-mix' %} - @use 'sass:color'; - - @debug color.mix(#036, #d2e1dd); // #698aa2 - @debug color.mix(#036, #d2e1dd, 75%); // #355f84 - @debug color.mix(#036, #d2e1dd, 25%); // #9eb6bf - @debug color.mix(rgba(242, 236, 228, 0.5), #6b717f); // rgba(141, 144, 152, 0.75) - === - @use 'sass:color' - - @debug color.mix(#036, #d2e1dd) // #698aa2 - @debug color.mix(#036, #d2e1dd, 75%) // #355f84 - @debug color.mix(#036, #d2e1dd, 25%) // #9eb6bf - @debug color.mix(rgba(242, 236, 228, 0.5), #6b717f) // rgba(141, 144, 152, 0.75) - {% endcodeExample %} -{% endfunction %} - {% function 'opacify($color, $amount)', 'fade-in($color, $amount)', 'returns:color' %} Makes `$color` more opaque. + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + The `$amount` must be a number between `0` and `1` (inclusive). Increases the alpha channel of `$color` by that amount. @@ -674,7 +1070,7 @@ title: sass:color Because `opacify()` is usually not the best way to make a color more opaque, it's not included directly in the new module system. However, if you have to preserve the existing behavior, `opacify($color, $amount)` can be written - [`adjust($color, $alpha: -$amount)`](#adjust). + [`color.adjust($color, $alpha: -$amount)`](#adjust). {% codeExample 'color-opacify' %} @use 'sass:color'; @@ -711,16 +1107,15 @@ title: sass:color {% function 'color.red($color)', 'red($color)', 'returns:number' %} Returns the red channel of `$color` as a number between 0 and 255. - See also: + The `$color` must be in a [legacy color space]. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.blue()`](#blue) for getting a color's blue channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.lightness()`](#lightness) for getting a color's lightness. - * [`color.whiteness()`](#whiteness) for getting a color's whiteness. - * [`color.blackness()`](#blackness) for getting a color's blackness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + [legacy color space]: /documentation/values/colors#legacy-color-spaces + + {% headsUp %} + Because `color.red()` is redundant with [`color.channel()`](#channel), it's + no longer recommended. Instead of `color.red($color)`, you can write + [`color.channel($color, "red")`](#channel). + {% endheadsUp %} {% codeExample 'color-red' %} @use 'sass:color'; @@ -737,9 +1132,13 @@ title: sass:color {% endcodeExample %} {% endfunction %} -{% function 'color.saturate($color, $amount)', 'saturate($color, $amount)', 'returns:color' %} +{% function 'saturate($color, $amount)', 'returns:color' %} Makes `$color` more saturated. + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + The `$amount` must be a number between `0%` and `100%` (inclusive). Increases the HSL saturation of `$color` by that amount. @@ -751,7 +1150,7 @@ title: sass:color Because `saturate()` is usually not the best way to make a color more saturated, it's not included directly in the new module system. However, if you have to preserve the existing behavior, `saturate($color, $amount)` can - be written [`adjust($color, $saturation: $amount)`](#adjust). + be written [`color.adjust($color, $saturation: $amount, $space: hsl)`](#adjust). {% codeExample 'color-saturate' %} @use 'sass:color'; @@ -798,16 +1197,16 @@ title: sass:color {% function 'color.saturation($color)', 'saturation($color)', 'returns:number' %} Returns the HSL saturation of `$color` as a number between `0%` and `100%`. - See also: + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces - * [`color.red()`](#red) for getting a color's red channel. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.blue()`](#blue) for getting a color's blue channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.lightness()`](#lightness) for getting a color's lightness. - * [`color.whiteness()`](#whiteness) for getting a color's whiteness. - * [`color.blackness()`](#blackness) for getting a color's blackness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + {% headsUp %} + Because `color.saturation()` is redundant with + [`color.channel()`](#channel), it's no longer recommended. Instead of + `color.saturation($color)`, you can write + [`color.channel($color, "saturation")`](#channel). + {% endheadsUp %} {% codeExample 'color-saturation' %} @use 'sass:color'; @@ -824,57 +1223,13 @@ title: sass:color {% endcodeExample %} {% endfunction %} -{% capture color_scale %} - color.scale($color, - $red: null, $green: null, $blue: null, - $saturation: null, $lightness: null, - $whiteness: null, $blackness: null, - $alpha: null) -{% endcapture %} - -{% function color_scale, 'scale-color(...)', 'returns:color' %} - {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false', 'feature: "$whiteness and $blackness"' %}{% endcompatibility %} - - Fluidly scales one or more properties of `$color`. - - Each keyword argument must be a number between `-100%` and `100%` (inclusive). - This indicates how far the corresponding property should be moved from its - original position towards the maximum (if the argument is positive) or the - minimum (if the argument is negative). This means that, for example, - `$lightness: 50%` will make all colors `50%` closer to maximum lightness - without making them fully white. - - It's an error to specify an RGB property (`$red`, `$green`, and/or `$blue`) at - the same time as an HSL property (`$saturation`, and/or `$lightness`), or - either of those at the same time as an [HWB][] property (`$whiteness`, and/or - `$blackness`). - - [HWB]: https://en.wikipedia.org/wiki/HWB_color_model - - See also: - - * [`color.adjust()`](#adjust) for changing a color's properties by fixed - amounts. - * [`color.change()`](#change) for setting a color's properties. - - {% codeExample 'color-scale' %} - @use 'sass:color'; - - @debug color.scale(#6b717f, $red: 15%); // #81717f - @debug color.scale(#d2e1dd, $lightness: -10%, $saturation: 10%); // #b3d4cb - @debug color.scale(#998099, $alpha: -40%); // rgba(153, 128, 153, 0.6) - === - @use 'sass:color' - - @debug color.scale(#6b717f, $red: 15%) // #81717f - @debug color.scale(#d2e1dd, $lightness: -10%, $saturation: 10%) // #b3d4cb - @debug color.scale(#998099, $alpha: -40%) // rgba(153, 128, 153, 0.6) - {% endcodeExample %} -{% endfunction %} - {% function 'transparentize($color, $amount)', 'fade-out($color, $amount)', 'returns:color' %} Makes `$color` more transparent. + The `$color` must be in a [legacy color space]. + + [legacy color space]: /documentation/values/colors#legacy-color-spaces + The `$amount` must be a number between `0` and `1` (inclusive). Decreases the alpha channel of `$color` by that amount. @@ -887,8 +1242,8 @@ title: sass:color Because `transparentize()` is usually not the best way to make a color more transparent, it's not included directly in the new module system. However, if you have to preserve the existing behavior, `transparentize($color, - $amount)` can be written [`color.adjust($color, $alpha: - -$amount)`](#adjust). + $amount)` can be written [`color.adjust($color, $alpha: -$amount, + $space: hsl)`](#adjust). {% codeExample 'transparentize' %} @use 'sass:color'; @@ -925,19 +1280,19 @@ title: sass:color {% function 'color.whiteness($color)', 'returns:number' %} {% compatibility 'dart: "1.28.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} - Returns the [HWB][] whiteness of `$color` as a number between `0%` and `100%`. + Returns the [HWB] whiteness of `$color` as a number between `0%` and `100%`. [HWB]: https://en.wikipedia.org/wiki/HWB_color_model - See also: + The `$color` must be in a [legacy color space]. - * [`color.red()`](#red) for getting a color's red channel. - * [`color.green()`](#green) for getting a color's green channel. - * [`color.hue()`](#hue) for getting a color's hue. - * [`color.saturation()`](#saturation) for getting a color's saturation. - * [`color.lightness()`](#lightness) for getting a color's lightness. - * [`color.blackness()`](#blackness) for getting a color's blackness. - * [`color.alpha()`](#alpha) for getting a color's alpha channel. + [legacy color space]: /documentation/values/colors#legacy-color-spaces + + {% headsUp %} + Because `color.whiteness()` is redundant with [`color.channel()`](#channel), + it's no longer recommended. Instead of `color.whiteness($color)`, you can + write [`color.channel($color, "whiteness")`](#channel). + {% endheadsUp %} {% codeExample 'color-whiteness' %} @use 'sass:color'; diff --git a/source/documentation/modules/index.md b/source/documentation/modules/index.md index 76e1c3a23..39fe9329f 100644 --- a/source/documentation/modules/index.md +++ b/source/documentation/modules/index.md @@ -83,6 +83,46 @@ Sass provides the following built-in modules: ## Global Functions +{% funFact %} + You can pass [special functions] like `calc()` or `var()` in place of any + argument to a global color constructor. You can even use `var()` in place of + multiple arguments, since it might be replaced by multiple values! When a + color function is called this way, it returns an unquoted string using the + same signature it was called with. + + [special functions]: /documentation/syntax/special-functions + + {% codeExample 'color-special', false %} + @debug rgb(0 51 102 / var(--opacity)); // rgb(0 51 102 / var(--opacity)) + @debug color(display-p3 var(--peach)); // color(display-p3 var(--peach)) + === + @debug rgb(0 51 102 / var(--opacity)) // rgb(0 51 102 / var(--opacity)) + @debug color(display-p3 var(--peach)) // color(display-p3 var(--peach)) + {% endcodeExample %} +{% endfunFact %} + +{% function 'color($space $channel1 $channel2 $channel3)', 'color($space $channel1 $channel2 $channel3 / $alpha)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns a color in the given color space with the given channel values. + + This supports the color spaces `srgb`, `srgb-linear`, `display-p3`, `a98-rgb`, + `prophoto-rgb`, `rec2020`, `xyz`, and `xyz-d50`, as well as `xyz-d65` which is + an alias for `xyz`. For all spaces, the channels are numbers between 0 and 1 + (inclusive) or percentages between `0%` and `100%` (inclusive). + + If any color channel is outside the range 0 to 1, this represents a color + outside the standard gamut for its color space. + + {% codeExample 'hsl', false %} + @debug color(srgb 0.1 0.6 1); // color(srgb 0.1 0.6 1) + @debug color(xyz 30% 0% 90% / 50%); // color(xyz 0.3 0 0.9 / 50%) + === + @debug color(srgb 0.1 0.6 1) // color(srgb 0.1 0.6 1) + @debug color(xyz 30% 0% 90% / 50%) // color(xyz 0.3 0 0.9 / 50%) + {% endcodeExample %} +{% endfunction %} + {% function 'hsl($hue $saturation $lightness)', 'hsl($hue $saturation $lightness / $alpha)', 'hsl($hue, $saturation, $lightness, $alpha: 1)', 'hsla($hue $saturation $lightness)', 'hsla($hue $saturation $lightness / $alpha)', 'hsla($hue, $saturation, $lightness, $alpha: 1)', 'returns:color' %} {% compatibility 'dart: "1.15.0"', 'libsass: false', 'ruby: false', 'feature: "Level 4 Syntax"' %} LibSass and Ruby Sass only support the following signatures: @@ -106,28 +146,15 @@ Sass provides the following built-in modules: [hue, saturation, and lightness]: https://en.wikipedia.org/wiki/HSL_and_HSV The hue is a number between `0deg` and `360deg` (inclusive) and may be - unitless. The saturation and lightness are numbers between `0%` and `100%` - (inclusive) and may *not* be unitless. The alpha channel can be specified as - either a unitless number between 0 and 1 (inclusive), or a percentage between - `0%` and `100%` (inclusive). - - {% funFact %} - You can pass [special functions][] like `calc()` or `var()` in place of any - argument to `hsl()`. You can even use `var()` in place of multiple - arguments, since it might be replaced by multiple values! When a color - function is called this way, it returns an unquoted string using the same - signature it was called with. - - [special functions]: /documentation/syntax/special-functions - - {% codeExample 'hsl-special', false %} - @debug hsl(210deg 100% 20% / var(--opacity)); // hsl(210deg 100% 20% / var(--opacity)) - @debug hsla(var(--peach), 20%); // hsla(var(--peach), 20%) - === - @debug hsl(210deg 100% 20% / var(--opacity)) // hsl(210deg 100% 20% / var(--opacity)) - @debug hsla(var(--peach), 20%) // hsla(var(--peach), 20%) - {% endcodeExample %} - {% endfunFact %} + unitless. The saturation and lightness are typically numbers between `0%` and + `100%` (inclusive) and may *not* be unitless. The alpha channel can be + specified as either a unitless number between 0 and 1 (inclusive), or a + percentage between `0%` and `100%` (inclusive). + + A hue outside `0deg` and `360deg` is equivalent to `$hue % 360deg`. A + saturation less than `0%` is clamped to `0%`. A saturation above `100%` or a + lightness outside `0%` and `100%` are both allowed, and represent colors + outside the standard RGB gamut. {% headsUp %} Sass's [special parsing rules][] for slash-separated values make it @@ -140,14 +167,65 @@ Sass provides the following built-in modules: {% codeExample 'hsl', false %} @debug hsl(210deg 100% 20%); // #036 - @debug hsl(34, 35%, 92%); // #f2ece4 @debug hsl(210deg 100% 20% / 50%); // rgba(0, 51, 102, 0.5) - @debug hsla(34, 35%, 92%, 0.2); // rgba(242, 236, 228, 0.2) + @debug hsla(34, 35%, 92%, 0.2); // rgba(241.74, 235.552, 227.46, 0.2) === @debug hsl(210deg 100% 20%) // #036 - @debug hsl(34, 35%, 92%) // #f2ece4 @debug hsl(210deg 100% 20% / 50%) // rgba(0, 51, 102, 0.5) - @debug hsla(34, 35%, 92%, 0.2) // rgba(242, 236, 228, 0.2) + @debug hsla(34, 35%, 92%, 0.2) // rgba(241.74, 235.552, 227.46, 0.2) + {% endcodeExample %} +{% endfunction %} + +{% function 'if($condition, $if-true, $if-false)' %} + Returns `$if-true` if `$condition` is [truthy][], and `$if-false` otherwise. + + This function is special in that it doesn't even evaluate the argument that + isn't returned, so it's safe to call even if the unused argument would throw + an error. + + [truthy]: /documentation/at-rules/control/if#truthiness-and-falsiness + + {% codeExample 'debug', false %} + @debug if(true, 10px, 15px); // 10px + @debug if(false, 10px, 15px); // 15px + @debug if(variable-defined($var), $var, null); // null + === + @debug if(true, 10px, 15px) // 10px + @debug if(false, 10px, 15px) // 15px + @debug if(variable-defined($var), $var, null) // null + {% endcodeExample %} +{% endfunction %} + +{% function 'hwb($hue $whiteness $blackness)', 'hwb($hue $whiteness $blackness / $alpha)', 'color.hwb($hue $whiteness $blackness)', 'color.hwb($hue $whiteness $blackness / $alpha)', 'color.hwb($hue, $whiteness, $blackness, $alpha: 1)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns a color with the given [hue, whiteness, and blackness] and the + given alpha channel. + + [hue, whiteness, and blackness]: https://en.wikipedia.org/wiki/HWB_color_model + + The hue is a number between `0deg` and `360deg` (inclusive) and may be + unitless. The whiteness and blackness are numbers typically between `0%` and + `100%` (inclusive) and may *not* be unitless. The alpha channel can be + specified as either a unitless number between 0 and 1 (inclusive), or a + percentage between `0%` and `100%` (inclusive). + + A hue outside `0deg` and `360deg` is equivalent to `$hue % 360deg`. If + `$whiteness + $blackness > 100%`, the two values are scaled so that they add + up to `100%`. If `$whiteness`, `$blackness`, or both are less than `0%`, this + represents a color outside the standard RGB gamut. + + {% headsUp %} + The `color.hwb()` variants are deprecated. New Sass code should use the + global `hwb()` function instead. + {% endheadsUp %} + + {% codeExample 'hwb', false %} + @debug hwb(210deg 0% 60%); // #036 + @debug hwb(210 0% 60% / 0.5); // rgba(0, 51, 102, 0.5) + === + @debug hwb(210deg 0% 60%) // #036 + @debug hwb(210 0% 60% / 0.5) // rgba(0, 51, 102, 0.5) {% endcodeExample %} {% endfunction %} @@ -171,6 +249,129 @@ Sass provides the following built-in modules: {% endcodeExample %} {% endfunction %} +{% function 'lab($lightness $a $b)', 'lab($lightness $a $b / $alpha)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns a color with the given [lightness, a, b], and alpha channels. + + [hue, whiteness, and blackness]: https://en.wikipedia.org/wiki/CIELAB_color_space + + The lightness is a number between `0%` and `100%` (inclusive) and may be + unitless. The a and b channels can be specified as either [unitless] numbers + between -125 and 125 (inclusive), or percentages between `-100%` and `100%` + (inclusive). The alpha channel can be specified as either a unitless number + between 0 and 1 (inclusive), or a percentage between `0%` and `100%` + (inclusive). + + [unitless]: /documentation/values/numbers#units + + A lightness outside the range `0%` and `100%` is clamped to be within that + range. If the a or b channels are outside the range `-125` to `125`, this + represents a color outside the standard CIELAB gamut. + + {% codeExample 'lab', false %} + @debug lab(50% -20 30); // lab(50% -20 30) + @debug lab(80% 0% 20% / 0.5); // lab(80% 0 25 / 0.5); + === + @debug lab(50% -20 30) // lab(50% -20 30) + @debug lab(80% 0% 20% / 0.5) // lab(80% 0 25 / 0.5); + {% endcodeExample %} +{% endfunction %} + +{% function 'lch($lightness $chroma $hue)', 'lch($lightness $chroma $hue / $alpha)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns a color with the given [lightness, chroma, and hue], and the given + alpha channel. + + [hue, whiteness, and blackness]: https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_model + + The lightness is a number between `0%` and `100%` (inclusive) and may be + unitless. The chroma channel can be specified as either a [unitless] number + between 0 and 150 (inclusive), or a percentage between `0%` and `100%` + (inclusive). The hue is a number between `0deg` and `360deg` (inclusive) and + may be unitless. The alpha channel can be specified as either a unitless + number between 0 and 1 (inclusive), or a percentage between `0%` and `100%` + (inclusive). + + [unitless]: /documentation/values/numbers#units + + A lightness outside the range `0%` and `100%` is clamped to be within that + range. A chroma below 0 is clamped to 0, and a chroma above 150 represents a + color outside the standard CIELAB gamut. A hue outside `0deg` and `360deg` is + equivalent to `$hue % 360deg`. + + {% codeExample 'lch', false %} + @debug lch(50% 10 270deg); // lch(50% 10 270deg) + @debug lch(80% 50% 0.2turn / 0.5); // lch(80% 75 72deg / 0.5); + === + @debug lch(50% 10 270deg) // lch(50% 10 270deg) + @debug lch(80% 50% 0.2turn / 0.5) // lch(80% 75 72deg / 0.5); + {% endcodeExample %} +{% endfunction %} + +{% function 'oklab($lightness $a $b)', 'oklab($lightness $a $b / $alpha)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns a color with the given [perceptually-uniform lightness, a, b], and + alpha channels. + + [perceptually-uniform lightness, a, b]: https://bottosson.github.io/posts/oklab/ + + The lightness is a number between `0%` and `100%` (inclusive) and may be + unitless. The a and b channels can be specified as either [unitless] numbers + between -0.4 and 0.4 (inclusive), or percentages between `-100%` and `100%` + (inclusive). The alpha channel can be specified as either a unitless number + between 0 and 1 (inclusive), or a percentage between `0%` and `100%` + (inclusive). + + [unitless]: /documentation/values/numbers#units + + A lightness outside the range `0%` and `100%` is clamped to be within that + range. If the a or b channels are outside the range `-0.4` to `0.4`, this + represents a color outside the standard Oklab gamut. + + {% codeExample 'oklab', false %} + @debug oklab(50% -0.1 0.15); // oklab(50% -0.1 0.15) + @debug oklab(80% 0% 20% / 0.5); // oklab(80% 0 0.08 / 0.5) + === + @debug oklab(50% -0.1 0.15) // oklab(50% -0.1 0.15) + @debug oklab(80% 0% 20% / 0.5) // oklab(80% 0 0.08 / 0.5) + {% endcodeExample %} +{% endfunction %} + +{% function 'oklch($lightness $chroma $hue)', 'oklch($lightness $chroma $hue / $alpha)', 'returns:color' %} + {% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %} + + Returns a color with the given [perceptually-uniform lightness, chroma, and + hue], and the given alpha channel. + + [hue, whiteness, and blackness]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch + + The lightness is a number between `0%` and `100%` (inclusive) and may be + unitless. The chroma channel can be specified as either a [unitless] number + between 0 and 0.4 (inclusive), or a percentage between `0%` and `100%` + (inclusive). The hue is a number between `0deg` and `360deg` (inclusive) and + may be unitless. The alpha channel can be specified as either a unitless + number between 0 and 1 (inclusive), or a percentage between `0%` and `100%` + (inclusive). + + [unitless]: /documentation/values/numbers#units + + A lightness outside the range `0%` and `100%` is clamped to be within that + range. A chroma below 0 is clamped to 0, and a chroma above 0.4 represents a + color outside the standard Oklab gamut. A hue outside `0deg` and `360deg` is + equivalent to `$hue % 360deg`. + + {% codeExample 'oklch', false %} + @debug oklch(50% 0.3 270deg); // oklch(50% 0.3 270deg) + @debug oklch(80% 50% 0.2turn / 0.5); // oklch(80% 0.2 72deg / 0.5); + === + @debug oklch(50% 0.3 270deg) // oklch(50% 0.3 270deg) + @debug oklch(80% 50% 0.2turn / 0.5) // oklch(80% 0.2 72deg / 0.5); + {% endcodeExample %} +{% endfunction %} + {% function 'rgb($red $green $blue)', 'rgb($red $green $blue / $alpha)', 'rgb($red, $green, $blue, $alpha: 1)', 'rgb($color, $alpha)', 'rgba($red $green $blue)', 'rgba($red $green $blue / $alpha)', 'rgba($red, $green, $blue, $alpha: 1)', 'rgba($color, $alpha)', 'returns:color' %} {% compatibility 'dart: "1.15.0"', 'libsass: false', 'ruby: false', 'feature: "Level 4 Syntax"' %} LibSass and Ruby Sass only support the following signatures: @@ -192,30 +393,15 @@ Sass provides the following built-in modules: If `$red`, `$green`, `$blue`, and optionally `$alpha` are passed, returns a color with the given red, green, blue, and alpha channels. - Each channel can be specified as either a [unitless][] number between 0 and + Each channel can be specified as either a [unitless] number between 0 and 255 (inclusive), or a percentage between `0%` and `100%` (inclusive). The alpha channel can be specified as either a unitless number between 0 and 1 (inclusive), or a percentage between `0%` and `100%` (inclusive). [unitless]: /documentation/values/numbers#units - {% funFact %} - You can pass [special functions][] like `calc()` or `var()` in place of any - argument to `rgb()`. You can even use `var()` in place of multiple - arguments, since it might be replaced by multiple values! When a color - function is called this way, it returns an unquoted string using the same - signature it was called with. - - [special functions]: /documentation/syntax/special-functions - - {% codeExample 'rgb-special', false %} - @debug rgb(0 51 102 / var(--opacity)); // rgb(0 51 102 / var(--opacity)) - @debug rgba(var(--peach), 0.2); // rgba(var(--peach), 0.2) - === - @debug rgb(0 51 102 / var(--opacity)) // rgb(0 51 102 / var(--opacity)) - @debug rgba(var(--peach), 0.2) // rgba(var(--peach), 0.2) - {% endcodeExample %} - {% endfunFact %} + If any color channel is outside the range 0 to 255, this represents a color + outside the standard RGB gamut. {% headsUp %} Sass's [special parsing rules][] for slash-separated values make it diff --git a/source/documentation/operators/equality.md b/source/documentation/operators/equality.md index bbee2c5d7..ac66ba556 100644 --- a/source/documentation/operators/equality.md +++ b/source/documentation/operators/equality.md @@ -23,7 +23,9 @@ different types: their values are equal when their units are converted between one another. * [Strings][] are unusual in that [unquoted][] and [quoted][] strings with the same contents are considered equal. -* [Colors][] are equal if they have the same red, green, blue, and alpha values. +* [Colors] are equal if they're in the same [color space] and have the same + channel values, *or* if they're both in [legacy color spaces] and have the + same RGBA channel values. * [Lists][] are equal if their contents are equal. Comma-separated lists aren't equal to space-separated lists, and bracketed lists aren't equal to unbracketed lists. @@ -40,6 +42,8 @@ different types: [quoted]: /documentation/values/strings#quoted [unquoted]: /documentation/values/strings#unquoted [Colors]: /documentation/values/colors +[color space]: /documentation/values/colors#color-spaces +[legacy color spaces]: /documentation/values/colors#legacy-color-spaces [Lists]: /documentation/values/lists [`true`, `false`]: /documentation/values/booleans [`null`]: /documentation/values/null diff --git a/source/documentation/values/colors.md b/source/documentation/values/colors.md index 0309d1041..227fa2c8c 100644 --- a/source/documentation/values/colors.md +++ b/source/documentation/values/colors.md @@ -1,77 +1,347 @@ --- title: Colors +table_of_contents: true --- -{% compatibility 'dart: "1.14.0"', 'libsass: "3.6.0"', 'ruby: "3.6.0"', 'feature: "Level 4 Syntax"' %} +{% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "Color Spaces"' %} + LibSass, Ruby Sass, and older versions of Dart Sass don't support color spaces + other than `rgb` and `hsl`. + + As well as to adding support for new color spaces, this release changed some + details of the way colors were handled. In particular, even the legacy `rgb` + and `hsl` color spaces are no longer clamped to their gamuts; it's now + possible to represent `rgb(500 0 0)` or other out-of-bounds values. In + addition, `rgb` colors are no longer rounded to the nearest integer because + the CSS spec now requires implementations to maintain precision wherever + possible. +{% endcompatibility %} + +{% compatibility 'dart: "1.14.0"', 'libsass: false', 'ruby: "3.6.0"', 'feature: "Level 4 Syntax"' %} LibSass and older versions of Dart or Ruby Sass don't support [hex colors with an alpha channel][]. [hex colors with an alpha channel]: https://drafts.csswg.org/css-color/#hex-notation {% endcompatibility %} -Sass has built-in support for color values. Just like CSS colors, they represent -points in the [sRGB color space][], although many Sass [color functions][] -operate using [HSL coordinates][] (which are just another way of expressing sRGB -colors). Sass colors can be written as hex codes (`#f2ece4` or `#b37399aa`), -[CSS color names][] (`midnightblue`, `transparent`), or the functions -[`rgb()`][], [`rgba()`][], [`hsl()`][], and [`hsla()`][]. +Sass has built-in support for color values. Just like CSS colors, each color +represents a point in a particular color space such as `rgb` or `lab`. Sass +colors can be written as hex codes (`#f2ece4` or `#b37399aa`), [CSS color names] +(`midnightblue`, `transparent`), or color functions like [`rgb()`], [`lab()`], +or [`color()`]. [sRGB color space]: https://en.wikipedia.org/wiki/SRGB [color functions]: /documentation/modules/color -[HSL coordinates]: https://en.wikipedia.org/wiki/HSL_and_HSV [CSS color names]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords [`rgb()`]: /documentation/modules#rgb -[`rgba()`]: /documentation/modules#rgba -[`hsl()`]: /documentation/modules#hsl -[`hsla()`]: /documentation/modules#hsla +[`lab()`]: /documentation/modules#lab +[`color()`]: /documentation/modules#color {% codeExample 'colors', false %} @debug #f2ece4; // #f2ece4 @debug #b37399aa; // rgba(179, 115, 153, 67%) @debug midnightblue; // #191970 - @debug rgb(204, 102, 153); // #c69 - @debug rgba(107, 113, 127, 0.8); // rgba(107, 113, 127, 0.8) - @debug hsl(228, 7%, 86%); // #dadbdf - @debug hsla(20, 20%, 85%, 0.7); // rgb(225, 215, 210, 0.7) + @debug rgb(204 102 153); // #c69 + @debug lab(32.4% 38.4 -47.7 / 0.7); // lab(32.4% 38.4 -47.7 / 0.7) + @debug color(display-p3 0.597 0.732 0.576); // color(display-p3 0.597 0.732 0.576) === @debug #f2ece4 // #f2ece4 @debug #b37399aa // rgba(179, 115, 153, 67%) @debug midnightblue // #191970 - @debug rgb(204, 102, 153) // #c69 - @debug rgba(107, 113, 127, 0.8) // rgba(107, 113, 127, 0.8) - @debug hsl(228, 7%, 86%) // #dadbdf - @debug hsla(20, 20%, 85%, 0.7) // rgb(225, 215, 210, 0.7) + @debug rgb(204 102 153) // #c69 + @debug lab(32.4% 38.4 -47.7 / 0.7) // lab(32.4% 38.4 -47.7 / 0.7) + @debug color(display-p3 0.597 0.732 0.576) // color(display-p3 0.597 0.732 0.576) {% endcodeExample %} -{% funFact %} - No matter how a Sass color is originally written, it can be used with both - HSL-based and RGB-based functions! -{% endfunFact %} +## Color Spaces + +Sass supports the same set of color spaces as CSS. A Sass color will always be +emitted in the same color space it was written in unless it's in a [legacy color +space] or you convert it to another space using [`color.to-space()`]. All the +other color functions in Sass will always return a color in the same spaces as +the original color, even if the function made changes to that color in another +space. + +[legacy color space]: #legacy-color-spaces +[`color.to-space()`]: /documentation/modules/color#to-space + +Although each color space has bounds on the gamut it expects for its channels, +Sass can represent out-of-gamut values for any color space. This allows a color +from a wide-gamut space to be safely converted into and back out of a +narrow-gamut space without losing information. + +{% headsUp %} + CSS requires that some color functions clip their input channels. For example, + `rgb(500 0 0)` clips its red channel to be within [0, 255] and so is + equivalent to `rgb(255 0 0)` even though `rgb(500 0 0)` is a distinct value + that Sass can represent. You can always use Sass's [`color.change()`] function + to set an out-of-gamut value for any space. + + [`color.change()`]: /documentation/modules/color#change +{% endheadsUp %} + +Following is a full list of all the color spaces Sass supports. You can read +learn about these spaces [on MDN]. + +[on MDN]: https://developer.mozilla.org/en-US/docs/Glossary/Color_space + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SpaceSyntaxChannels [min, max]
rgb* + rgb(102 51 153)
+ #663399
+ rebeccapurple +
+ red [0, 255]; + green [0, 255]; + blue [0, 255] +
hsl*hsl(270 50% 40%) + hue [0, 360]; + saturation [0%, 100%]; + lightness [0%, 100%] +
hwb*hwb(270 20% 40%) + hue [0, 360]; + whiteness [0%, 100%]; + blackness [0%, 100%] +
srgbcolor(srgb 0.4 0.2 0.6) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
srgb-linearcolor(srgb-linear 0.133 0.033 0.319) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
display-p3color(display-p3 0.374 0.21 0.579) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
a98-rgbcolor(a98-rgb 0.358 0.212 0.584) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
prophoto-rgbcolor(prophoto-rgb 0.316 0.191 0.495) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
rec2020color(rec2020 0.305 0.168 0.531) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
xyz, xyz-d65 + color(xyz 0.124 0.075 0.309)
+ color(xyz-d65 0.124 0.075 0.309) +
+ x [0, 1]; + y [0, 1]; + z [0, 1] +
xyz-d50color(xyz-d50 0.116 0.073 0.233) + x [0, 1]; + y [0, 1]; + z [0, 1] +
lablab(32.4% 38.4 -47.7) + lightness [0%, 100%]; + a [-125, 125]; + b [-125, 125] +
lchlch(32.4% 61.2 308.9deg) + lightness [0%, 100%]; + chroma [0, 150]; + hue [0deg, 360deg] +
oklaboklab(44% 0.088 -0.134) + lightness [0%, 100%]; + a [-0.4, 0.4]; + b [-0.4, 0.4] +
oklchoklch(44% 0.16 303.4deg) + lightness [0%, 100%]; + chroma [0, 0.4]; + hue [0deg, 360deg] +
+ +Spaces marked with * are [legacy color spaces]. + +[legacy color spaces]: #legacy-color-spaces + +## Missing Channels + +Colors in CSS and Sass can have "missing channels", which are written `none` and +represent a channel whose value isn't known or doesn't affect the way the color +is rendered. For example, you might write `hsl(none 0% 50%)`, because the hue +doesn't matter if the saturation is `0%`. In most cases, missing channels are +just treated as 0 values, but they do come up occasionally: + +* If you're mixing colors together, either as part of CSS interpolation for + something like an animation or using Sass's [`color.mix()`] function, missing + channels always take on the other color's value for that channel if possible. + + [`color.mix()`]: /documentation/modules/color#mix + +* If you convert a color with a missing channel to another space that has an + analogous channel, that channel will be set to `none` after the conversion is + complete. + +Although [`color.channel()`] will return 0 for missing channels, you can always +check for them using [`color.is-missing()`]. -CSS supports many different formats that can all represent the same color: its -name, its hex code, and [functional notation][]. Which format Sass chooses to -compile a color to depends on the color itself, how it was written in the -original stylesheet, and the current output mode. Because it can vary so much, -stylesheet authors shouldn't rely on any particular output format for colors -they write. +[`color.channel()`]: /documentation/modules/color#channel +[`color.is-missing()`]: /documentation/modules/color#is-missing -[functional notation]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value +{% codeExample 'missing-channels', false %} + @use 'sass:color'; -Sass supports many useful [color functions][] that can be used to create new -colors based on existing ones by [mixing colors together][] or [scaling their -hue, saturation, or lightness][]. + $grey: hsl(none 0% 50%); + + @debug color.mix($grey, blue, $method: hsl); // hsl(240, 50%, 50%) + @debug color.to-space($grey, lch); // lch(53.3889647411% 0 none) + === + @use 'sass:color' + + $grey: hsl(none 0% 50%) + + @debug color.mix($grey, blue, $method: hsl) // hsl(240, 50%, 50%) + @debug color.to-space($grey, lch) // lch(53.3889647411% 0 none) +{% endcodeExample %} + +### Powerless Channels + +A color channel is considered "powerless" under certain circumstances its value +doesn't affect the way the color is rendered on screen. The CSS spec requires +that when a color is converted to a new space, any powerless channels are +replaced by `none`. Sass does this in all cases except conversions to legacy +spaces, to guarantee that converting to a legacy space always produces a color +that's compatible with older browsers. + +For more details on powerless channels, see [`color.is-powerless()`]. + +[`color.is-powerless()`]: /documentation/modules/color#is-powerless + +## Legacy Color Spaces + +Historically, CSS and Sass only supported the standard RGB gamut, and only +supported the `rgb`, `hsl`, and `hwb` functions for defining colors. Because at +the time all colors used the same gamut, every color function worked with every +color regardless of its color space. Sass still preserves this behavior, but +only for older functions and only for colors in these three "legacy" color +spaces. Even so, it's still a good practice to explicitly specify the `$space` +you want to work in when using color functions. + +Sass will also freely convert between different legacy color spaces when +converting legacy color values to CSS. This is always safe, because they all use +the same underlying color model, and this helps ensure that Sass emits colors in +as compatible a format as possible. + +## Color Functions + +Sass supports many useful [color functions] that can be used to create new +colors based on existing ones by [mixing colors together] or [scaling their +channel values]. When calling color functions, color spaces should always be +written as unquoted strings to match CSS, while channel names should be written +as quoted strings so that channels like `"red"` aren't parsed as color values. [mixing colors together]: /documentation/modules/color#mix -[scaling their hue, saturation, or lightness]: /documentation/modules/color#scale +[scaling their channel values]: /documentation/modules/color#scale + +{% funFact %} + Sass color functions can automatically convert colors between spaces, which + makes it easy to do transformations in perceptually-uniform color spaces like + Oklch. But they'll *always* return a color in the same space you gave it, + unless you explicitly call [`color.to-space()`] to convert it. + + [`color.to-space()`]: /documentation/modules/color#to-space +{% endfunFact %} {% codeExample 'color-formats', false %} + @use 'sass:color'; + $venus: #998099; - @debug scale-color($venus, $lightness: +15%); // #a893a8 - @debug mix($venus, midnightblue); // #594d85 + @debug color.scale($venus, $lightness: +15%, $space: oklch); + // rgb(170.1523703626, 144.612080603, 170.1172627174) + @debug color.mix($venus, midnightblue, $method: oklch); + // rgb(95.9363315581, 74.5687109346, 133.2082569526) === + @use 'sass:color' + $venus: #998099 - @debug scale-color($venus, $lightness: +15%) // #a893a8 - @debug mix($venus, midnightblue) // #594d85 + @debug color.scale($venus, $lightness: +15%, $space: oklch) + // rgb(170.1523703626, 144.612080603, 170.1172627174) + @debug color.mix($venus, midnightblue, $method: oklch) + // rgb(95.9363315581, 74.5687109346, 133.2082569526) {% endcodeExample %} diff --git a/source/feed.liquid b/source/feed.liquid index 7fa750172..1a11548f8 100644 --- a/source/feed.liquid +++ b/source/feed.liquid @@ -13,7 +13,7 @@ eleventyExcludeFromCollections: true {%- for post in posts limit:6 -%} {%- assign absolutePostUrl = post.url | absoluteUrl: site.url %} - {{ post.data.title }} + {{ post.data.title | escape }} {{ absolutePostUrl }} {{ post.date | dateToRfc3339 }} diff --git a/tool/generate-module-metadata.ts b/tool/generate-module-metadata.ts new file mode 100644 index 000000000..f3d7315ee --- /dev/null +++ b/tool/generate-module-metadata.ts @@ -0,0 +1,112 @@ +// Gets list of all functions and variables for the built-in modules +// +// Outputs `sass-site/source/assets/js/playground/module-metadata.ts` file, which is omitted from source code and +// must be built before the `playground` bundle is built with rollup. +import {compileString, sassTrue} from 'sass'; +import {writeFileSync} from 'fs'; +import path from 'path'; + +// Descriptions for built in Sass module. +const moduleDescriptions = { + color: + 'generates new colors based on existing ones, making it easy to build color themes', + list: 'lets you access and modify values in lists', + map: 'makes it possible to look up the value associated with a key in a map, and much more', + math: 'provides functions that operate on numbers', + meta: 'exposes the details of Sass’s inner workings', + selector: 'provides access to Sass’s powerful selector engine', + string: 'makes it easy to combine, search, or split apart strings', +}; + +// List of built in Sass module names. +const modules = [ + 'color', + 'list', + 'map', + 'math', + 'meta', + 'selector', + 'string', +] as const; + +// Enum of built in Sass module names. +type ModuleName = (typeof modules)[number]; + +// Data structure for modules and their members. +interface ModuleDefinition { + name: ModuleName; + description: string; + functions: string[]; + variables: string[]; +} + +// Generate SCSS with a custom function that extracts each module's functions +// and variables. +function generateModuleMetadata(): ModuleDefinition[] { + // Generate Sass + const moduleSass = ` + ${modules.map(mod => `@use "sass:${mod}";`).join('\n')} + $modules: ${modules.join(',')}; + + @each $module in $modules { + $_: extract($module, ( + functions: map.keys(meta.module-functions($module)), + variables: map.keys(meta.module-variables($module)) + )); + } + `; + + const modMap: ModuleDefinition[] = []; + + compileString(moduleSass, { + functions: { + 'extract($name, $members)': function (args) { + const [_name, _members] = args; + // const keys = _keys.asList.toArray().map(key => key.assertString().text); + const {functions, variables} = _members + .assertMap('members') + .contents.toObject(); + const name = _name.assertString('name').toString() as ModuleName; + + const moduleDefinition: ModuleDefinition = { + name, + description: moduleDescriptions[name], + functions: functions.asList + .toArray() + .map(key => key.assertString().text), + variables: variables.asList + .toArray() + .map(key => key.assertString().text), + }; + + modMap.push(moduleDefinition); + + return sassTrue; + }, + }, + }); + + return modMap; +} + +// Generates metadata, and outputs it as source. +function writeFile(): void { + const moduleMembers = generateModuleMetadata(); + const filePath = path.resolve( + __dirname, + '../source/assets/js/playground/module-metadata.ts' + ); + try { + writeFileSync( + filePath, + `export default ${JSON.stringify(moduleMembers, null, 2)} as const;`, + 'utf8' + ); + console.log('module-metadata.ts built successfully'); + } catch (error) { + console.error('module-metadata.ts not built'); + throw error; + } +} + +writeFile(); diff --git a/tsconfig.json b/tsconfig.json index 7ade04147..143c01401 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,6 @@ "isolatedModules": true, "noEmit": true }, - "include": ["source/**/*.ts"], + "include": ["source/**/*.ts", "tool/generate-module-metadata.ts"], "exclude": ["node_modules"] }