CSS DSL

JWeb's CSS DSL provides type-safe Java methods for all CSS properties. Write styles with IDE autocomplete and compile-time validation.

Import Statements

// One import covers the whole core CSS DSL: style(), rule(), units
// (px, rem, vh, ...), colors (named, rgb(), hex(), hsl()), grid,
// animations, variables, media() and keyframes()
import static jweb.Css.*;

// The Style type your helpers return
import jweb.Style;

Bare Styles — the concise form

When an element only needs styling, pass style() directly as an argument — no attrs() ceremony. It composes with Attr shortcuts like class_() and id().

CSSValue SP_4 = rem(1);
CSSValue TEXT = hex("#1e293b");

div(style().padding(SP_4).color(TEXT), "hi")

div(class_("card"), id("hero"),
    style().margin(zero).borderRadius(px(12)),
    p("content"))

Composing Style Fragments

Define a style once, reuse it everywhere with apply() — the composition primitive for design systems.

// In your Theme:
public static jweb.Style<?> brandFlow() {
    return style().background(BRAND_GRADIENT)
                  .backgroundSize(percent(300), percent(100))
                  .animation("gradientShift", s(3), linear, s(0), infinite);
}

// Anywhere:
button(style().padding(SP_3).apply(brandFlow()).color(white), ...)

// Handy helpers that replace prop("...") strings:
style().content()                      // content: '' for ::before/::after
style().borderMask()                   // gradient-border mask trick
style().srOnly()                       // screen-reader-only pattern
style().transitionAll(s(0.2))          // also transitionColors/Background/...
style().backdropFilter(blur(px(10)))   // emits -webkit- prefix too

Inline Styles

Apply styles directly to elements using the style builder.

// Bare style() builder as a standalone argument
div(class_("card"),
    style()
        .padding(rem(1.5))
        .backgroundColor(white)
        .borderRadius(px(8))
        .boxShadow(px(0), px(2), px(8), rgba(0, 0, 0, 0.1)),
    content
)

// Reusable Style object
Style cardStyle = style()
    .padding(rem(1.5))
    .backgroundColor(white)
    .borderRadius(px(8));

div(cardStyle, content)

CSS Rules

Generate CSS rules for stylesheets.

Cls BTN = cls("btn");
Cls CONTAINER = cls("container");

// Single rule
String buttonCss = rule(BTN)
    .display(inlineBlock)
    .padding(px(10), px(20))
    .backgroundColor(blue)
    .color(white)
    .borderRadius(px(4))
    .toRule();

// Multiple rules
String css = styles(
    rule(Selector.any()).boxSizing(borderBox),
    rule(Selector.type("body"))
        .margin(zero)
        .fontFamily("system-ui, -apple-system, sans-serif"),
    rule(CONTAINER)
        .maxWidth(px(1200))
        .margin(px(0), auto)
        .padding(px(0), rem(1))
);

Page-owned stylesheets

A component owns the class rules it needs. Override styles() and the render collects it — the page's, its layout's, and every template rendered inside them — dedupes by content, and emits one <style> in the document head. Ten instances of a card contribute one copy.

public class Card implements Template {
    static final Cls CARD = cls("card");

    @Override
    public Element render() {
        String title = "Welcome";
        String body = "Hello World!";
        return div(class_("card"), h3(title), p(body));
    }

    @Override
    public Stylesheet styles() {
        return stylesheet()
            .rule(CARD, style().padding(rem(1)).borderRadius(px(12)))
            .rule(CARD.hover(), style().boxShadow(zero, px(4), px(12), rgba(0, 0, 0, 0.1)))
            .add(md().rule(CARD, style().padding(rem(1.5))));
    }
}

A layout's stylesheet is the natural home for the reset and the design tokens, so no page has to remember to include them.

static final CSSValue TEXT = hex("#1e293b");
static final CSSValue BG = hex("#ffffff");
static final Theme TOKENS = Theme.light().color("text", TEXT).color("bg", BG);

public class Layout implements Template {

    @Override
    public Element render() {
        return body();
    }

    @Override
    public Stylesheet styles() {
        return stylesheet()
            .add(TOKENS)
            .rule(Selector.any().or(Selector.any().before()).or(Selector.any().after()), style().boxSizing(borderBox).margin(zero))
            .rule(Selector.type("body"), style().color(TEXT).backgroundColor(BG));
    }
}

Pseudo-classes and media on an inline style

An inline style can carry conditional rules — the things a style attribute cannot express. They get a generated class named after a hash of the rules themselves, the rules go into the page's collected stylesheet, and the plain declarations still ride the style attribute.

CSSValue PRIMARY = hex("#4f46e5");
CSSValue SP_2 = rem(0.5);
CSSValue SP_3 = rem(0.75);
CSSValue SP_4 = rem(1);
CSSValue SP_6 = rem(1.5);
CSSValue ROUNDED = px(8);

a(href("/docs"), style()
        .color(PRIMARY).padding(SP_2, SP_4).borderRadius(ROUNDED)
        .hover(style().backgroundColor(hex("#eef2ff")))
        .focusVisible(style().outline(px(2), solid, PRIMARY))
        .at(md(), style().padding(SP_3, SP_6))
        .dark(style().color(hex("#a5b4fc"))),
    "Documentation")

// renders:  <a href="/docs" class="j-3f9a1c" style="color: var(--color-primary); …">
// and adds: .j-3f9a1c:hover{background-color:#eef2ff}
//           .j-3f9a1c:focus-visible{outline:2px solid var(--color-primary)}
//           @media (min-width: 768px){.j-3f9a1c{padding:0.75rem 1.5rem}}
//           @media (prefers-color-scheme: dark){.j-3f9a1c{color:#a5b4fc}}

Because the class name is a content hash, fifty buttons with the same hover rule share one class and one rule block, and the name is the same in every render — a swap fragment naming a class the page already carries is a no-op.

The conditional vocabulary

import static jweb.css.ContainerQuery.*;

Style<?> s = style();

style()
    .hover(s)              // :hover
    .focus(s)              // :focus
    .focusVisible(s)       // :focus-visible — keyboard focus only
    .focusWithin(s)        // :focus-within
    .active(s)             // :active
    .visited(s)            // :visited
    .disabled(s)           // :disabled
    .checked(s)            // :checked
    .placeholder(s)        // ::placeholder
    .popoverOpen(s)        // :popover-open
    .before("→ ", s)       // ::before, with its content
    .after("", s)          // ::after
    .on(":nth-child(2n)", s)              // anything else, colons included
    .at(md(), s)                          // inside a media query
    .at(container("card").minWidth(px(400)), s)   // inside a container query
    .dark(s)               // @media (prefers-color-scheme: dark)
    .reducedMotion(s)      // @media (prefers-reduced-motion: reduce)
    .startingStyle(s)      // @starting-style — the values a transition starts from

They nest: a hover inside an at() is wrapped by that query too. And apply() carries them, so a mixin can bring its own hover state.

static final CSSValue ROUNDED = px(8);
static final CSSValue TEXT = hex("#1a1a1a");
static final CSSValue SP_1 = rem(0.25);
static final CSSValue SP_2 = rem(0.5);
static final CSSValue SP_3 = rem(0.75);

static Style<?> chip() {
    return row(SP_2).padding(SP_1, SP_3).borderRadius(ROUNDED)
                    .hover(style().backgroundColor(hex("#eef2ff")));
}

span(chip().color(TEXT), "Java 21")      // the hover comes along

Fragments and streaming

A swap fragment, a streamed Suspense block and a WebSocket DOM patch each carry the CSS their own render introduced — a <style> inserted through innerHTML applies, unlike a script. Nothing is sent twice: each render path drains only what it has not delivered yet.

Handles: one name for HTML, CSS and JS

cls("card") returns a Cls and id("x") an Id: an element argument, an immutable Selector and a JavaScript target. Declare it once as a static field; the stylesheet and the script name the same thing the element does, and a rename is a refactor instead of a search.

public class Card implements Template {
    static final Cls CARD = cls("card");
    static final Cls COPY = cls("copy");
    static final Id EDITOR = id("editor");

    @Override
    public Element render() {
        return article(CARD,
            button(COPY, "Copy"),
            textarea(EDITOR));
    }

    @Override
    public Stylesheet styles() {
        return stylesheet()
            .rule(CARD, style().padding(rem(1)).borderRadius(px(12)))
            .rule(COPY.hover(), style().color(hex("#4f46e5")))
            .rule(CARD.descendant(Selector.type("pre")), style().margin(zero))
            .rule(EDITOR.focus(), style().outline(none))
            .add(media().maxWidth(px(960)).rule(CARD, style().display(block)));
    }

    @Override
    public Optional<Action> scripts() {
        return Optional.of(actions().does(delegate(CARD, "click", COPY)
            .handler(callback("e", "t").does(copyFrom(Selector.type("pre")).trigger(v("t"))))));
    }
}

Every rule(...) takes a Selector as well as a String — on Stylesheet, MediaQuery, ContainerQuery, Supports, Rule.of, startingStyle and scope. Tag and universal selectors are Selector.type("pre") and Selector.any(). A multi-word cls("a b") sets both classes; cls() with no name mints a class scoped to the declaring class. A static component attaches its rules with styled(sheet, element), as a Template's styles() does.

Values that were strings

Shadows, font stacks, calc, gradient stops and keyframe names have builders, so no CSS is typed as text.

Keyframes shift = keyframes("shift").from(style().opacity(0)).to(style().opacity(1));

Style<?> card = style()
    .boxShadow(shadow(0, px(1), px(2), rgba(15, 23, 42, 0.04)),
               shadow(0, px(12), px(32), px(-16), rgba(79, 70, 229, 0.35)))
    .fontFamily(uiMonospace, font("SFMono-Regular"), font("Menlo"), monospace)
    .maxHeight(vh(100).minus(px(50)))
    .backgroundImage(radialGradient("circle", stop(hex("#e2e8f0"), px(1)), stop(transparent, px(1))))
    .whiteSpace(preWrap).overflowWrap(anywhere).textOverflow(ellipsis)
    .animation(shift, s(3), linear);

Stylesheet sheet = stylesheet().add(shift).rule(cls("card"), card);
Every CSS property has a camelCase setter and every keyword a constant; prop("name", "value") is never needed. The framework's own app is built under a test that fails on any raw CSS or JS string.

Layout mixins

Every app writes display:flex; align-items:center a hundred times. These are that, named. Each returns a Style, so it composes with apply(), with tokens, and with the conditional rules above.

row()                  // flex, items centred
row(SP_4)              // …with a gap
stack()                // flex column
stack(SP_4)            // …with a gap
center()               // centred on both axes
cluster(SP_2)          // a row that wraps — chips, tags, a toolbar
container(px(1200))    // full width up to a maximum, then centred
card()                 // surface: background, hairline border, radius, padding
truncate()             // one line, ellipsis
truncate(3)            // three lines, ellipsis
srOnly()               // visually hidden, still read aloud
fullBleed()            // break out of the container to the viewport width
aspect(16, 9)          // a fixed ratio
cover() / contain()    // fill or fit a box — images and video
grid(3, SP_4)          // three equal columns
autoGrid(px(240))      // as many columns as fit — no breakpoints needed
// Before
div(style().display(flex).flexDirection(column).gap(SP_4)
           .maxWidth(px(500)).margin(zero, auto), …)

// After
div(stack(SP_4).maxWidth(px(500)).marginInline(auto), …)

// They stack up
nav(cluster(SP_2).justifyContent(spaceBetween).padding(SP_3, GUTTER), …)
div(row(SP_4).apply(card()).hover(style().borderColor(PRIMARY)), …)

Design tokens

A Theme is a set of CSS custom properties. Write it once, emit it through a page's stylesheet, and read it back anywhere as a CSSValue — the value lives in the document, so devtools shows it and a theme swap is a value change rather than a recompile.

import jweb.css.Theme;

public static final Theme TOKENS = Theme.light()
    .color("primary", hex("#4f46e5"))
    .color("text",    hex("#1e293b"))
    .color("bg",      hex("#ffffff"))
    .space("4",       rem(1))
    .radius("md",     px(6))
    .text("lg",       rem(1.125))
    .dark(Theme.dark()
        .color("text", hex("#e2e8f0"))
        .color("bg",   hex("#0f172a")));

// emit it from the layout
public Stylesheet styles() { return stylesheet().add(TOKENS); }

// read it anywhere
style().color(Theme.color("text")).padding(Theme.space("4"))

The named groups are color, space, radius, font, text, shadow and gradient — each prefixes its property (--color-primary, --space-4). token(name, value) defines any other name verbatim, and Theme.var(name, fallback) reads one with a default.

css() emits three blocks: the light values on :root, the dark values under prefers-color-scheme (guarded so an explicit light choice still wins), and the same dark values under [data-theme=dark] so a toggle overrides the system setting in both directions.

:root{--color-text:#1e293b;--color-bg:#ffffff;…}
@media (prefers-color-scheme: dark){
  :root:not([data-theme=light]){--color-text:#e2e8f0;--color-bg:#0f172a;}
}
:root[data-theme=dark]{--color-text:#e2e8f0;--color-bg:#0f172a;}

An app names its tokens once and keeps its own constants — the demo app's Theme is exactly this: TOKENS plus PRIMARY, SP_4, ROUNDED and friends, each a var() reference.

public static final CSSValue PRIMARY = Theme.color("primary");
public static final CSSValue SP_4    = Theme.space("4");
public static final CSSValue ROUNDED = Theme.radius("md");

CSS Units

Type-safe unit functions prevent invalid CSS values.

// Absolute units
px(10)              // 10px - pixels
px(15.5)            // 15.5px - decimal pixels

// Relative units (recommended)
rem(1)              // 1rem - relative to root font-size
rem(1.5)            // 1.5rem
em(1.2)             // 1.2em - relative to parent font-size
percent(50)         // 50%
percent(33.33)      // 33.33%

// Viewport units
vh(100)             // 100vh - viewport height
vw(50)              // 50vw - viewport width
vmin(10)            // 10vmin - smaller of vw/vh
vmax(20)            // 20vmax - larger of vw/vh

// Dynamic viewport (mobile-safe)
dvh(100)            // 100dvh - accounts for mobile browser chrome
dvw(100)            // 100dvw

// Grid units
fr(1)               // 1fr - fraction of available space
fr(2)               // 2fr - 2 fractions

// Time (for animations)
ms(300)             // 300ms - milliseconds
s(0.3)              // 0.3s - seconds

// Angles (for transforms)
deg(45)             // 45deg - degrees
turn(0.5)           // 0.5turn - half rotation

CSS Functions

Mathematical functions for dynamic values.

// calc() - calculations
.width(calc("100% - 200px"))
.height(calc("100vh - 60px"))

// min() - smallest value
.width(min(px(400), percent(100)))

// max() - largest value
.width(max(px(300), percent(50)))

// clamp() - value within range
.fontSize(clamp(rem(1), vw(4), rem(2)))
// Font scales between 1rem and 2rem based on viewport

// Example: responsive container
.maxWidth(min(px(1200), calc("100% - 2rem")))
.padding(clamp(rem(1), vw(5), rem(3)))

Common Constants

Pre-defined values for common CSS keywords.

zero                // 0
auto                // auto
inherit             // inherit
initial             // initial
unset               // unset
none                // none

// Usage
.margin(zero)
.width(auto)
.display(none)

Colors

Multiple color formats with full type safety.

// Hex colors
hex("#6366f1")           // 6-digit hex
hex("#f5f")              // 3-digit hex shorthand

// RGB/RGBA
rgb(99, 102, 241)        // RGB values 0-255
rgba(0, 0, 0, 0.5)       // RGBA with alpha 0-1

// HSL/HSLA
hsl(239, 84, 67)         // hue, saturation%, lightness%
hsla(239, 84, 67, 0.8)   // with alpha

// Named colors
white, black, red, blue, green, yellow,
gray, transparent, currentColor, inherit

Using Colors

.color(hex("#1e293b"))
.backgroundColor(rgba(255, 255, 255, 0.9))
.borderColor(hex("#e2e8f0"))

// Gradients
.background(linearGradient(deg(135),
    hex("#667eea"), hex("#764ba2")))

.background(radialGradient(circle,
    hex("#fff"), hex("#f0f0f0")))

Creating a Color Palette

public final class Colors {
    public static final CSSValue PRIMARY = hex("#6366f1");
    public static final CSSValue PRIMARY_DARK = hex("#4f46e5");
    public static final CSSValue SECONDARY = hex("#ec4899");
    public static final CSSValue SUCCESS = hex("#10b981");
    public static final CSSValue WARNING = hex("#f59e0b");
    public static final CSSValue ERROR = hex("#ef4444");
    public static final CSSValue TEXT = hex("#1e293b");
    public static final CSSValue TEXT_LIGHT = hex("#64748b");
    public static final CSSValue BG = hex("#ffffff");
    public static final CSSValue BG_MUTED = hex("#f8fafc");
}

Box Model

Control dimensions, spacing, and borders.

// Dimensions
.width(px(300))
.height(px(200))
.minWidth(px(200))
.maxWidth(px(800))
.minHeight(px(100))
.maxHeight(px(600))

// Size shorthand
.size(px(100))                  // width & height
.size(px(200), px(100))         // width, height

Margin

External spacing around elements.

// All sides
.margin(rem(1))

// Vertical, horizontal
.margin(rem(1), rem(2))

// Top, horizontal, bottom
.margin(rem(1), rem(2), rem(1))

// Top, right, bottom, left
.margin(rem(1), rem(2), rem(1), rem(2))

// Individual sides
.marginTop(rem(1))
.marginRight(rem(2))
.marginBottom(rem(1))
.marginLeft(rem(2))

// Horizontal/vertical shortcuts
.marginX(rem(2))                // left & right
.marginY(rem(1))                // top & bottom

// Centering
.margin(zero, auto)             // Horizontal center

Padding

Internal spacing inside elements.

// All sides
.padding(rem(1))

// Vertical, horizontal
.padding(rem(1), rem(2))

// Individual sides
.paddingTop(rem(1))
.paddingRight(rem(2))
.paddingBottom(rem(1))
.paddingLeft(rem(2))

// Horizontal/vertical shortcuts
.paddingX(rem(2))               // left & right
.paddingY(rem(1))               // top & bottom

Border

Element borders with various styles.

// Full border
.border(px(1), solid, gray)

// Individual properties
.borderWidth(px(2))
.borderStyle(solid)
.borderColor(blue)

// Individual sides
.borderTop(px(1), solid, black)
.borderBottom(px(2), dashed, gray)
.borderLeft(px(1), solid, black)
.borderRight(px(1), solid, black)

// Border styles
solid, dashed, dotted, double_, none

// Border radius
.borderRadius(px(8))
.borderRadius(px(8), px(4), px(8), px(4))  // corners
.borderTopLeftRadius(px(16))
.borderTopRightRadius(px(16))

// Presets
.roundedNone()      // 0
.roundedSm()        // 4px
.roundedMd()        // 6px
.roundedLg()        // 8px
.roundedXl()        // 12px
.roundedFull()      // 9999px (pill/circle)

Box Sizing

Control how dimensions are calculated.

// Include padding/border in width/height
.boxSizing(borderBox)

// Exclude padding/border (default)
.boxSizing(contentBox)

// Best practice: apply to all elements
rule(Selector.any()).boxSizing(borderBox)

Display

Control element rendering.

.display(block)
.display(inline)
.display(inlineBlock)
.display(flex)
.display(grid)
.display(none)

// Visibility (keeps space)
.visibility(visible)
.visibility(hidden)

// Overflow
.overflow(hidden)
.overflow(auto)
.overflow(scroll)
.overflowX(auto)
.overflowY(scroll)

Flexbox

Flexible box layout for one-dimensional layouts.

// Basic flex container
.display(flex)
.flexDirection(row)      // row, column, rowReverse, columnReverse
.justifyContent(center)  // start, end, center, spaceBetween, spaceAround
.alignItems(center)      // start, end, center, stretch, baseline
.gap(rem(1))

Common Flex Patterns

// Center content horizontally and vertically
Style centered = style()
    .display(flex)
    .justifyContent(center)
    .alignItems(center)
    .minHeight(vh(100));

// Horizontal navigation
Style nav = style()
    .display(flex)
    .gap(rem(2))
    .alignItems(center);

// Space between (logo left, nav right)
Style header = style()
    .display(flex)
    .justifyContent(spaceBetween)
    .alignItems(center)
    .padding(rem(1), rem(2));

// Vertical stack
Style stack = style()
    .display(flex)
    .flexDirection(column)
    .gap(rem(1));

Flex Item Properties

// Flex item sizing
.flexGrow(1)       // grow to fill space
.flexShrink(0)     // don't shrink
.flexBasis(px(200)) // initial size
.flex(1)           // shorthand: grow shrink basis

// Self alignment
.alignSelf(flexEnd)

CSS Grid

Two-dimensional grid layout system.

// Basic grid
.display(grid)
.gridTemplateColumns(repeat(3, fr(1)))  // 3 equal columns
.gridTemplateRows(auto)
.gap(rem(2))

Grid Column Patterns

// Fixed columns
.gridTemplateColumns(px(200), px(200), px(200))

// Flexible columns
.gridTemplateColumns(fr(1), fr(2), fr(1))  // 1:2:1 ratio

// Auto-fit responsive grid
.gridTemplateColumns(repeat(autoFit(), minmax(px(250), fr(1))))

// Mixed
.gridTemplateColumns(px(250), fr(1), px(300))  // sidebar, content, aside

Grid Areas

// Define areas
.gridTemplateAreas(
    "header header header",
    "sidebar content aside",
    "footer footer footer"
)
.gridTemplateRows(auto, fr(1), auto)
.gridTemplateColumns(px(200), fr(1), px(200))

// Place items in areas
.gridArea("header")
.gridArea("sidebar")
.gridArea("content")

Grid Item Placement

// Span columns/rows
.gridColumn("span 2")
.gridRow("span 3")

// Explicit placement (start line, end line)
.gridColumn(1, 3)
.gridRow(1, 2)

Typography

Font and text styling properties.

// Font family
.fontFamily("Inter, system-ui, sans-serif")
.fontFamily("'Fira Code', monospace")

// Font size
.fontSize(rem(1.125))
.fontSize(px(18))

// Font weight
.fontWeight(400)     // normal
.fontWeight(600)     // semi-bold
.fontWeight(700)     // bold

// Line height
.lineHeight(1.6)     // unitless multiplier
.lineHeight(px(24))  // explicit value

// Letter spacing
.letterSpacing(px(-0.5))
.letterSpacing(em(0.05))

Text Properties

// Text alignment
.textAlign(center)   // left, center, right, justify

// Text decoration
.textDecoration(none)
.textDecoration(underline)

// Text transform
.textTransform(uppercase)
.textTransform(capitalize)

// Text overflow — truncate() preset does all three
.truncate()  // overflow hidden + ellipsis + nowrap

// Or individually
.overflow(hidden)
.textOverflow("ellipsis")
.whiteSpace(nowrap)

// Word wrapping
.wordBreak(breakWord)
.overflowWrap(breakWord)

Font Presets

public final class Typography {
    public static final CSSValue TEXT_XS = rem(0.75);
    public static final CSSValue TEXT_SM = rem(0.875);
    public static final CSSValue TEXT_BASE = rem(1);
    public static final CSSValue TEXT_LG = rem(1.125);
    public static final CSSValue TEXT_XL = rem(1.25);
    public static final CSSValue TEXT_2XL = rem(1.5);
    public static final CSSValue TEXT_3XL = rem(1.875);
}

Visual Effects

Shadows, filters, transforms, and transitions.

Box Shadow

// Basic shadow
.boxShadow(px(0), px(4), px(6), rgba(0, 0, 0, 0.1))
//         x-offset, y-offset, blur, color

// With spread
.boxShadow(px(0), px(4), px(6), px(2), rgba(0, 0, 0, 0.1))
//         x, y, blur, spread, color

// String syntax
.boxShadow("0 4px 6px rgba(0,0,0,0.1)")

// Presets
.shadowXs()         // Subtle shadow
.shadowSm()         // Small shadow
.shadow()           // Default shadow
.shadowMd()         // Medium shadow
.shadowLg()         // Large shadow
.shadowXl()         // Extra large shadow
.shadowInner()      // Inset shadow
.shadowNone()       // No shadow

Transforms

2D and 3D transformations.

// Translate (move)
.transform(translateX(px(10)))
.transform(translateY(px(20)))
.transform(translate(px(10), px(20)))

// Rotate
.transform(rotate(deg(45)))
.transform(rotate(turn(0.25)))  // Quarter turn

// Scale
.transform(scale(1.2))          // Uniform
.transform(scale(2, 0.5))       // Non-uniform
.transform(scaleX(1.5))
.transform(scaleY(0.8))

// Skew
.transform(skewX(deg(10)))
.transform(skewY(deg(10)))

// Multiple transforms
.transform(translateY(px(-4)), scale(1.05))

// Transform origin
.transformOrigin("center center")
.transformOrigin("top left")

Transitions

Animate property changes.

// Simple transition
.transition(all, s(0.3), ease)
//          property, duration, timing

// Specific property
.transition(propColor, s(0.2))
.transition(propBackground, s(0.3), easeInOut)
.transition(propTransform, s(0.2), easeOut)

// Multiple properties
.transition(transitions(
    trans(propColor, s(0.2)),
    trans(propBackground, s(0.3)),
    trans(propTransform, s(0.2))
))

// Individual settings
.transitionProperty(all)
.transitionDuration(s(0.3))
.transitionTimingFunction(easeInOut)
.transitionDelay(ms(100))

// Timing functions
ease, linear, easeIn, easeOut, easeInOut

// Custom bezier
.transitionTimingFunction(cubicBezier(0.4, 0, 0.2, 1))

Filters

Image and element filters.

// Blur
.filter(blur(px(5)))

// Brightness (1 = normal)
.filter(brightness(1.2))

// Contrast
.filter(contrast(1.5))

// Grayscale (0 to 1)
.filter(grayscale(1))

// Opacity filter (0 to 1)
.filter(filterOpacity(0.5))

// Saturate (1 = normal)
.filter(saturate(2))

// Sepia (0 to 1)
.filter(sepia(1))

// Drop shadow (for transparent images)
.filter(dropShadow(px(2), px(4), px(6), rgba(0, 0, 0, 0.3)))

// Multiple filters
.filter("blur(5px) brightness(1.1)")

Backdrop Filter

Filter the area behind an element (for glass effects).

// Frosted glass effect
.backgroundColor(rgba(255, 255, 255, 0.7))
.backdropFilter(blur(px(10)))

// Glass card
style()
    .backgroundColor(rgba(255, 255, 255, 0.2))
    .backdropFilter(blur(px(20)))
    .borderRadius(px(16))
    .border(px(1), solid, rgba(255, 255, 255, 0.3))

Opacity

.opacity(1)         // Fully visible
.opacity(0.5)       // Half transparent
.opacity(0)         // Invisible

Cursor & Interaction

// Cursor styles
.cursor(pointer)    // Hand cursor (clickable)
.cursor(notAllowed) // Disabled cursor
.cursor(grab)       // Grab cursor
.cursor(text)       // Text selection cursor

// Disable interactions
.pointerEvents(none)  // Click-through
.userSelect(none)     // No text selection

Media Queries

Create responsive designs that adapt to screen sizes. A breakpoint can hang off an inline style with at(), or carry its own rules in a stylesheet.

CSSValue SP_4 = rem(1);
CSSValue SP_8 = rem(2);
CSSValue GUTTER = rem(1.5);
List<Element> cards = List.of();
Element content = div();

// On an inline style — the breakpoint gets a generated class,
// the plain declarations stay inline
h1(style()
        .fontSize(rem(1.9))
        .at(md(), style().fontSize(rem(2.5)))
        .at(xl(), style().fontSize(rem(3))),
    "Java Web Framework")

// Often no breakpoint is needed at all
h1(style().fontSize(clamp(rem(1.9), vw(6.5), rem(2.5))), "Java Web Framework")
div(autoGrid(px(240), SP_4), cards)      // as many columns as fit
div(container(px(1200)).padding(SP_8, GUTTER), content)
Cls CONTAINER = cls("container");
Cls SIDEBAR = cls("sidebar");

// Basic breakpoint
media().minWidth(px(768))
    .rule(CONTAINER, style().maxWidth(px(720)))

// Combined conditions
media().screen().minWidth(px(1024)).maxWidth(px(1280))
    .rule(SIDEBAR, style().display(none))

// Common breakpoints (presets)
xs()        // max-width: 575px
sm()        // min-width: 576px
md()        // min-width: 768px
lg()        // min-width: 992px
xl()        // min-width: 1200px
xxl()       // min-width: 1400px

// One breakpoint set only. The mobile()/tablet()/desktop() presets are
// deprecated: they overlapped xs()-xxl() with different pixel values, so
// which set a page used changed where it broke.

// Anything else: state the query
media().maxWidth(px(900))
media().minWidth(px(600)).maxWidth(px(1200))

Responsive Example

import static jweb.Css.*;   // brings in stylesheet(), media(), style()

Cls GRID = cls("grid");

String css = stylesheet()
    // Base styles (mobile-first)
    .rule(GRID, style()
        .display(grid)
        .gridTemplateColumns(fr(1))
        .gap(rem(1)))

    // Tablet and up
    .add(media().minWidth(px(768))
        .rule(GRID, style().gridTemplateColumns(fr(1), fr(1))))

    // Desktop
    .add(media().minWidth(px(1024))
        .rule(GRID, style().gridTemplateColumns(fr(1), fr(1), fr(1))))
    .build();

Dark Mode

Support dark color scheme.

// Detect system preference
media().prefersDark()
    .rule(Selector.of(":root"), style()
        .var("bg-color", hex("#1a1a1a"))
        .var("text-color", hex("#f5f5f5")))
    .rule(Selector.type("body"), style()
        .backgroundColor(var("bg-color"))
        .color(var("text-color")))

// lightDark() function
.backgroundColor(lightDark(white, hex("#1a1a1a")))
.color(lightDark(black, white))

Accessibility Queries

// Reduced motion (accessibility)
media().prefersReducedMotion()
    .rule(Selector.any(), style()
        .animationDuration(ms(0))
        .transitionDuration(ms(0)))

// High contrast
media().prefersContrast("more")
    .rule(Selector.any(), style().borderWidth(px(2)))

Orientation & Features

Cls CARD = cls("card");
Cls LOGO = cls("logo");
Cls NO_PRINT = cls("no-print");

// Portrait/landscape
media().portrait().rule(Selector.type("body"), style().fontSize(rem(0.95)))
media().landscape().rule(Selector.type("body"), style().fontSize(rem(1)))

// Hover capability
media().hover()
    .rule(CARD.hover(), style().transform(translateY(px(-4))))

media().noHover()
    .rule(CARD, style().boxShadow("none"))  // touch devices

// Retina displays
media().retina()
    .rule(LOGO, style().backgroundImage(url("/logo@2x.png")))

// Print styles
media().print()
    .rule(NO_PRINT, style().display(none))
    .rule(Selector.type("body"), style().fontSize(px(12)))

Container Queries

Style based on container size, not viewport.

Cls CARD_CONTAINER = cls("card-container");
Cls CARD = cls("card");

// Define container
rule(CARD_CONTAINER)
    .containerType(inlineSize)
    .containerName("card")

// Query the container
ContainerQuery.container("card").minWidth(px(400))
    .rule(CARD, style()
        .display(grid)
        .gridTemplateColumns(fr(1), fr(2)))

// Inline container query in style
style()
    .containerType(inlineSize)
    // Children can use @container queries

Keyframe Animations

Create custom CSS animations.

import static jweb.Css.*;

// Define keyframes
String fadeInKeyframes = keyframes("fadeIn")
    .from(style().opacity(0))
    .to(style().opacity(1))
    .build();

// Multi-step animation
String bounceKeyframes = keyframes("bounce")
    .at(0, style().transform(translateY(zero)))
    .at(50, style().transform(translateY(px(-20))))
    .at(100, style().transform(translateY(zero)))
    .build();

Using Animations

// Apply animation to element
.animation("fadeIn", s(1), ease)
//          name, duration, timing

// With all options
.animationName("bounce")
.animationDuration(s(2))
.animationTimingFunction(easeInOut)
.animationDelay(ms(500))
.animationIterationCount(infinite)
.animationDirection(directionAlternate)
.animationFillMode(fillModeForwards)

// Shorthand
.animation("fadeIn 0.5s ease-out forwards")

Pre-built Animations

Eleven ready-to-use animations. Each one has matching @keyframes in Keyframes, so it animates as soon as you add that stylesheet.

// Fade
fadeIn(s(0.5))
fadeOut(s(0.3))

// Slide
slideInLeft(s(0.5))
slideInRight(s(0.5))

// Scale
zoomIn(s(0.5))
zoomOut(s(0.5))
pulse(s(1.5))

// Bounce / rotate
bounce(s(1))
spin(s(2))

// Attention
shake(s(0.5))

// Add the matching keyframes to your stylesheet:
stylesheet().add(Keyframes.fadeIn()).add(Keyframes.spin())

// For anything else, write the keyframes and name them:
keyframes("wobble")
    .from(style().transform("rotate(-3deg)"))
    .to(style().transform("rotate(3deg)"))

Animation Usage

// Style with pre-built animation
style()
    .prop("animation", fadeIn(s(0.5)))

// With modifiers
style()
    .prop("animation",
        slideInLeft(s(0.6))
            .delay(ms(200))
            .timing(easeOut)
    )

// Infinite animation
style()
    .prop("animation",
        pulse(s(1.5))
            .iterationCount(infinite)
    )

// Multiple animations at once
style()
    .animationName(composeAnimations("fadeIn", "bounce"))
    .animationDuration(s(0.5))

Timing Functions

// Standard easing — the CSS keyword names
ease                 // Default easing
linear               // Constant speed
easeIn               // Slow start
easeOut              // Slow end
easeInOut            // Slow start and end

// Custom bezier curve
cubicBezier(0.4, 0, 0.2, 1)

// Steps (frame-by-frame)
steps(5, "end")
stepStart
stepEnd

Animation State

// Pause/play
.animationPlayState(paused)
.animationPlayState(running)

// Fill mode (final state)
.animationFillMode(fillModeForwards)  // Keep end state
.animationFillMode(fillModeBackwards) // Apply start before delay
.animationFillMode(fillModeBoth)      // Both

// Direction
.animationDirection(directionNormal)
.animationDirection(directionReverse)
.animationDirection(directionAlternate)
Use prefers-reduced-motion media query to disable animations for users who prefer reduced motion.

CSS Variables

Use CSS custom properties for theming and design tokens.

import static jweb.Css.*;

// Define variables in :root
rule(Selector.of(":root"))
    .var("primary-color", hex("#6366f1"))
    .var("secondary-color", hex("#8b5cf6"))
    .var("text-color", hex("#1f2937"))
    .var("bg-color", white)
    .var("spacing-sm", rem(0.5))
    .var("spacing-md", rem(1))
    .var("spacing-lg", rem(2))
    .var("radius", px(8))

Using Variables

// Reference variable
.color(var("primary-color"))
.padding(var("spacing-md"))
.borderRadius(var("radius"))

// With fallback value
.color(var("accent-color", blue))

// Chained fallback
.color(varChain("custom-color", "primary-color", blue))

Component Example

Cls BTN = cls("btn");
Cls CARD = cls("card");

// Button using variables
rule(BTN)
    .padding(var("spacing-sm"), var("spacing-md"))
    .backgroundColor(var("primary-color"))
    .color(white)
    .borderRadius(var("radius"))
    .transition(propBackground, s(0.2))

rule(BTN.hover())
    .backgroundColor(var("secondary-color"))

// Card using variables
rule(CARD)
    .padding(var("spacing-lg"))
    .backgroundColor(var("bg-color"))
    .color(var("text-color"))
    .borderRadius(var("radius"))

Dark Mode with Variables

Theme.dark() does this for you — the block below is what it emits. Write it by hand only when the tokens are not a design system.

Cls CARD = cls("card");

// Light theme (default)
rule(Selector.of(":root"))
    .var("bg-color", white)
    .var("text-color", hex("#1f2937"))
    .var("border-color", hex("#e5e7eb"))

// Dark theme
media().prefersDark().rule(Selector.of(":root"), style()
    .var("bg-color", hex("#1f2937"))
    .var("text-color", hex("#f9fafb"))
    .var("border-color", hex("#374151")))

// Components automatically adapt
rule(Selector.type("body"))
    .backgroundColor(var("bg-color"))
    .color(var("text-color"))

rule(CARD)
    .border(px(1), solid, var("border-color"))

One token builder

designSystem(), theme() and the scoped()/component() string helpers are gone in 3.0 — they were four naming schemes for the same job. jweb.css.Theme is the one that stayed; see Design tokens above.

// Before (3 competing builders) — designSystem(), theme() and the scoped()/component() helpers are gone
// String tokens = designSystem().spacing(rem(0.5), rem(1)).colors("primary", blue).build();
// String themes = theme().light("bg", white).dark("bg", black).buildBoth();

// After
Theme TOKENS = Theme.light()
    .space("2", rem(0.5)).space("4", rem(1))
    .color("primary", blue).color("bg", white)
    .dark(Theme.dark().color("bg", black));

stylesheet().add(TOKENS);                       // emit
style().color(Theme.color("primary"));          // read
CSS variables cascade and can be overridden at any level. Define global variables in :root and override in component classes as needed.

Modern CSS Features

Anchor positioning, scroll snap, text wrapping, subgrid, masking, and logical properties — each as typed builders.

Anchor Positioning

Position elements relative to an anchor element without JavaScript.

import static jweb.Css.*;

Cls TRIGGER = cls("trigger");
Cls TOOLTIP = cls("tooltip");
Cls POPUP = cls("popup");
Cls DROPDOWN = cls("dropdown");
Cls TIP = cls("tip");
Cls SUB = cls("sub");

// Define an anchor
rule(TRIGGER).apply(anchorName("--tooltip-anchor"))

// Position relative to anchor using position-area
rule(TOOLTIP)
    .apply(positionAnchor("--tooltip-anchor"))
    .apply(positionArea("top"))
    .position(absolute)

// Using anchor() function for fine-grained control
rule(POPUP)
    .top(anchor("--btn", "bottom"))
    .left(anchor("--btn", "left"))

// Fallback positioning (try top, then bottom)
rule(DROPDOWN)
    .apply(positionAnchor("--menu"))
    .apply(positionTryFallbacks("flip-block", "flip-inline"))

// Convenience: position above/below/left/right
rule(TIP)
    .apply(positionAnchor("--target"))
    .apply(positionAbove())    // shorthand for positionArea("top")
rule(SUB)
    .apply(positionAnchor("--target"))
    .apply(positionBelow())    // shorthand for positionArea("bottom")

Scroll Snap

Control scroll behavior to snap to specific elements.

import static jweb.Css.*;

Cls CAROUSEL = cls("carousel");
Cls CAROUSEL_ITEM = cls("carousel-item");
Cls PAGE_CONTAINER = cls("page-container");
Cls PAGE_SECTION = cls("page-section");
Cls PANEL = cls("panel");

// Horizontal carousel
rule(CAROUSEL)
    .apply(snapTypeX("mandatory"))    // snap on X axis
    .apply(snapPadding("0 20px"))     // padding at snap edges
    .overflowX(auto)

rule(CAROUSEL_ITEM)
    .apply(snapAlignCenter())         // snap to center
    .apply(snapStopAlways())          // always stop at each item

// Vertical page snap
rule(PAGE_CONTAINER)
    .apply(snapTypeY("proximity"))    // snap when close
    .overflowY(scroll)
    .height(vh(100))

rule(PAGE_SECTION)
    .apply(snapAlignStart())
    .height(vh(100))

// Overscroll behavior
rule(PANEL)
    .apply(overscrollBehaviorY("contain"))  // prevent parent scroll

Text Wrapping

Modern text wrapping and truncation controls.

import static jweb.Css.*;

Cls PREVIEW = cls("preview");
Cls NOWRAP = cls("nowrap");
Cls LONG_URLS = cls("long-urls");
Cls CJK = cls("cjk");
Cls OVERFLOW = cls("overflow");
Cls ARTICLE = cls("article");
Cls ELLIPSIS = cls("ellipsis");

// Balanced line lengths (great for headings)
rule(Selector.type("h1").or(Selector.type("h2"))).apply(textWrapBalance())

// Better orphan/widow handling (for body text)
rule(Selector.type("p")).apply(textWrapPretty())

// Truncate to N lines with ellipsis
rule(PREVIEW).apply(lineClamp(3))
// Shows max 3 lines, then "..."

// Prevent wrapping
rule(NOWRAP).apply(textWrapNowrap())

// Word breaking
rule(LONG_URLS).apply(wordBreakAll())        // break anywhere
rule(CJK).apply(wordBreakKeepAll())          // keep CJK together
rule(OVERFLOW).apply(overflowWrapBreakWord()) // break on overflow

// Hyphenation
rule(ARTICLE).apply(hyphensAuto())

// Text overflow
rule(ELLIPSIS).apply(textOverflowEllipsis())

Subgrid

Inherit parent grid tracks in nested grids.

import static jweb.Css.*;

Cls CARD_GRID = cls("card-grid");
Cls CARD = cls("card");
Cls FULL_CHILD = cls("full-child");
Cls NAMED_CHILD = cls("named-child");
Cls SPAN_ALL = cls("span-all");

// Parent grid
rule(CARD_GRID)
    .display(grid)
    .gridTemplateColumns("repeat(3, 1fr)")
    .gap(rem(1))

// Child inherits parent columns
rule(CARD)
    .display(grid)
    .apply(subgridColumns())    // grid-template-columns: subgrid

// Inherit both axes
rule(FULL_CHILD)
    .display(grid)
    .apply(subgridBoth())       // columns and rows from parent

// Named line references
rule(NAMED_CHILD)
    .display(grid)
    .apply(subgridColumnsNamed("[start] [end]"))

// Grid placement helpers
rule(SPAN_ALL)
    .apply(gridColumnFull())    // grid-column: 1 / -1

Masking and Clipping

Apply masks and clip paths for creative shapes.

import static jweb.Css.*;

Cls MASKED = cls("masked");
Cls AVATAR = cls("avatar");
Cls OVAL = cls("oval");
Cls ARROW = cls("arrow");
Cls DIAMOND = cls("diamond");
Cls PENTAGON = cls("pentagon");
Cls HEXAGON = cls("hexagon");
Cls STAR = cls("star");
Cls TRI_UP = cls("tri-up");
Cls TRI_DOWN = cls("tri-down");
Cls CUSTOM = cls("custom");

// Mask with image
rule(MASKED).apply(maskImage("url(mask.svg)"))
               .apply(maskMode("alpha"))

// Clip to circle
rule(AVATAR).apply(clipCircle("50%"))

// Clip to ellipse
rule(OVAL).apply(clipEllipse("50%", "40%"))

// Clip to polygon
rule(ARROW).apply(clipPolygon(
    "50% 0%", "100% 100%", "0% 100%"))

// Preset shapes
rule(DIAMOND).apply(clipDiamond())
rule(PENTAGON).apply(clipPentagon())
rule(HEXAGON).apply(clipHexagon())
rule(STAR).apply(clipStar())
rule(TRI_UP).apply(clipTriangleUp())
rule(TRI_DOWN).apply(clipTriangleDown())

// SVG path clipping
rule(CUSTOM).apply(clipSvgPath("M0,0 L100,0 L50,100 Z"))

Logical Properties

Direction-aware properties that adapt to RTL/LTR and writing modes.

import static jweb.Css.*;

Cls BOX = cls("box");
Cls CENTERED = cls("centered");
Cls INDENT = cls("indent");
Cls CARD = cls("card");
Cls HIGHLIGHTED = cls("highlighted");
Cls OVERLAY = cls("overlay");
Cls START_ALIGNED = cls("start-aligned");
Cls END_ALIGNED = cls("end-aligned");

// Sizing (replaces width/height)
rule(BOX)
    .apply(inlineSize("300px"))       // width in LTR
    .apply(blockSize("200px"))        // height in LTR
    .apply(maxInlineSize("100%"))

// Margin (replaces margin-left/right/top/bottom)
rule(CENTERED)
    .apply(marginInline("auto"))       // horizontal center
    .apply(marginBlock("1rem"))        // vertical margin

// Start/end for asymmetric spacing
rule(INDENT)
    .apply(marginInlineStart("2rem"))  // left in LTR, right in RTL
    .apply(paddingInlineEnd("1rem"))

// Padding
rule(CARD)
    .apply(paddingInline("1.5rem"))    // horizontal padding
    .apply(paddingBlock("1rem"))       // vertical padding

// Borders
rule(HIGHLIGHTED)
    .apply(borderInlineStart("3px solid blue"))  // left border in LTR
    .apply(borderBlock("1px solid gray"))

// Inset (replaces top/right/bottom/left)
rule(OVERLAY)
    .apply(insetInline("0"))           // left: 0; right: 0
    .apply(insetBlock("0"))            // top: 0; bottom: 0

// Text alignment
rule(START_ALIGNED).apply(textAlignStart())  // left in LTR
rule(END_ALIGNED).apply(textAlignEnd())      // right in LTR
Anchor Positioning and Subgrid are newer CSS features. Check browser support on caniuse.com. Logical properties are well-supported and recommended for all new projects to enable RTL support.

Newer properties

Properties the DSL was missing — each one is a typed method now, not a prop("name", "value") string.

// An input that grows with what is typed into it
input(type("text"), style().fieldSizing(content))

// Scrollbars
style().scrollbarColor(hex("#94a3b8"), transparent)
style().scrollbarWidth(thin)
style().scrollbarGutter(stable)              // reserve the space
style().scrollbarGutter(stableBothEdges)     // …on both sides

// Trim the font's half-leading so a heading's box hugs its glyphs
style().textBoxTrim(trimBoth).textBoxEdge(capAlphabetic)
style().textBox(trimBoth, capAlphabetic)     // the shorthand

// Animate to and from auto / min-content / fit-content
stylesheet().rule(Selector.of(":root"), style().interpolateSize(allowKeywords))

// Anchor positioning: confine anchor names to this subtree, so a repeated
// component resolves to its own anchor instead of the first in the document
style().anchorScope("all")

// One view-transition rule for every card, instead of a name per element
style().viewTransitionClass("card")

// One-value border sides (the three-value form is still there)
style().borderRight(none)

@starting-style

The values a transition animates from the first time an element is rendered — what makes a popover or a toast fade in rather than appear. It works on an inline style and on a stylesheet.

Cls TOAST = cls("toast");

// On an inline style
div(style()
        .opacity(1).transition(all, ms(200), ease)
        .startingStyle(style().opacity(0)),
    "I fade in")

// On a stylesheet
stylesheet()
    .rule(TOAST, style().opacity(1).transition(all, ms(200), ease))
    .startingStyle(TOAST, style().opacity(0))

Container style queries

A container query can also match on a custom property, not just a size — one variant flag on the container restyles everything in it.

import static jweb.css.ContainerQuery.*;

Cls TITLE = cls("title");
Cls BADGE = cls("badge");

container("card").style("variant", "featured")
    .rule(TITLE, style().fontWeight(700))
    .rule(BADGE, style().display(block))

// @container card style(--variant: featured) { … }

:popover-open and :open

// On the Selector builder — Selectors.cls/tag, since cls()/tag() are also El's
Selectors.cls("menu").popoverOpen()        // .menu:popover-open
Selectors.tag("details").open()            // details:open

// Or straight on an inline style
div(popover("auto"), style()
        .opacity(0)
        .popoverOpen(style().opacity(1))
        .startingStyle(style().opacity(0)),
    "menu")