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 tooInline 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 fromThey 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 alongFragments 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);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 rotationCSS 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, inheritUsing 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, heightMargin
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 centerPadding
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 & bottomBorder
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, asideGrid 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 shadowTransforms
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) // InvisibleCursor & 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 selectionMedia 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 queriesKeyframe 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
stepEndAnimation 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)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")); // readModern 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 scrollText 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 / -1Masking 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 LTRNewer 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")