Enhance JavaFX with style themes, which are collections of CSS stylesheets with custom logic to dynamically change or swap stylesheets at runtime. This allows developers to create visually pleasing themes that, due to their dynamic nature, can integrate well with the look&feel of modern operating systems (e.g. dark mode and accent colors).
- Expose UI settings of the operating system (color preferences, dark mode, etc.) as "platform preferences" in JavaFX.
- Allow the light/dark appearance of window decorations be controlled by JavaFX.
- Promote CSS user-agent themes from an implementation detail to a first-class concept.
- Support user-defined themes that are composed of multiple user-agent stylesheets (like Modena and Caspian), which is not possible with the current JavaFX API.
- Implement Caspian and Modena as first-class themes.
It is not a goal to
- Provide a rich/opinionated framework that developers can use to create custom theme classes;
- Add any new features to Caspian or Modena (e.g. dark mode).
While JavaFX knows about themes (it ships with two of them: Caspian and Modena), it doesn't offer a public API to create custom themes. What's a theme? It's a dynamic collection of user-agent stylesheets with some logic to determine when and how individual stylesheets are included in the CSS cascade. For example, a stylesheet with high-contrast theme colors may be dynamically included in the cascade depending on whether the operating system was set to a high contrast mode.
In the CSS subsystem, JavaFX already supports application-wide user-agent stylesheet collections (that's how the built-in themes are implemented). The public API seems like an afterthought: the Application.userAgentStylesheet
property, which specifies a stylesheet URI, also accepts two magic constants ("CASPIAN" and "MODENA") to select one of the built-in themes.
There are two workarounds to create a custom theme for a JavaFX application:
- Add author stylesheets to the Scene
- Replace the built-in theme with a single new user-agent stylesheet
The first option can be used to extend or modify the built-in theme, but it does so by changing the semantics of the new styles: author stylesheets override user code, while user-agent stylesheets don't. It also makes it harder to create entirely new themes for JavaFX, since the built-in styles are always present in the CSS cascade.
The second option retains the semantics of themes (allow user code to override properties), but comes at the price of being quite clunky:
- Only a single stylesheet can be specified. As such, there's no way to create a custom theme that is comprised of many individual stylesheets (like the built-in themes are).
- Existing themes can't be modified. Once the
Application.userAgentStylesheet
property is set to any stylesheet, all stylesheets that came with the built-in theme are discarded. - Even if developers choose to copy and modify a built-in stylesheet in its entirety, they will lose all dynamic features (for example, reacting to changes of the high contrast platform preference), because the logic that conditionally adds to or removes stylesheets from the CSS cascade is only available for the two built-in themes and cannot be added to custom stylesheets.
Ultimately, JavaFX needs to enable developers to easily create visually pleasing themes that keep up with the changing trends of user interface design. This includes the ability to integrate well with the platforms that JavaFX applications run on, for example by supporting dark mode and accent coloring.
Platform preferences are the preferred UI settings of the operating system. For example, on Windows this includes the color values identified by the Windows.UI.ViewManagement.UIColorType
enumeration; on macOS this includes the system color values of the NSColor
class. Exposing these dynamic values to JavaFX applications allows developers to create themes that can integrate seamlessly with the color scheme of the operating system.
Platform preferences are exposed as a read-only ObservableMap
of platform-specific key-value pairs, which means that the preferences available on Windows are different from macOS or Linux. JavaFX provides a small, curated list of preferences that are available on most platforms, and are therefore exposed with a platform-independent API:
public interface Platform.Preferences extends ObservableMap<String, Object> {
// Platform-independent API
ReadOnlyObjectProperty<Appearance> appearanceProperty();
ReadOnlyObjectProperty<Color> backgroundColorProperty();
ReadOnlyObjectProperty<Color> foregroundColorProperty();
ReadOnlyObjectProperty<Color> accentColorProperty();
...
// Convenience methods to retrieve platform-specific values from the map
Optional<Integer> getInteger(String key);
Optional<Double> getDouble(String key);
Optional<String> getString(String key);
Optional<Boolean> getBoolean(String key);
Optional<Color> getColor(String key);
...
}
The platform appearance is defined by the new javafx.stage.Appearance
enumeration:
public enum Appearance {
LIGHT,
DARK
}
An instance of the Platform.Preferences
interface can be retrieved by calling Platform.getPreferences()
.
In general, platform preferences correspond to OS-level settings and are updated dynamically. Third-party themes might integrate platform preferences into their look and feel, which is often what users expect to see. But consider a scenario where an application uses a third-party theme that adapts to the OS appearance, but the application author wants the application to be able to select a light or dark appearance independently from the OS appearance.
For this scenario, JavaFX enables authors to customize application preferences. The Application.Preferences
interface extends Platform.Preferences
, and adds a set of methods to override the value of computed properties and key-value mappings:
interface Application.Preferences extends Platform.Preferences {
...
/**
* Overrides the value of the {@link #appearanceProperty() appearance} property.
* <p>
* Specifying {@code null} clears the override, which restores the value of the
* {@code appearance} property to the platform-provided value.
* <p>
* Calling this method does not update the {@code appearance} property instantaneously;
* instead, the property is only updated after calling {@link #commit()}, or after the
* occurrence of an operating system event that causes the {@code appearance} property
* to be recomputed.
*
* @param appearance the platform appearance override, or {@code null} to clear the override
*/
void setAppearance(Appearance appearance);
...
/**
* Overrides a preference mapping.
* <p>
* If a platform-provided mapping for the key already exists, calling this method overrides
* the value that is mapped to the key. If a platform-provided mapping for the key doesn't
* exist, this method creates a new mapping.
*
* @param key the key
* @param value the new value
* @throws NullPointerException if {@code key} or {@code value} is null
* @throws IllegalArgumentException if a platform-provided mapping for the key exists, and
* the specified value is an instance of a different class
* than the platform-provided value
* @return the previous value associated with {@code key}
*/
Object put(String key, Object value);
/**
* Resets an overridden preference mapping to its platform-provided value.
* <p>
* If the preference is overridden, but the platform does not provide a mapping for the
* specified key, the mapping will be removed. If no mapping exists for the specified
* key, calling this method has no effect.
*
* @param key the key
* @throws NullPointerException if {@code key} is null
*/
void reset(String key);
/**
* Resets all overridden preference mappings to their platform-provided values and removes
* all mappings for which the platform does not provide a default value.
*/
void reset();
}
An instance of Application.Preferences
can be retrieved by calling Application.getPreferences()
. Like platform preferences, application preferences are updated dynamically when OS-level settings are changed. However, when an application preference mapping is overridden by user code, the user value takes precedence.
It is important to point out that Platform.getPreferences()
will always return the platform-provided preference mappings, while Application.getPreferences()
will also contain potentially overridden values. Libraries that use the preferences API are encouraged to use Application.getPreferences()
, which will allow applications to change preference mappings.
See SystemParametersInfo, GetSysColor and Windows.UI.ViewManagement.UISettings.GetColorValue. Deprecated colors are not included.
Key | Type |
---|---|
Windows.SPI.HighContrast | Boolean |
Windows.SPI.HighContrastColorScheme | String |
Windows.SysColor.COLOR_3DFACE | Color |
Windows.SysColor.COLOR_BTNTEXT | Color |
Windows.SysColor.COLOR_GRAYTEXT | Color |
Windows.SysColor.COLOR_HIGHLIGHT | Color |
Windows.SysColor.COLOR_HIGHLIGHTTEXT | Color |
Windows.SysColor.COLOR_HOTLIGHT | Color |
Windows.SysColor.COLOR_WINDOW | Color |
Windows.SysColor.COLOR_WINDOWTEXT | Color |
Windows.UIColor.Background | Color |
Windows.UIColor.Foreground | Color |
Windows.UIColor.AccentDark3 | Color |
Windows.UIColor.AccentDark2 | Color |
Windows.UIColor.AccentDark1 | Color |
Windows.UIColor.Accent | Color |
Windows.UIColor.AccentLight1 | Color |
Windows.UIColor.AccentLight2 | Color |
Windows.UIColor.AccentLight3 | Color |
See UI Element Colors and Adaptable System Colors. Deprecated colors are not included.
Key | Type |
---|---|
macOS.NSColor.labelColor | Color |
macOS.NSColor.secondaryLabelColor | Color |
macOS.NSColor.tertiaryLabelColor | Color |
macOS.NSColor.quaternaryLabelColor | Color |
macOS.NSColor.textColor | Color |
macOS.NSColor.placeholderTextColor | Color |
macOS.NSColor.selectedTextColor | Color |
macOS.NSColor.textBackgroundColor | Color |
macOS.NSColor.selectedTextBackgroundColor | Color |
macOS.NSColor.keyboardFocusIndicatorColor | Color |
macOS.NSColor.unemphasizedSelectedTextColor | Color |
macOS.NSColor.unemphasizedSelectedTextBackgroundColor | Color |
macOS.NSColor.linkColor | Color |
macOS.NSColor.separatorColor | Color |
macOS.NSColor.selectedContentBackgroundColor | Color |
macOS.NSColor.unemphasizedSelectedContentBackgroundColor | Color |
macOS.NSColor.selectedMenuItemTextColor | Color |
macOS.NSColor.gridColor | Color |
macOS.NSColor.headerTextColor | Color |
macOS.NSColor.alternatingContentBackgroundColors | Color[] |
macOS.NSColor.controlAccentColor | Color |
macOS.NSColor.controlColor | Color |
macOS.NSColor.controlBackgroundColor | Color |
macOS.NSColor.controlTextColor | Color |
macOS.NSColor.disabledControlTextColor | Color |
macOS.NSColor.selectedControlColor | Color |
macOS.NSColor.selectedControlTextColor | Color |
macOS.NSColor.alternateSelectedControlTextColor | Color |
macOS.NSColor.currentControlTint | String |
macOS.NSColor.windowBackgroundColor | Color |
macOS.NSColor.windowFrameTextColor | Color |
macOS.NSColor.underPageBackgroundColor | Color |
macOS.NSColor.findHighlightColor | Color |
macOS.NSColor.highlightColor | Color |
macOS.NSColor.shadowColor | Color |
macOS.NSColor.systemBlueColor | Color |
macOS.NSColor.systemBrownColor | Color |
macOS.NSColor.systemGrayColor | Color |
macOS.NSColor.systemGreenColor | Color |
macOS.NSColor.systemIndigoColor | Color |
macOS.NSColor.systemOrangeColor | Color |
macOS.NSColor.systemPinkColor | Color |
macOS.NSColor.systemPurpleColor | Color |
macOS.NSColor.systemRedColor | Color |
macOS.NSColor.systemTealColor | Color |
macOS.NSColor.systemYellowColor | Color |
See public CSS colors:
Key | Type |
---|---|
GTK.theme_name | String |
GTK.theme_fg_color | Color |
GTK.theme_bg_color | Color |
GTK.theme_base_color | Color |
GTK.theme_selected_bg_color | Color |
GTK.theme_selected_fg_color | Color |
GTK.insensitive_bg_color | Color |
GTK.insensitive_fg_color | Color |
GTK.insensitive_base_color | Color |
GTK.theme_unfocused_fg_color | Color |
GTK.theme_unfocused_bg_color | Color |
GTK.theme_unfocused_base_color | Color |
GTK.theme_unfocused_selected_bg_color | Color |
GTK.theme_unfocused_selected_fg_color | Color |
GTK.borders | Color |
GTK.unfocused_borders | Color |
GTK.warning_color | Color |
GTK.error_color | Color |
GTK.success_color | Color |
The Preferences.appearance
property indicates whether the operating system uses a light or dark color scheme. A well-designed dark mode integration requires a JavaFX application to not only provide a set of suitable dark stylesheets, but also configure the platform window decorations to reflect the dark theme. This requires a new API to control the appearance of window decorations:
public class Stage {
...
public ObjectProperty<Appearance> appearanceProperty();
public Appearance getAppearance();
public void setAppearance(Appearance appearance);
...
}
By default, a JavaFX stage uses a light appearance. Developers can set the stage appearance to either light or dark independently from the operating system's dark mode setting. This can be useful for applications that only offer either a light or a dark theme. Applications that support both appearances and want to reflect the OS preference can do so by simply binding the stage appearance to the platform appearance:
var stage = new Stage();
stage.appearanceProperty().bind(
Platform.getPreferences().appearanceProperty());
stage.setScene(...);
stage.show();
The following screenshots show the same stage with a light and dark appearance on macOS:
The new javafx.css.StyleTheme
interface promotes themes to first-class citizens in JavaFX:
public interface StyleTheme {
List<Stylesheet> getStylesheets();
}
Note that the getStylesheets()
method returns a list of javafx.css.Stylesheet
instances, not a list of URIs. Applications can load stylesheets from CSS files with a set of new method on the Stylesheet
class:
public class Stylesheet {
...
public static Stylesheet load(String url);
public static Stylesheet load(InputStream stream);
...
}
A future enhancement of the javafx.css
APIs might allow applications to programmatically create Stylesheet
instances from scratch, without parsing external CSS files or data URIs.
If a StyleTheme
implementation returns an ObservableList<Stylesheet>
from its StyleTheme.getStylesheets()
method, the CSS subsystem observes the list for changes and automatically reapplies the stylesheets when the list has changed. This allows themes to dynamically respond to changing platform or user preferences by adding or removing stylesheets at runtime.
A new Application.userAgentStyleTheme
property is added to javafx.application.Application
, and Application.userAgentStylesheet
is promoted to a JavaFX property (currently, this is just a getter/setter pair):
public abstract class Application
...
public static StringProperty userAgentStylesheetProperty();
public static String getUserAgentStylesheet();
public static void setUserAgentStylesheet(String url);
...
public static ObjectProperty<StyleTheme> userAgentStyleThemeProperty();
public static StyleTheme getUserAgentStyleTheme();
public static void setUserAgentStyleTheme(StyleTheme theme);
}
userAgentStyleTheme
and userAgentStylesheet
are correlated to preserve backwards compatibility: setting userAgentStylesheet
to the magic values "CASPIAN" or "MODENA" will implicitly set userAgentStyleTheme
to a new instance of the CaspianTheme
or ModenaTheme
class. In the CSS cascade, userAgentStylesheet
has a higher precedence than userAgentStyleTheme
. Setting userAgentStylesheet
to a value other than "CASPIAN" or "MODENA" will override the style theme of the application in its entirety, preserving backwards compatibility with applications that were created before the style theme API was added.
A future enhancement may add a userAgentStyleTheme
property to other scene graph classes like Scene
, SubScene
or Region
. This would allow applications to use different style themes across the application.
The default appearance of a newly created Stage
can be changed by a StyleTheme
implementation. For this purpose, the javafx.stage.DefaultAppearance
annotation is added:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultAppearance {
// Specifies the default appearance of newly created stages.
Appearance value() default Appearance.LIGHT;
// Specifies the name of a parameterless method on the annotated StyleTheme
// that returns an ObservableValue<Appearance>, which determines the default
// appearance of newly created stages.
String source() default "";
}
StyleTheme
implementations can use this annotation to specify either a new constant default value, or the name of a method that returns an ObservableValue<Appearance>
to which the Stage.appearance
property is automatically bound (unless overridden by user code):
/* Option 1 */ @DefaultAppearance(Appearance.DARK) // constant default value
/* Option 2 */ @DefaultAppearance(source = "myAppearanceProperty") // binding source for Stage.appearance
public class MyTheme implements StyleTheme {
@Override
List<Stylesheet> getStylesheet() {
...
}
private final ObjectProperty<Appearance> myAppearance = new SimpleObjectProperty(Appearance.DARK);
public final ObjectProperty<Appearance> myAppearanceProperty() {
return myAppearance;
}
}
To avoid StyleTheme
implementations from overriding user code, the default appearance is only applied to newly created stages if the Stage.appearance
property has not been set or bound by user code before the stage is shown. Consider the following code:
// Case 1: Theme with constant appearance
@DefaultAppearance(Appearance.DARK)
class MyTheme1 implements StyleTheme { ... }
Application.setUserAgentStyleTheme(new MyTheme1());
// The following stage has a dark appearance by default, as if
// stage.setAppearance(Appearance.DARK) had been called
var stage1 = new Stage();
stage1.setScene(...);
stage2.show();
// The following stage has a light appearance, because the value of
// the Stage.appearance property has been set by user code before
// the stage is shown. This prevents MyTheme1 from overriding the
// property value.
var stage2 = new Stage();
stage2.setAppearance(Appearance.LIGHT);
stage2.setScene(...);
stage2.show();
// Case 2: Theme with observable appearance
@DefaultAppearance(source = "myAppearance")
class MyTheme2 implements StyleTheme {
public ObjectProperty<Appearance> myAppearanceProperty() { ... }
}
Application.setUserAgentStyleTheme(new MyTheme2());
// For the following stage, the Stage.appearance property will be
// automatically bound to the MyTheme2.myAppearance property.
var stage3 = new Stage();
stage3.setScene(...);
stage3.show();
// For the following stage, the Stage.appearance property will NOT
// be bound to the MyTheme2.myAppearance property, since it was set
// by user code before the stage was shown:
var stage4 = new Stage();
stage4.setAppearance(Appearance.LIGHT);
stage4.setScene(...);
stage4.show();
The two built-in themes CaspianTheme
and ModenaTheme
are exposed as public API in the javafx.scene.control.theme
package. Both classes extend ThemeBase
, which is a simple StyleTheme
implementation that allows developers to easily extend the built-in themes by prepending or appending additional stylesheets:
Application.setUserAgentStyleTheme(new ModenaTheme() {
{
addFirst(Stylesheet.load("stylesheet1.css"));
addLast(Stylesheet.load("stylesheet2.css"));
}
});
ThemeBase
has no other extension points aside from addFirst
and addLast
, since the built-in theme stylesheets are not designed to support further extension. A future enhancement may refactor the built-in themes to make color definitions swappable, and automatically pick up preferred platform colors and accent colors.