Skip to content

Instantly share code, notes, and snippets.

@mstr2
Last active December 2, 2024 16:17
Show Gist options
  • Save mstr2/cbb93bff03e073ec0c32aac317b22de7 to your computer and use it in GitHub Desktop.
Save mstr2/cbb93bff03e073ec0c32aac317b22de7 to your computer and use it in GitHub Desktop.

Media Queries

In JavaFX 22, we added Platform Preferences to enable applications adjust their styling to OS preferences. However, except for replacing its stylesheets entirely, there is no easy way for an application to adjust its stylesheets at runtime.

We propose to add media queries to JavaFX, which is a powerful feature that allows stylesheets to dynamically test certain aspects of the JavaFX scene. Media queries are independent of the contents of the scene graph, its styling, or any other internal aspect; they’re only dependent on "external" configuration of the scene.

A media query is defined in a stylesheet as follows:

.button {
  -fx-background-color: lightgray;
}

@media (prefers-color-scheme: dark) {
  .button {
    -fx-background-color: darkgray;
  }
}

In this example, the button's background color will change at runtime when the preferred color scheme is changed to dark. Since a media query can contain arbitrary CSS rules, all aspects of styling can be adjusted.

Media features

JavaFX CSS does not support the concept of media types at the moment; it only supports media features. A media query tests one or several media features. JavaFX supports the following media features:

Media feature Values
prefers-color-scheme light | dark
prefers-reduced-motion no-preference | reduce
prefers-reduced-transparency no-preference | reduce

These media features correspond to the colorScheme, reducedMotion, and reducedTransparency platform preferences.

Media query context

Media queries are not evaluated against the platform preferences directly, but instead against the Scene that contains the styleable scene graph. If we evaluted media queries against the platform preferences directly, application developers would have no way of providing in-app toggles, e.g. an option to change the application color scheme independently from the operating system.

Therefore, the following properties are added to Scene:

public class javafx.scene.Scene {
    ...

    /**
     * Specifies whether the scene should prefer light text on dark backgrounds, or dark text
     * on light backgrounds.
     * <p>
     * This is a <em>null-coalescing</em> property: if set to {@code null}, it evaluates to the
     * value of {@link Platform.Preferences#colorSchemeProperty()}. Therefore, specifying a value
     * for this property overrides the platform-provided value.
     * <p>
     * This property corresponds to the following CSS media feature:
     * <table class="striped">
     *     <caption>Media Feature</caption>
     *     <tbody>
     *         <tr><th>Name</th><td><code>prefers-color-scheme</code></td></tr>
     *         <tr><th>For</th><td><code>@media</code></td></tr>
     *         <tr><th>Value</th><td><code>light</code> | <code>dark</code></td></tr>
     *         <tr><th>Boolean Context</th><td>no</td></tr>
     *     </tbody>
     * </table>
     *
     * @defaultValue {@link Platform.Preferences#getColorScheme()}
     */
     public final ObjectProperty<ColorScheme> colorSchemeProperty();
     
    /**
     * Specifies whether the scene should minimize the amount of non-essential animations,
     * reducing discomfort for users who experience motion sickness or vertigo.
     * <p>
     * This is a <em>null-coalescing</em> property: if set to {@code null}, it evaluates to the
     * value of {@link Platform.Preferences#reducedMotionProperty()}. Therefore, specifying a
     * value for this property overrides the platform-provided value.
     * <p>
     * This property corresponds to the following CSS media feature:
     * <table class="striped">
     *     <caption>Media Feature</caption>
     *     <tbody>
     *         <tr><th>Name</th><td><code>prefers-reduced-motion</code></td></tr>
     *         <tr><th>For</th><td><code>@media</code></td></tr>
     *         <tr><th>Value</th><td><code>no-preference</code> | <code>reduce</code></td></tr>
     *         <tr><th>Boolean Context</th>
     *             <td>yes, <code>no-preference</code> evaluates as <code>false</code></td>
     *         </tr>
     *     </tbody>
     * </table>
     *
     * @defaultValue {@link Platform.Preferences#isReducedMotion()}
     */
    public final ObjectProperty<Boolean> reducedMotionProperty();
    
    /**
     * Specifies whether the scene should minimize the amount of transparent or translucent
     * layer effects, which can help to increase contrast and readability for some users.
     * <p>
     * This is a <em>null-coalescing</em> property: if set to {@code null}, it evaluates to the
     * value of {@link Platform.Preferences#reducedTransparencyProperty()}. Therefore, specifying
     * a value for this property overrides the platform-provided value.
     * <p>
     * This property corresponds to the following CSS media feature:
     * <table class="striped">
     *     <caption>Media Feature</caption>
     *     <tbody>
     *         <tr><th>Name</th><td><code>prefers-reduced-transparency</code></td></tr>
     *         <tr><th>For</th><td><code>@media</code></td></tr>
     *         <tr><th>Value</th><td><code>no-preference</code> | <code>reduce</code></td></tr>
     *         <tr><th>Boolean Context</th>
     *             <td>yes, <code>no-preference</code> evaluates as <code>false</code></td>
     *         </tr>
     *     </tbody>
     * </table>
     *
     * @defaultValue {@link Platform.Preferences#isReducedTransparency()}
     */
    public final ObjectProperty<Boolean> reducedTransparencyProperty();
    
    ...
  }

All new properties are null-coalescing: if set to null, they evaluate to their respective platform preference. This provides a way for users to specify "no preference", as well as override the platform preference with a specific value.

It also allows developers to create multi-window applications where each window can be independently configured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment