Marble Theme
since 11.0.0
What is Marble
Marble is ZK’s default look and feel since 11.0, styled after Material Design 3. You customize it with CSS custom properties and a few declarative HTML attributes, so you can change it without forking the theme or running a build step.
What You Can Customize
| You want to… | Use |
|---|---|
| Recolor the whole application from one brand color | Brand Color |
| Make the whole application, or one region, more compact | Density |
| Restyle one kind of component, for example make all buttons pill-shaped | Component Theme Variables |
| Add spacing, layout or visibility rules in ZUL without writing CSS | Utility Classes |
Marble also supports Windows high-contrast mode, the reduced-motion setting and printing out of the box. See Accessibility and Print.
Current Limitations
- Light mode only: Marble has no dark mode.
- RTL not complete: ZK supports
dir="rtl", but Marble’s CSS does not fully support right-to-left layouts yet. For example, directional icons such as chevrons and arrows are not flipped.
Getting Started
Upgrade to ZK 11.0 or later, and your application uses Marble. No configuration is needed.
<zk>
<window title="Hello Marble" border="normal">
<vlayout>
<label value="Your app now uses the Marble theme"/>
<button label="Click me"/>
</vlayout>
</window>
</zk>
Marble and IceBlue
Marble and IceBlue are two parallel themes, not an upgrade path from one to the other. IceBlue was the default theme from ZK 8.5 to 10.x; Marble is the default since 11.0.
Staying on IceBlue
If your application needs to keep IceBlue, see Switching Themes for how to include and select a theme, and org.zkoss.theme.preferred for the library property.
CSS Variables Compatibility Note
The CSS Variables page (since 10.3.0) describes the variables of the IceBlue theme. Variable names used in its examples are not guaranteed to exist in Marble, so the overrides shown there may have no effect. For Marble, use the Brand Color, Density and Component Theme Variables sections below.
Brand Color
Change Marble’s brand color with one value. The rest of the palette is derived from it automatically.
How It Works
The brand color is the CSS custom property --zk-color-primary, called the seed. Marble derives the related colors from it with CSS relative color syntax (oklch(from …)): the light container tint (--zk-color-primary-container), the dark text on that tint (--zk-color-on-primary-container), the secondary color, the hover and pressed overlays, and the keyboard focus ring. Each derived color keeps the seed’s hue but uses a fixed lightness, so a blue, teal, orange or purple seed all produce a legible palette.
The secondary color is derived from the primary seed by default, so changing the primary color re-tints the secondary color too. If your brand has an independent second color, set --zk-color-secondary yourself (see Custom CSS).
The brand color is whole-application only. It must be set on the document root (<html> or :root). Setting data-brand or --zk-color-primary on a container inside the page does not re-derive the colors for that region.
Ways to Set the Brand Color
1. zk.xml Library Property (Recommended for a Fixed Brand)
Set the brand once in zk.xml. ZK renders it with the page, so the first paint already uses your brand color (no flash).
<library-property>
<name>org.zkoss.theme.marble.brand</name>
<value>slate</value> <!-- built-in preset: default, slate, copper -->
</library-property>
Or use your own hex color:
<library-property>
<name>org.zkoss.theme.marble.brand</name>
<value>#6a1b9a</value> <!-- #rgb or #rrggbb -->
</library-property>
For a preset, ZK renders a data-brand attribute on <html>. For a hex color, ZK renders <style>:root{--zk-color-primary:#…}</style> right after the theme stylesheets.
Scope: This property only works when the ZUL page renders the whole HTML document (a top-level ZUL page). If the ZUL page is included by another page, embedded in a JSP or another template, or uses zhtml’s <html> as its root element, you write the <html> tag yourself. In that case, add the attribute to your own tag for a preset (for example <html data-brand="slate">), or load a CSS file that sets --zk-color-primary through a theme URI for a hex color.
See also: org.zkoss.theme.marble.brand
2. Page Root Attributes
To use a preset on one ZUL page, put the data-brand attribute on that page’s <html> element with the root-attributes directive:
<?root-attributes data-brand="copper"?>
<zk>
<window>…</window>
</zk>
This overrides the library property. For a hex color, use custom CSS (option 3).
3. Custom CSS (Loaded After the Theme)
Override the seed in a CSS file that is loaded after the theme CSS:
/* custom-brand.css */
:root {
--zk-color-primary: #6a1b9a; /* your brand color; everything else derives */
}
Load it for the whole application in zk.xml:
<desktop-config>
<theme-uri>/css/custom-brand.css</theme-uri>
</desktop-config>
Or on one page:
<?link rel="stylesheet" type="text/css" href="/css/custom-brand.css"?>
For a fuller rebrand, override the other seeds too. Each one re-derives its own container colors. Setting --zk-color-secondary stops it from being derived from the primary color:
:root {
--zk-color-primary: #6a1b9a; /* brand purple */
--zk-color-secondary: #00897b; /* independent second color */
--zk-color-error: #c62828;
--zk-color-warning: #ef6c00;
}
If one derived color is not quite right for your brand, set it directly. The others still derive from the seed:
:root {
--zk-color-primary: #6a1b9a;
--zk-color-primary-container: #ede7f6; /* pin just this one */
}
4. Runtime Switch with Java
To let users pick a brand at runtime, call MarbleBrand.apply() inside an event listener or an MVVM command:
import org.zkoss.zul.theme.MarbleBrand;
MarbleBrand.apply(MarbleBrand.Brand.SLATE); // switch the whole app to a preset
MarbleBrand.apply(MarbleBrand.Brand.DEFAULT); // back to the default blue
MVVM example:
@Command
public void selectBrand(@BindingParam("brand") String brand) {
MarbleBrand.apply(MarbleBrand.Brand.valueOf(brand)); // "DEFAULT", "SLATE" or "COPPER"
}
Use this only for a choice the user makes at runtime. MarbleBrand.apply() runs after the first paint, so calling it when a page loads causes a brief color flash. For a fixed brand, use the library property (option 1) or custom CSS (option 3).
Precedence
From the weakest to the strongest:
- The
org.zkoss.theme.marble.brandlibrary property <?root-attributes data-brand="..."?>in the page- CSS loaded by the page’s own
<?link?> - Runtime calls to
MarbleBrand.apply()
Built-in Presets
| Preset | Seed |
|---|---|
default (blue) |
#376fd0 |
slate |
#506274 |
copper |
#b45309 |
All three are mid-to-dark colors, so white text on their filled surfaces meets WCAG AA contrast.
Filled Background Classes
To paint a button or a surface with a role color, use the z-bg-* utility classes: z-bg-primary, z-bg-secondary, z-bg-error, z-bg-success, z-bg-warning and z-bg-info. Their background is darkened automatically when the color is too light, so white text on them stays legible whichever brand color you choose:
<button label="Save" sclass="z-bg-secondary"/>
What Is Not Re-tinted
- Neutral surfaces: the
--zk-color-surface*colors keep Marble’s own light, cool tint. They do not follow the brand color. - Status colors:
--zk-color-status-success,-warning,-error,-infoand-neutralare a separate palette (success is always green, and so on). They do not follow the brand color.
Override them directly if you need to.
Light Brand Colors
⚠ Contrast Warning
--zk-color-on-primary (the text color on filled primary surfaces, such as the default button) is always white, and the default button uses the brand color as is. If you set a light brand color, for example yellow #ffd400, the white text falls below the WCAG AA contrast ratio (4.5:1).
Choose one of these:
- Pick a medium or dark brand color. The built-in presets (blue, slate, copper) all work.
-
Override
--zk-color-on-primarywith a dark color in your CSS::root { --zk-color-primary: #ffd400; /* light yellow */ --zk-color-on-primary: #1a1a1a; /* dark text */ } - Paint the buttons with
z-bg-primary, which darkens a light brand color enough for white text (see Filled Background Classes).
ZK does not choose white or black text automatically. This is intentional.
Density
Marble’s default (comfortable) spacing and control heights suit general business UIs. For data-dense applications, such as ERP screens with many rows and fields, switch to compact density to tighten control heights, row heights and cell padding.
How It Works
Density is controlled by the data-density attribute. Put data-density="compact" on an element, and everything inside it shrinks. On <html>, it applies to the whole application, including popups appended to the body (menus, modal windows, notifications). On a container, it applies only to that region. It nests: a container inside a compact region can set data-density="comfortable" to go back.
Ways to Set Density
1. zk.xml Library Property (Recommended for a Fixed Default)
Set the default density for the whole application once. ZK renders data-density="compact" on <html> with the page, so the first paint is already compact (no flash).
<library-property>
<name>org.zkoss.theme.marble.density</name>
<value>compact</value> <!-- comfortable (default) | compact -->
</library-property>
It can also be set as a Java system property: -Dorg.zkoss.theme.marble.density=compact.
Scope: This property only works when the ZUL page renders the whole HTML document. If the ZUL page is included by another page, embedded in a JSP or another template, or uses zhtml’s <html> as its root element, write the attribute in your own <html> tag instead:
<html data-density="compact">
See also: org.zkoss.theme.marble.density
2. Single Region (ZUL Container)
Make one region compact while the rest of the page stays comfortable. In ZUL, set the attribute through the client/attribute namespace:
<zk xmlns:ca="client/attribute">
<vlayout ca:data-density="compact">
<grid>…</grid>
</vlayout>
</zk>
<vlayout data-density='compact'> without the ca: prefix. data-density is not a property of the component, so ZK fails with a PropertyNotFoundException.
3. Runtime Switch with Java
To let users switch density at runtime, call MarbleDensity.apply() inside an event listener or an MVVM command:
import org.zkoss.zul.theme.MarbleDensity;
// Whole app
MarbleDensity.apply(MarbleDensity.Density.COMPACT);
MarbleDensity.apply(MarbleDensity.Density.COMFORTABLE); // back to the default
// One region: a component and its descendants
MarbleDensity.apply(myGridPanel, MarbleDensity.Density.COMPACT);
MVVM example with a toggle:
private boolean compact;
@Command
public void toggleDensity() {
compact = !compact;
MarbleDensity.apply(compact ? MarbleDensity.Density.COMPACT
: MarbleDensity.Density.COMFORTABLE);
}
Calling MarbleDensity.apply() when a page loads causes a brief flash, because it runs after the first paint. For a fixed default, use the library property (option 1) or write data-density in your own <html> tag.
4. Your Own Switch
Marble does not ship a density switch widget. The data-density attribute is the only contract, so you can also set it from your own JavaScript, for example:
document.documentElement.setAttribute('data-density', 'compact');
Precedence
From the weakest to the strongest:
- The
org.zkoss.theme.marble.densitylibrary property <?root-attributes data-density="..."?>in the page- CSS loaded by the page’s own
<?link?> - Runtime calls to
MarbleDensity.apply()
Compact Values
| Token | Comfortable | Compact | Used by |
|---|---|---|---|
--zk-control-height-xs |
28px | 24px | small and icon buttons, close buttons |
--zk-control-height-sm |
32px | 28px | paging controls, window close button |
--zk-control-height-md |
40px | 32px | inputs, icon buttons, list rows, vertical menu items |
--zk-control-height-lg |
48px | 40px | toolbar, menubar, tabs, panel and groupbox header, notification |
--zk-control-height-xl |
56px | 48px | window header |
--zk-button-height |
36px | 30px | buttons, combobutton |
--zk-button-height-lg |
44px | 38px | .z-button-lg |
--zk-menuitem-height |
36px | 30px | popup menu rows |
--zk-data-row-min-height |
52px | 36px | tree rows, grid and listbox paging bar |
--zk-grid-cell-padding |
16px | 6px 12px | grid cells |
--zk-listbox-cell-padding |
16px | 6px 12px | listbox cells |
--zk-tree-cell-padding |
8px / 16px | 4px 12px | tree cells |
For example, inputs shrink from 40px to 32px, buttons from 36px to 30px, and grid cells from 16px padding to 6px top and bottom and 12px left and right.
Tuning the Values
To change the compact values, write your own html[data-density="compact"] rule and set any token from the table above. This selector is more specific than Marble’s own rule, so it wins regardless of the loading order:
html[data-density="compact"] {
--zk-button-height: 28px;
}
You can also adjust a single size without touching the rest, for example --zk-toolbar-height: 40px;.
Intentionally Not Scaled
Icon sizes, calendar day cells, the minimum height of a textarea, and decorative spacing stay the same in compact mode. Scaling them would distort icons and content-driven layouts. Adjust them individually if you need to.
Touch Targets
Compact heights are below the Material Design 3 minimum touch target of 44 to 48px. Compact mode is meant for dense desktop screens, not touch devices.
Component Theme Variables
To restyle one kind of component without forking the theme or adding !important, override that component’s theme variables. Each themed component reads a small set of CSS custom properties named --zk-<component>-*, such as --zk-button-bg and --zk-button-radius.
Whole Application
Set the variables on :root in a stylesheet that is loaded after the theme CSS (for example, through a theme-uri in zk.xml):
:root {
--zk-button-radius: 9999px; /* every button becomes a pill */
--zk-button-bg: #6750a4;
}
The theme declares its defaults on :root too, so a stylesheet loaded before the theme CSS loses. If you cannot control the loading order, use a more specific selector such as html:root.
One Region
Variables inherit, so setting them on a container restyles only the components inside it. Loading order does not matter here:
<div style="--zk-button-radius:9999px; --zk-button-bg:#6750a4; --zk-button-fg:#fff">
<button label="Pill"/> <!-- restyled -->
</div>
<button label="Default"/> <!-- not inside the div, unchanged -->
Rules
- Override the component variable, not a global token, for a region. For example, to recolor the buttons in one region, set
--zk-button-bgon the container. Setting--zk-color-primaryon the container does not change them, because the component defaults are resolved once on:root. - Typography is global. Font size, weight and line height come from the shared type scale, not from component variables.
- Finding the variables: the defaults are declared on
:root, so you can see which--zk-<component>-*variables a component reads by inspecting it in your browser’s developer tools.
Utility Classes
Marble ships utility classes with the z- prefix, for spacing, display, flex and grid layout, sizing, typography, borders, colors and printing. Use them in sclass to lay out a page without writing CSS.
Naming Rules
- Bootstrap names for properties:
z-m-1(margin),z-px-2(horizontal padding),z-d-flex(display),z-position-absolute,z-border-top. - Tailwind names where they are shorter:
z-justify-center,z-items-center,z-gap-4,z-text-xs. - One name per rule: there are no aliases.
- Logical directions:
startandendinstead ofleftandright, for examplez-ms-2(margin at the start side) andz-border-end. - Breakpoints in the middle:
z-d-md-none(hide at the medium breakpoint and wider). A trailing-sm,-mdor-lgalways means a size, as inz-vstack-md.
Spacing Policy
Marble components have no default margin. You declare spacing with a parent container class or a utility class.
Use stack classes to space children:
<div sclass="z-vstack-md"> <!-- 16px between stacked children -->
<window>A</window>
<window>B</window>
</div>
<div sclass="z-hstack"> <!-- 12px between children in a row -->
<button label="Save"/>
<button label="Cancel"/>
</div>
Sizes: z-vstack-sm (8px), z-vstack (12px), z-vstack-md (16px), z-vstack-lg (24px); z-hstack has the same sizes. ZK’s own <vlayout spacing> and <hlayout spacing> also work.
For a single exception, use a margin utility:
<window sclass="z-mb-6"/> <!-- margin-bottom: 24px -->
Layout components such as vlayout, hlayout, div and borderlayout bodies have no default padding either. Add a padding utility where you need it:
<vlayout>
<div sclass="z-p-4">Content with 16px padding</div>
</vlayout>
Common Classes (Examples)
<!-- Layout -->
<div sclass="z-d-flex z-gap-4">
<button label="One"/>
<button label="Two"/>
</div>
<!-- Text -->
<label sclass="z-text-lg z-fw-bold" value="Title"/>
<!-- Sizing -->
<div sclass="z-w-100">Full width</div>
<!-- Spacing -->
<div sclass="z-p-4 z-m-0">16px padding, no margin</div>
Responsive Classes
The responsive display classes apply at a breakpoint and wider:
| Breakpoint | Minimum width |
|---|---|
sm |
600px |
md |
900px |
lg |
1200px |
xl |
1536px |
<!-- Hidden on narrow screens, shown at 900px and wider -->
<div sclass="z-d-none z-d-md-block">Wide content</div>
For a card layout that adjusts its column count to the available width without breakpoints, use z-grid-fill with z-d-grid. Each column is at least 180px wide by default; change it with --zk-grid-min:
<div sclass="z-d-grid z-grid-fill z-gap-4" style="--zk-grid-min: 240px">
<div sclass="z-paper z-p-4">Card A</div>
<div sclass="z-paper z-p-4">Card B</div>
<div sclass="z-paper z-p-4">Card C</div>
</div>
Accessibility and Print
Forced Colors (Windows High-Contrast Mode)
When @media (forced-colors: active) applies, the operating system replaces the page colors and removes shadows. Marble restores what would otherwise be lost:
- Focus: inputs show a visible outline when focused.
- Selected rows: use the system
HighlightandHighlightTextcolors. - Surfaces: windows, panels and popups get a border in place of their shadow.
- Icons: follow the system text color.
- Buttons: get a border to show their shape.
- Invalid inputs: get a double border, because the red border color is lost.
This works on every page with no extra code. To test it, use the rendering tools in Chrome or Edge DevTools and emulate the CSS media feature forced-colors: active.
Reduced Motion
When the user’s operating system asks for reduced motion (@media (prefers-reduced-motion: reduce)), Marble shortens all CSS transitions and animations to almost zero.
Marble includes z-d-print-* classes for print layout. They only take effect when printing:
<!-- Hidden when printing, e.g. toolbars and navigation -->
<div sclass="z-d-print-none">Toolbar</div>
<!-- Hidden on screen, shown when printing -->
<div sclass="z-d-none z-d-print-block">For printed copy only</div>
The available values are none, block, flex, grid and inline-block.
When printing, Marble also automatically:
- Hides floating elements such as modal masks, toasts, the loading bar and drawers.
- Unsticks sticky grid, listbox and tree headers, so long tables can break across pages.
- Expands scrollable grid, listbox and tree bodies, so all loaded rows are printed.
- Replaces shadows with thin borders.
- Keeps brand colors and backgrounds (they are not converted to grayscale).
Frozen columns (<frozen>) are positioned by JavaScript, so a horizontally scrolled grid may be cut off on paper. For wide reports, consider paging or a server-side export.