Skip to content

Commit 49d4ef8

Browse files
committed
feat: add ColorScheme API with enum-based type-safe implementation
Replaced the ThemeVariant API with a new ColorScheme API that uses the browser-native CSS color-scheme property instead of a custom theme attribute. The API is fully type-safe using ColorScheme.Value enum. Core Implementation: - Created ColorScheme.Value enum: LIGHT, DARK, LIGHT_DARK, DARK_LIGHT, NORMAL - Added @colorscheme annotation for setting initial color scheme on AppShell - Page.setColorScheme(ColorScheme.Value) sets CSS color-scheme property - Page.getColorScheme() returns ColorScheme.Value (never null, defaults to NORMAL) - ExtendedClientDetails stores ColorScheme.Value internally - String values from client automatically converted via ColorScheme.Value.fromString() Frontend Changes: - Flow.ts reads CSS color-scheme computed property - Wire protocol parameter: v-cs (client to server) - Theme name detection unchanged (v-tn for Lumo/Aura identification) Backend Changes: - Registered @colorscheme in VaadinAppShellInitializer - IndexHtmlRequestHandler applies @colorscheme as inline style - Deprecated @Theme.variant() with migration path to @colorscheme Test Updates: - All tests updated to use ColorScheme.Value enum - Integration tests verify CSS color-scheme property (not attribute) - Test CSS uses @media (prefers-color-scheme: dark) queries Benefits: - Type-safe API with compile-time checking - Uses standard CSS property for browser integration - Automatic form control and scrollbar adaptation - No ambiguity about valid values - Single, clean API surface Backwards Compatibility: - @theme(variant) still works via IndexHtmlRequestHandler for migration
1 parent 2c1e328 commit 49d4ef8

File tree

13 files changed

+315
-149
lines changed

13 files changed

+315
-149
lines changed

flow-client/src/main/frontend/Flow.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -539,15 +539,10 @@ export class Flow {
539539
params['v-np'] = ($wnd as any).navigator.platform;
540540
}
541541

542-
/* Theme variant from HTML element - supports both Lumo and Aura */
543-
let themeAttr = document.documentElement.getAttribute('theme');
544-
if (!themeAttr) {
545-
// If no theme attribute, check for native color-scheme property
546-
const colorScheme = getComputedStyle(document.documentElement).getPropertyValue('color-scheme').trim();
547-
// "normal" is the default value and means no variant is set
548-
themeAttr = colorScheme && colorScheme !== 'normal' ? colorScheme : '';
549-
}
550-
params['v-tv'] = themeAttr;
542+
/* Color scheme from CSS color-scheme property */
543+
const colorScheme = getComputedStyle(document.documentElement).colorScheme.trim();
544+
// "normal" is the default value and means no color scheme is set
545+
params['v-cs'] = colorScheme && colorScheme !== 'normal' ? colorScheme : '';
551546
/* Theme name - detect which theme is in use */
552547
const computedStyle = getComputedStyle(document.documentElement);
553548
let themeName = '';
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component.page;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Inherited;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Defines the color scheme for the application using the CSS color-scheme
27+
* property.
28+
* <p>
29+
* This annotation should be placed on a class that implements
30+
* {@link com.vaadin.flow.component.page.AppShellConfigurator} to set the
31+
* initial color scheme for the entire application.
32+
* <p>
33+
* Example usage:
34+
*
35+
* <pre>
36+
* &#64;ColorScheme(ColorScheme.Value.DARK)
37+
* public class AppShell implements AppShellConfigurator {
38+
* }
39+
* </pre>
40+
* <p>
41+
* The color scheme can also be changed programmatically at runtime using
42+
* {@link Page#setColorScheme(ColorScheme.Value)}.
43+
*
44+
* @see Page#setColorScheme(ColorScheme.Value)
45+
* @see Page#getColorScheme()
46+
*/
47+
@Retention(RetentionPolicy.RUNTIME)
48+
@Target(ElementType.TYPE)
49+
@Inherited
50+
@Documented
51+
public @interface ColorScheme {
52+
53+
/**
54+
* The initial color scheme for the application.
55+
*
56+
* @return the color scheme value
57+
*/
58+
Value value() default Value.NORMAL;
59+
60+
/**
61+
* Enumeration of supported color scheme values.
62+
* <p>
63+
* These values correspond to the CSS color-scheme property values and
64+
* control how the browser renders UI elements and how the application
65+
* responds to system color scheme preferences.
66+
*/
67+
enum Value {
68+
/**
69+
* Light color scheme only. The application will use a light theme
70+
* regardless of system preferences.
71+
*/
72+
LIGHT("light"),
73+
74+
/**
75+
* Dark color scheme only. The application will use a dark theme
76+
* regardless of system preferences.
77+
*/
78+
DARK("dark"),
79+
80+
/**
81+
* Supports both light and dark color schemes, with a preference for
82+
* light. The application can adapt to system preferences but defaults
83+
* to light mode.
84+
*/
85+
LIGHT_DARK("light dark"),
86+
87+
/**
88+
* Supports both light and dark color schemes, with a preference for
89+
* dark. The application can adapt to system preferences but defaults to
90+
* dark mode.
91+
*/
92+
DARK_LIGHT("dark light"),
93+
94+
/**
95+
* Normal/default color scheme. Uses the browser's default behavior
96+
* without any specific color scheme preference.
97+
*/
98+
NORMAL("normal");
99+
100+
private final String value;
101+
102+
Value(String value) {
103+
this.value = value;
104+
}
105+
106+
/**
107+
* Gets the CSS color-scheme property value.
108+
*
109+
* @return the CSS value string
110+
*/
111+
public String getValue() {
112+
return value;
113+
}
114+
115+
/**
116+
* Converts a string to a ColorScheme.Value enum.
117+
*
118+
* @param value
119+
* the CSS color-scheme value string
120+
* @return the corresponding enum value, or NORMAL if not recognized
121+
*/
122+
public static Value fromString(String value) {
123+
if (value == null || value.isEmpty()) {
124+
return NORMAL;
125+
}
126+
for (Value v : values()) {
127+
if (v.value.equals(value)) {
128+
return v;
129+
}
130+
}
131+
return NORMAL;
132+
}
133+
}
134+
}

flow-server/src/main/java/com/vaadin/flow/component/page/ExtendedClientDetails.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public class ExtendedClientDetails implements Serializable {
6060
private double devicePixelRatio = -1.0D;
6161
private String windowName;
6262
private String navigatorPlatform;
63-
private String themeVariant = "";
63+
private ColorScheme.Value colorScheme = ColorScheme.Value.NORMAL;
6464
private String themeName;
6565

6666
/**
@@ -102,8 +102,8 @@ public class ExtendedClientDetails implements Serializable {
102102
* a unique browser window name which persists on reload
103103
* @param navigatorPlatform
104104
* navigation platform received from the browser
105-
* @param themeVariant
106-
* the current theme variant
105+
* @param colorScheme
106+
* the current color scheme
107107
* @param themeName
108108
* the theme name (e.g., "lumo", "aura")
109109
*/
@@ -113,7 +113,7 @@ public ExtendedClientDetails(UI ui, String screenWidth, String screenHeight,
113113
String rawTzOffset, String dstShift, String dstInEffect,
114114
String tzId, String curDate, String touchDevice,
115115
String devicePixelRatio, String windowName,
116-
String navigatorPlatform, String themeVariant, String themeName) {
116+
String navigatorPlatform, String colorScheme, String themeName) {
117117
this.ui = ui;
118118
if (screenWidth != null) {
119119
try {
@@ -190,7 +190,7 @@ public ExtendedClientDetails(UI ui, String screenWidth, String screenHeight,
190190

191191
this.windowName = windowName;
192192
this.navigatorPlatform = navigatorPlatform;
193-
setThemeVariant(themeVariant);
193+
setColorScheme(ColorScheme.Value.fromString(colorScheme));
194194
this.themeName = themeName;
195195
}
196196

@@ -406,12 +406,12 @@ public boolean isIOS() {
406406
}
407407

408408
/**
409-
* Gets the theme variant.
409+
* Gets the color scheme.
410410
*
411-
* @return the theme variant, or empty string if not set
411+
* @return the color scheme, never {@code null}
412412
*/
413-
public String getThemeVariant() {
414-
return themeVariant;
413+
public ColorScheme.Value getColorScheme() {
414+
return colorScheme;
415415
}
416416

417417
/**
@@ -425,13 +425,14 @@ public String getThemeName() {
425425
}
426426

427427
/**
428-
* Updates the theme variant. For internal use only.
428+
* Updates the color scheme. For internal use only.
429429
*
430-
* @param themeVariant
431-
* the new theme variant
430+
* @param colorScheme
431+
* the new color scheme
432432
*/
433-
void setThemeVariant(String themeVariant) {
434-
this.themeVariant = themeVariant == null ? "" : themeVariant;
433+
void setColorScheme(ColorScheme.Value colorScheme) {
434+
this.colorScheme = colorScheme == null ? ColorScheme.Value.NORMAL
435+
: colorScheme;
435436
}
436437

437438
/**
@@ -484,7 +485,7 @@ public static ExtendedClientDetails fromJson(UI ui, JsonNode json) {
484485
getStringElseNull.apply("v-pr"),
485486
getStringElseNull.apply("v-wn"),
486487
getStringElseNull.apply("v-np"),
487-
getStringElseNull.apply("v-tv"),
488+
getStringElseNull.apply("v-cs"),
488489
getStringElseNull.apply("v-tn"));
489490
}
490491

flow-server/src/main/java/com/vaadin/flow/component/page/Page.java

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -84,35 +84,37 @@ public void setTitle(String title) {
8484
}
8585

8686
/**
87-
* Sets the theme variant for the page.
87+
* Sets the color scheme for the page using the CSS color-scheme property.
88+
* <p>
89+
* The color scheme affects how the browser renders UI elements and allows
90+
* the application to adapt to system color scheme preferences.
8891
*
89-
* @param variant
90-
* the theme variant to set (e.g., "dark", "light"), or
91-
* {@code null} or empty string to remove the theme variant
92+
* @param colorScheme
93+
* the color scheme to set (e.g., ColorScheme.Value.DARK,
94+
* ColorScheme.Value.LIGHT), or {@code null} to reset to NORMAL
9295
*/
93-
public void setThemeVariant(String variant) {
94-
String newValue = (variant == null || variant.isEmpty()) ? null
95-
: variant;
96-
if (newValue == null) {
97-
executeJs("document.documentElement.removeAttribute('theme');");
96+
public void setColorScheme(ColorScheme.Value colorScheme) {
97+
if (colorScheme == null || colorScheme == ColorScheme.Value.NORMAL) {
98+
executeJs("document.documentElement.style.colorScheme = '';");
99+
getExtendedClientDetails().setColorScheme(ColorScheme.Value.NORMAL);
98100
} else {
99-
executeJs("document.documentElement.setAttribute('theme', $0);",
100-
newValue);
101+
executeJs("document.documentElement.style.colorScheme = $0;",
102+
colorScheme.getValue());
103+
getExtendedClientDetails().setColorScheme(colorScheme);
101104
}
102-
getExtendedClientDetails().setThemeVariant(newValue);
103105
}
104106

105107
/**
106-
* Gets the theme variant for the page.
108+
* Gets the color scheme for the page.
107109
* <p>
108110
* Note that this method returns the server-side cached value and will not
109-
* detect theme changes made directly via JavaScript or browser developer
110-
* tools.
111+
* detect color scheme changes made directly via JavaScript or browser
112+
* developer tools.
111113
*
112-
* @return the theme variant, or empty string if not set
114+
* @return the color scheme value, never {@code null}
113115
*/
114-
public String getThemeVariant() {
115-
return getExtendedClientDetails().getThemeVariant();
116+
public ColorScheme.Value getColorScheme() {
117+
return getExtendedClientDetails().getColorScheme();
116118
}
117119

118120
/**

flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public boolean synchronizedHandleRequest(VaadinSession session,
177177
}
178178

179179
addDevBundleTheme(indexDocument, context);
180-
applyThemeVariant(indexDocument, context);
180+
applyColorScheme(indexDocument, context);
181181

182182
if (config.isDevToolsEnabled()) {
183183
addDevTools(indexDocument, config, session, request);
@@ -253,12 +253,35 @@ private static void addDevBundleTheme(Document document,
253253
}
254254
}
255255

256-
private void applyThemeVariant(Document indexDocument,
256+
private void applyColorScheme(Document indexDocument,
257257
VaadinContext context) {
258+
// Check for @ColorScheme annotation first
259+
AppShellRegistry registry = AppShellRegistry.getInstance(context);
260+
Class<?> shell = registry.getShell();
261+
if (shell != null) {
262+
com.vaadin.flow.component.page.ColorScheme colorSchemeAnnotation = shell
263+
.getAnnotation(
264+
com.vaadin.flow.component.page.ColorScheme.class);
265+
if (colorSchemeAnnotation != null) {
266+
String colorScheme = colorSchemeAnnotation.value().getValue();
267+
if (!colorScheme.isEmpty() && !colorScheme.equals("normal")) {
268+
indexDocument.head().parent().attr("style",
269+
"color-scheme: " + colorScheme);
270+
}
271+
}
272+
}
273+
274+
// Also apply from deprecated @Theme variant attribute for backwards
275+
// compatibility
258276
ThemeUtils.getThemeAnnotation(context).ifPresent(theme -> {
259277
String variant = theme.variant();
260278
if (!variant.isEmpty()) {
261-
indexDocument.head().parent().attr("theme", variant);
279+
String existingStyle = indexDocument.head().parent()
280+
.attr("style");
281+
String newStyle = existingStyle.isEmpty()
282+
? "color-scheme: " + variant
283+
: existingStyle + "; color-scheme: " + variant;
284+
indexDocument.head().parent().attr("style", newStyle);
262285
}
263286
});
264287
}

flow-server/src/main/java/com/vaadin/flow/server/startup/VaadinAppShellInitializer.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.vaadin.flow.component.dependency.StyleSheet;
3636
import com.vaadin.flow.component.page.AppShellConfigurator;
3737
import com.vaadin.flow.component.page.BodySize;
38+
import com.vaadin.flow.component.page.ColorScheme;
3839
import com.vaadin.flow.component.page.Inline;
3940
import com.vaadin.flow.component.page.Meta;
4041
import com.vaadin.flow.component.page.Push;
@@ -61,8 +62,9 @@
6162
*/
6263
@HandlesTypes({ AppShellConfigurator.class, Meta.class, Meta.Container.class,
6364
PWA.class, Inline.class, Inline.Container.class, Viewport.class,
64-
BodySize.class, PageTitle.class, Push.class, Theme.class, NoTheme.class,
65-
StyleSheet.class, StyleSheet.Container.class })
65+
BodySize.class, PageTitle.class, Push.class, ColorScheme.class,
66+
Theme.class, NoTheme.class, StyleSheet.class,
67+
StyleSheet.Container.class })
6668
// @WebListener is needed so that servlet containers know that they have to run
6769
// it
6870
@WebListener

flow-server/src/main/java/com/vaadin/flow/theme/Theme.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,15 @@
100100

101101
/**
102102
* The theme variant, if any.
103+
* <p>
104+
* <b>Deprecated:</b> Use {@link com.vaadin.flow.component.page.ColorScheme}
105+
* annotation instead to set the color scheme for the application.
103106
*
104107
* @return the theme variant
108+
* @deprecated Use {@link com.vaadin.flow.component.page.ColorScheme}
109+
* annotation instead
105110
*/
111+
@Deprecated(since = "25.0", forRemoval = true)
106112
String variant() default "";
107113

108114
/**

0 commit comments

Comments
 (0)