diff --git a/build-tools/tasks/styles.js b/build-tools/tasks/styles.js
index 50aa943278..d4c75812f4 100644
--- a/build-tools/tasks/styles.js
+++ b/build-tools/tasks/styles.js
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
const { parallel, series } = require('gulp');
-const { readFileSync } = require('fs');
+const { readFileSync, existsSync } = require('fs');
const { createHash } = require('crypto');
const { join } = require('path');
const { buildThemedComponentsInternal } = require('@cloudscape-design/theming-build/internal');
@@ -36,6 +36,112 @@ function compileStyleDictionary() {
});
}
+/**
+ * Extracts dark mode CSS rules from the generated styles and creates a separate
+ * CSS file that applies dark mode styles based on prefers-color-scheme media query.
+ *
+ * This allows consumers to opt-in to automatic dark mode without bundling the
+ * duplicate styles by default.
+ */
+function generateDarkModePrefersCss(theme) {
+ return task(`dark-mode-prefers:${theme.name}`, () => {
+ const baseStylesPath = join(theme.outputPath, 'internal/base-component/styles.scoped.css');
+
+ if (!existsSync(baseStylesPath)) {
+ console.log(` Base component styles not found at ${baseStylesPath}, skipping dark-mode-prefers CSS`);
+ return Promise.resolve();
+ }
+
+ const cssContent = readFileSync(baseStylesPath, 'utf-8');
+ const darkModeRules = extractDarkModeRules(cssContent);
+
+ if (darkModeRules.length === 0) {
+ console.log(' No dark mode rules found, skipping dark-mode-prefers CSS');
+ return Promise.resolve();
+ }
+
+ // Transform .awsui-dark-mode to .awsui-dark-mode-if-preferred
+ const transformedRules = darkModeRules.map(rule =>
+ rule.replace(/\.awsui-dark-mode/g, '.awsui-dark-mode-if-preferred')
+ );
+
+ const outputCss = `/* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. */
+/* SPDX-License-Identifier: Apache-2.0 */
+
+/*
+ * This file provides automatic dark mode support based on the user's system preference.
+ * Import this file and apply the .awsui-dark-mode-if-preferred class to enable automatic
+ * dark mode switching based on prefers-color-scheme.
+ *
+ * Usage:
+ * import darkModeStyles from '@cloudscape-design/design-tokens/dark-mode-prefers.css';
+ *
...
+ *
+ * Or without CSS modules:
+ * import '@cloudscape-design/design-tokens/dark-mode-prefers.css';
+ * ...
+ */
+
+@media (prefers-color-scheme: dark) {
+${transformedRules.join('\n\n')}
+}
+`;
+
+ const designTokensOutputDir = join(workspace.targetPath, theme.designTokensDir);
+ writeFile(join(designTokensOutputDir, 'dark-mode-prefers.css'), outputCss);
+ console.log(` Generated dark-mode-prefers.css (${transformedRules.length} rules)`);
+
+ return Promise.resolve();
+ });
+}
+
+/**
+ * Extracts all .awsui-dark-mode CSS rule blocks from the stylesheet content.
+ */
+function extractDarkModeRules(cssContent) {
+ const lines = cssContent.split('\n');
+ const darkModeRules = [];
+
+ let i = 0;
+ while (i < lines.length) {
+ const line = lines[i];
+
+ // Look for dark mode selectors (not inside @media blocks)
+ if (line.includes('.awsui-dark-mode') && line.includes('{') && !line.trim().startsWith('@media')) {
+ // Extract the full rule block
+ let braceCount = 0;
+ const ruleLines = [];
+ let j = i;
+
+ while (j < lines.length) {
+ const currentLine = lines[j];
+ ruleLines.push(currentLine);
+
+ for (const char of currentLine) {
+ if (char === '{') {
+ braceCount++;
+ }
+ if (char === '}') {
+ braceCount--;
+ }
+ }
+
+ if (braceCount === 0 && ruleLines.length > 0) {
+ break;
+ }
+ j++;
+ }
+
+ darkModeRules.push(ruleLines.join('\n'));
+ i = j + 1;
+ } else {
+ i++;
+ }
+ }
+
+ return darkModeRules;
+}
+
function stylesTask(theme) {
return task(`styles:${theme.name}`, async () => {
const designTokensOutputDir = join(workspace.targetPath, theme.designTokensDir);
@@ -88,5 +194,5 @@ function stylesTask(theme) {
module.exports = series(
generateEnvironment(),
compileStyleDictionary(),
- parallel(themes.map(theme => stylesTask(theme)))
+ parallel(themes.map(theme => series(stylesTask(theme), generateDarkModePrefersCss(theme))))
);
diff --git a/build-tools/utils/themes.js b/build-tools/utils/themes.js
index 5066477bbf..b60e2c6157 100644
--- a/build-tools/utils/themes.js
+++ b/build-tools/utils/themes.js
@@ -10,7 +10,15 @@ const themes = [
packageJson: { name: '@cloudscape-design/components' },
designTokensOutput: 'index',
designTokensDir: 'design-tokens',
- designTokensPackageJson: { name: '@cloudscape-design/design-tokens' },
+ designTokensPackageJson: {
+ name: '@cloudscape-design/design-tokens',
+ exports: {
+ '.': './index.js',
+ './index.js': './index.js',
+ './index.scss': './index.scss',
+ './dark-mode-prefers.css': './dark-mode-prefers.css',
+ },
+ },
outputPath: path.join(workspace.targetPath, 'components'),
primaryThemePath: './classic/index.js',
secondaryThemePaths: ['./visual-refresh-secondary/index.js'],