HTML DSL
JWeb's HTML DSL provides Java methods for all HTML elements. Elements are composable, nestable, and fully type-checked at compile time.
Import Statement
import static jweb.El.*;This single import gives you access to all HTML elements, attributes, and helpers.
Basic Pattern
Every HTML element has a corresponding Java method. Pass children as arguments to create nested structures.
// Simple element with text
h1("Hello World")
// Nested elements
div(
h1("Title"),
p("Content here")
)
// With attributes
div(id("main"), class_("container"),
h1("Title"),
p("Content")
)
// With inline styles (bare style() builder)
div(class_("card"),
style()
.padding(rem(1))
.backgroundColor(white),
p("Styled content")
)Text Elements
Create headings, paragraphs, and inline text.
// Headings h1-h6
h1("Main Title")
h2("Section Title")
h3("Subsection")
h4("Sub-subsection")
h5("Minor heading")
h6("Smallest heading")
// Paragraphs and text — a bare String child is already an escaped text node
p("A paragraph of text")
span("Inline text")
raw("<b>trusted html</b>") // the one thing a String cannot say on its own
// Text formatting
strong("Bold/important text")
em("Emphasized/italic text")
small("Fine print")
code("Inline code")
pre("Preformatted text")Links
Create anchor links with href shorthand or full attributes.
// Simple link (href shorthand)
a(href("/home"), "Home")
a(href("/about"), "About Us")
// Link with attributes
a(href("/contact"), class_("nav-link"), "Contact")
// External link (opens in new tab securely)
a(attrs().href("https://example.com").targetBlank(), "External Site")
// Adds: target="_blank" rel="noopener noreferrer"
// Link with multiple children
a(href("/profile"),
img("/avatar.png", "Avatar"),
span("View Profile")
)Semantic Structure
Use semantic elements for accessible, well-structured pages.
// Page structure
body(
header(
h1("Site Title"),
nav(
a(href("/"), "Home"),
a(href("/about"), "About"),
a(href("/contact"), "Contact")
)
),
main(
article(
h2("Article Title"),
p("Article content...")
),
aside(
h3("Related Links"),
ul(li(a(href("/related"), "Related Item")))
)
),
footer(
p("© 2026 My Company"),
address(a(href("mailto:email@example.com"), "Contact Us"))
)
)
// Sectioning
section(
h2("Features"),
p("Our product features...")
)
// Search section (HTML5.2)
search(
form(action("/search"),
input(type("search"), name("q")),
button("Search")
)
)Document Elements
Build complete HTML documents with head and body.
html(
head(
title("My Page"),
meta(attrs().charset("UTF-8")),
meta(attrs().name("viewport").content("width=device-width, initial-scale=1")),
css("/styles/main.css"),
icon("/favicon.ico")
),
body(
h1("Welcome"),
p("Page content")
)
)
// Helper methods
css("/styles.css") // Stylesheet link
icon("/favicon.ico") // Favicon
script("/app.js") // External script
inlineScript("console.log('Hi');") // Inline JSContainer Elements
Structural elements for organizing content.
Element child1 = div("Child 1");
Element child2 = div("Child 2");
Element child3 = div("Child 3");
Element header = div("Header");
Element content = div("Content");
Element footer = div("Footer");
Element title = div("Title");
Element body = div("Body");
Element sidebar = div("Sidebar");
Element nav = div("Nav");
Element logo = div("Logo");
Element links = div("Links");
Element copyright = div("© 2026");
Element menuItems = div("Menu");
// Basic containers
div(child1, child2, child3)
section(header, content, footer)
article(title, body)
aside(sidebar)
// Semantic structure
header(nav, logo)
main(content)
footer(links, copyright)
nav(menuItems)Nesting Elements
Elements can be nested to any depth.
div(
header(
h1("Welcome"),
nav(
a(href("/"), "Home"),
a(href("/about"), "About")
)
),
main(
article(
h2("Article Title"),
p("Article content here...")
)
),
footer(p("Copyright 2025"))
)Lists
Unordered, ordered, and definition lists.
// Unordered list
ul(
li("First item"),
li("Second item"),
li("Third item")
)
// Ordered list
ol(
li("Step one"),
li("Step two"),
li("Step three")
)
// Definition list
dl(
dt("Term"),
dd("Definition of the term"),
dt("Another term"),
dd("Its definition")
)Dynamic Lists
Use each() to render lists from collections.
record User(String getName, String getEmail) {}
class UserService { List<User> findAll() { return List.of(new User("Ada", "ada@example.com")); } }
UserService userService = new UserService();
List<String> items = List.of("Apple", "Banana", "Cherry");
ul(each(items, item -> li(item)))
// With index
ul(each(IntStream.range(0, items.size()).boxed().toList(), i ->
li((i + 1) + ". " + items.get(i))
))
// Complex list items
List<User> users = userService.findAll();
ul(each(users, user ->
li(
strong(user.getName()),
" - ",
span(user.getEmail())
)
))Tables
Create data tables with headers, body, and footer.
table(
thead(
tr(th("Name"), th("Email"), th("Role"))
),
tbody(
tr(td("John"), td("john@test.com"), td("Admin")),
tr(td("Jane"), td("jane@test.com"), td("User"))
),
tfoot(
tr(td(attrs().colspan(3), "2 users total"))
)
)Dynamic Tables
Render table rows from data collections.
record User(long getId, String getName, String getEmail) {}
class UserService { List<User> findAll() { return List.of(new User(1, "Ada", "ada@example.com")); } }
UserService userService = new UserService();
List<User> users = userService.findAll();
table(
thead(tr(th("ID"), th("Name"), th("Email"), th("Actions"))),
tbody(each(users, user -> tr(
td(String.valueOf(user.getId())),
td(user.getName()),
td(user.getEmail()),
td(
button(class_("btn-edit"), "Edit"),
button(class_("btn-delete"), "Delete")
)
)))
)Sortable Table Headers
th(attrs()
.class_("sortable")
.data("sort", "name")
.set("onclick", "sortTable('name')"),
"Name ", span(class_("sort-icon")))Images
Display images with required alt text for accessibility.
// Basic image
img(src("/images/logo.png"), alt("Company Logo"))
// Responsive image
img(
src("/images/hero.jpg"),
alt("Hero image"),
class_("responsive"),
loading("lazy"))
// Figure with caption
figure(
img(src("/images/chart.png"), alt("Sales chart")),
figcaption("Q4 2024 Sales Performance")
)Links
Create anchor links for navigation.
// Internal link
a(href("/about"), "About Us")
// External link (opens in new tab)
a(attrs()
.href("https://github.com")
.target("_blank")
.rel("noopener noreferrer"),
"GitHub")
// Download link
a(attrs()
.href("/files/report.pdf")
.download("report.pdf"),
"Download Report")
// Email link
a(href("mailto:contact@example.com"), "Email Us")Form Elements
Build forms with type-safe input elements and attributes.
// Basic form
form(action("/submit"), method("POST"),
label(for_("email"), "Email:"),
input(
type("email"),
name("email"),
id("email"),
placeholder("you@example.com"),
required()
),
button(type("submit"), "Submit")
)Input Types
All HTML5 input types are supported.
String token = "csrf-token-abc123";
// Text inputs
input(type("text"), name("username"))
input(type("password"), name("pwd"))
input(type("email"), name("email"))
input(type("tel"), name("phone"))
input(type("url"), name("website"))
input(type("search"), name("q"))
// Number inputs
input(attrs().type("number").name("qty").min(1).max(100))
input(attrs().type("range").name("volume").min(0).max(100))
// Date/time inputs
input(type("date"), name("birthday"))
input(type("time"), name("appointment"))
input(type("datetime-local"), name("meeting"))
input(type("month"), name("expiry"))
input(type("week"), name("week"))
// Selection inputs
input(type("checkbox"), name("agree"), value("yes"))
input(type("radio"), name("color"), value("red"))
// File input
input(attrs().type("file").name("avatar").accept("image/*"))
// Hidden input
input(type("hidden"), name("csrf"), value(token))
// Color picker
input(type("color"), name("theme"), value("#6366f1"))The CSRF token
Middlewares.recommended() includes CSRF, so every router POST must carry the token. A record form adds the field itself; a hand-built form renders Csrf.tokenField(), and a page whose script POSTs by hand renders Csrf.tokenMeta() and sends the _csrf parameter read from it. Inside a render the helpers need no request.
form(attrs().action("/save").method("POST"),
Csrf.tokenField(),
input(type("text"), name("title")),
button(type("submit"), "Save"))Textarea
Multi-line text input.
// Basic textarea
textarea(attrs().name("message").rows(5).cols(40))
// With default content
textarea(name("bio"), placeholder("Tell us about yourself"),
"Default text here"
)
// With validation
textarea(attrs()
.name("comment")
.required()
.minlength(10)
.maxlength(500)
)Select & Options
Dropdown selects with options.
// Basic select
select(name("country"),
option(value(""), "Select a country"),
option(value("us"), "United States"),
option(value("uk"), "United Kingdom"),
option(value("ca"), "Canada")
)
// With selected option
select(name("size"),
option(value("sm"), "Small"),
option(attrs().value("md").selected(), "Medium"),
option(value("lg"), "Large")
)
// Multiple select
select(attrs().name("colors").multiple(),
option(value("red"), "Red"),
option(value("green"), "Green"),
option(value("blue"), "Blue")
)
// Optgroup
select(name("car"),
optgroup(attrs().label("Swedish Cars"),
option(value("volvo"), "Volvo"),
option(value("saab"), "Saab")
),
optgroup(attrs().label("German Cars"),
option(value("mercedes"), "Mercedes"),
option(value("audi"), "Audi")
)
)Buttons
Different button types for various actions.
// Submit button (default)
button("Submit")
button(type("submit"), "Save")
// Reset button
button(type("reset"), "Clear Form")
// Regular button (for JS actions)
button(type("button"), onClick(call("handleClick")), "Click Me")
// Disabled button
button(disabled(), "Cannot Click")
// Button with icon
button(class_("btn-icon"),
svg(...), // Icon SVG
span("Download")
)Form Validation Attributes
HTML5 validation with type-safe attributes.
input(attrs()
.type("text")
.name("username")
.required() // Must be filled
.minlength(3) // Minimum 3 characters
.maxlength(20) // Maximum 20 characters
.pattern("[a-zA-Z0-9]+") // Regex pattern
.autocomplete("username") // Browser autocomplete hint
)
input(
type("email"),
name("email"),
required(),
placeholder("you@example.com")
)
input(attrs()
.type("number")
.name("age")
.min(18) // Minimum value
.max(120) // Maximum value
.step(1) // Step increment
)Complete Form Example
A complete registration form with validation.
form(action("/register"), method("POST"), class_("form"),
// Username field
div(class_("field"),
label(for_("username"), "Username"),
input(attrs()
.type("text")
.id("username")
.name("username")
.required()
.minlength(3)
.maxlength(20)
.autocomplete("username")
)
),
// Email field
div(class_("field"),
label(for_("email"), "Email"),
input(attrs()
.type("email")
.id("email")
.name("email")
.required()
.autocomplete("email")
)
),
// Password field
div(class_("field"),
label(for_("password"), "Password"),
input(attrs()
.type("password")
.id("password")
.name("password")
.required()
.minlength(8)
.autocomplete("new-password")
)
),
// Terms checkbox
div(class_("field-checkbox"),
input(type("checkbox"), id("terms"), name("terms"), required()),
label(for_("terms"), "I agree to the terms")
),
// Submit
button(type("submit"), class_("btn-primary"), "Register")
)Modern HTML5 Elements
Modern interactive elements with built-in browser functionality.
Dialog (Modal)
Native modal dialogs with backdrop and close behavior, driven by Actions on jweb.El.
void handleResult(Event e) {}
// Define the dialog
dialog(id("confirm-dialog"),
h2("Confirm Action"),
p("Are you sure you want to proceed?"),
div(class_("dialog-actions"),
button(onClick(closeDialog("confirm-dialog")), "Cancel"),
button(onClick(closeDialog("confirm-dialog", "confirmed")), "Confirm")
)
)
// Open as modal (with backdrop)
button(onClick(openDialog("confirm-dialog")), "Open Modal")
// Toggle: opens a closed dialog, closes an open one
button(onClick(toggleDialog("confirm-dialog")), "Toggle")
// Read the return value after it closes
dialog(id("my-dialog"), onClose(e -> handleResult(e)),
div(class_("dialog-content"),
h2("Pick one"),
button(onClick(closeDialog("my-dialog", "yes")), "Yes")
)
)Details & Summary (Accordion)
Expandable content sections with native toggle behavior.
// Basic collapsible
details(
summary("Click to expand"),
p("Hidden content revealed when expanded")
)
// Open by default
details(attrs().open(),
summary("Already expanded"),
p("Visible content")
)
// FAQ Accordion (exclusive - using the name attribute, a native browser feature)
div(class_("faq"),
details(name("faq"), id("faq-1"),
summary("What is JWeb?"),
p("JWeb is a pure Java web framework...")
),
details(name("faq"),
summary("How do I get started?"),
p("Add the dependency and create a page...")
),
details(name("faq"),
summary("Is it production ready?"),
p("Yes, JWeb is production ready...")
)
)
// Control one with an Action
button(onClick(openDetails("faq-1")), "Expand First")
button(onClick(toggleDetails("faq-1")), "Toggle First")Progress Bar
Display progress of a task.
// Determinate progress (known completion)
progress(attrs().value(70).set("max", "100"))
// Shorthand
progress(70, 100) // 70% complete
// Indeterminate (unknown completion)
progress() // Animated, no value (indeterminate)
// Styled with label
div(class_("progress-container"),
label("Uploading..."),
progress(45, 100),
span("45%")
)Meter
Display a scalar measurement within a known range.
// Basic meter
meter(attrs()
.value(0.6)
.set("min", "0")
.set("max", "1")
)
// Shorthand
meter(0.6, 0, 1) // 60% of range
// With thresholds (browser applies colors)
meter(attrs()
.value(75)
.set("min", "0")
.set("max", "100")
.low(25) // Below this is "low"
.high(75) // Above this is "high"
.optimum(50) // Optimal value
)
// Examples
div(
label("Disk usage: "),
meter(0.8, 0, 1), // 80% - shown as warning (high)
span(" 80%")
)
div(
label("Battery: "),
meter(0.2, 0, 1), // 20% - shown as danger (low)
span(" 20%")
)Template & Slot (Web Components)
Define reusable HTML templates and content slots.
// Template (not rendered, used by JavaScript)
template(id("card-template"),
div(class_("card"),
h3(class_("card-title")),
p(class_("card-content")),
slot("actions") // Named slot
)
)
// Slot for web components
slot() // Default slot
slot("header") // Named slot
// Output (for calculation results)
form(attrs().set("oninput", "result.value = a.valueAsNumber + b.valueAsNumber"),
input(type("number"), name("a"), value("0")),
" + ",
input(type("number"), name("b"), value("0")),
" = ",
output(attrs().name("result").set("for", "a b"), "0")
)Time & Data
Semantic elements for machine-readable dates and values.
// Time with machine-readable datetime
time(datetime("2026-01-21"), "January 21, 2026")
// Renders: <time datetime="2026-01-21">January 21, 2026</time>
// Event time
p("The concert starts at ",
time(datetime("20:00"), "8 PM"),
".")
// Duration
p("Flight duration: ",
time(datetime("PT2H30M"), "2 hours 30 minutes")
)
// Data (machine-readable value)
tag("data", value("12345"), "Product Name")
// Renders: <data value="12345">Product Name</data>
// Useful for product IDs, prices, etc.
p("Price: ", tag("data", value("99.99"), "$99.99"))Text Direction
Control text direction for internationalization.
String username = "jane_doe";
// BDI - Bi-Directional Isolation
// Isolates text that might have different direction
p("User ", bdi(username), " posted this comment")
// Prevents RTL usernames from affecting surrounding text
// BDO - Bi-Directional Override
// Force specific direction
bdo(attrs().dir("rtl"), "This text is right-to-left")
bdo(attrs().dir("ltr"), "This text is left-to-right")Ruby Annotation
For East Asian typography - pronunciation guides above characters.
// Ruby annotation for Japanese/Chinese
ruby(
"漢",
rp("("),
rt("かん"), // Reading/pronunciation
rp(")")
)
// Multiple characters
ruby(
"東京",
rp("("),
rt("とうきょう"),
rp(")")
)
// Renders with pronunciation above the charactersEnhanced HTML Elements
Additional semantic elements for popovers, responsive images, definitions, text annotations, and forms.
Popover API
Native popover elements with auto-dismiss and backdrop behavior.
// The core attributes are on El; PopoverElements duplicates them under its
// own names (importing both is ambiguous), so its extra helpers — like
// popoverHideButton — are reached by qualifying the type instead.
import static jweb.El.*;
// Auto popover (dismisses on click outside or Escape)
button(popovertarget("my-popup"), "Toggle Menu"),
div(popover(), id("my-popup"),
ul(
li("Option 1"),
li("Option 2"),
li("Option 3")
)
)
// Manual popover (only dismisses programmatically)
button(popovertarget("info-pop"), popovertargetaction("show"), "Show Info"),
jweb.el.PopoverElements.popoverHideButton("info-pop", "Close"),
div(popover("manual"), id("info-pop"),
p("This stays open until explicitly closed.")
)
// Popover attributes on custom elements (Attr args)
div(popover(), id("custom-pop"),
p("Custom popover content")
)
button(popovertarget("custom-pop"),
"Toggle Custom"
)
// JavaScript control
// showPopover("my-popup")
// hidePopover("my-popup")
// togglePopover("my-popup")Responsive Images
Art direction and format selection for optimized image delivery.
// picture/source/srcset/type/loading are on El already; PictureElements
// duplicates them under its own names, so importing its wildcard alongside
// El is ambiguous — import just the attributes El doesn't have by name.
import static jweb.El.*;
import static jweb.el.PictureElements.media;
import static jweb.el.PictureElements.width;
import static jweb.el.PictureElements.height;
import static jweb.el.PictureElements.eagerLoad;
import static jweb.el.PictureElements.fetchpriority;
import static jweb.el.PictureElements.decoding;
// Art direction: different images per viewport
picture(
source(srcset("hero-wide.jpg"), media("(min-width: 1024px)")),
source(srcset("hero-medium.jpg"), media("(min-width: 640px)")),
img("hero-small.jpg", "Hero image")
)
// Format selection: modern formats with fallback
picture(
source(srcset("photo.avif"), type("image/avif")),
source(srcset("photo.webp"), type("image/webp")),
img("photo.jpg", "Photo")
)
// Responsive with density descriptors
img(src("photo.jpg"), alt("Photo"), srcset("photo-2x.jpg 2x"))
// Renders: <img src="photo.jpg" srcset="photo.jpg 1x,photo-2x.jpg 2x">
// Lazy-loaded image with CLS prevention
img(src("hero.jpg"), alt("Hero"), width(800), height(600), loading("lazy"))
// Renders: <img src="hero.jpg" loading="lazy" width="800" height="600">
// Loading attributes
loading("lazy") // loading="lazy"
eagerLoad() // loading="eager"
fetchpriority("high")
decoding("async")Figure and Caption
Semantic container for self-contained content with optional captions.
import static jweb.El.*;
// Image with caption
figure(
img("chart.png", "Sales chart"),
figcaption("Figure 1: Quarterly sales data")
)
// Code listing with caption
figure(class_("code-example"),
pre(code("const greeting = 'Hello World';")),
figcaption("Example: Variable declaration")
)
// Blockquote with attribution
figure(
blockquote("To be or not to be, that is the question."),
figcaption("William Shakespeare")
)Definition Lists
Semantic element for term-definition pairs, glossaries, and key-value data.
import static jweb.El.*;
// Glossary
dl(
dt("HTML"), dd("HyperText Markup Language"),
dt("CSS"), dd("Cascading Style Sheets"),
dt("JS"), dd("JavaScript")
)
// Metadata / key-value pairs
dl(class_("metadata"),
dt("Author"), dd("Jane Doe"),
dt("Published"), dd("2026-01-29"),
dt("Category"), dd("Technology")
)Interactive Text Elements
Semantic inline elements for abbreviations, citations, quotations, keyboard input, and text annotations.
import static jweb.El.*;
// Abbreviation with tooltip expansion
p("The ", abbr(attr("title", "HyperText Markup Language"), "HTML"), " spec")
// Hovering shows "HyperText Markup Language"
// Definition term
p("A ", dfn("closure"), " captures its lexical scope.")
// Citation (title of a work)
p("As described in ", cite("The Art of Programming"), "...")
// Inline quotation (auto-adds quotes)
p("She said, ", q("Hello World"), " and it ran.")
// Blockquote with source URL
blockquote(attrs().cite("https://example.com"),
p("Knowledge is power.")
)
// Keyboard input
p("Press ", kbd("Ctrl"), "+", kbd("C"), " to copy.")
// Sample output
p("The program outputs: ", samp("Hello World"))
// Variable
p("Let ", tag("var", "x"), " = 5")
// Highlighted text
p("Search results for: ", mark("JWeb framework"))
// Subscript / Superscript
p("H", sub("2"), "O") // H₂O
p("E = mc", sup("2")) // E = mc²
// Inserted / Deleted text
p(del("Old price: $50"), " ", ins("New price: $30"))
// Strikethrough (no longer accurate)
p(s("Available in stores"), " — Now online only!")Form Enhancements
Modern form elements for autocomplete, grouping, and specialized inputs.
import static jweb.El.*;
// Autocomplete with datalist
input(attrs().list("browsers").name("browser")),
datalist(id("browsers"),
option("Chrome"),
option("Firefox"),
option("Safari"),
option("Edge")
)
// Grouped options in select
select(name("car"),
optgroup(attrs().label("Swedish Cars"),
option(value("volvo"), "Volvo"),
option(value("saab"), "Saab")),
optgroup(attrs().label("German Cars"),
option(value("bmw"), "BMW"),
option(value("audi"), "Audi"))
)
// Fieldset with legend (groups related controls)
fieldset(
legend("Personal Information"),
label("Name:"),
input(type("text"), name("name")),
label("Email:"),
input(type("email"), name("email"))
)
// Specialized input types — a plain input() with the right type attribute
input(type("color"), name("theme-color"), value("#3b82f6")) // Color picker
input(type("date"), name("birthday")) // Date picker
input(type("date"), name("event"), attrs().min("2026-01-01").max("2026-12-31")) // With range
input(type("time"), name("meeting")) // Time picker
input(type("datetime-local"), name("appointment")) // Date + time
input(type("month"), name("start-month")) // Month picker
input(type("week"), name("sprint-week")) // Week picker
input(type("range"), name("volume"), attrs().min(0).max(100).value("50")) // Slider
input(type("range"), name("opacity"), attrs().min(0).max(100).value("50").step(5)) // Slider with stepSVG Elements
Create scalable vector graphics inline with type-safe methods.
// Every SVG element is on jweb.El — one import covers HTML and SVG alike.
import static jweb.El.*;
// jweb.El also carries the SVG attributes (x, y, cx, cy, r, width, height,
// stroke-*, ...) with int, double and String overloads. Don't also
// wildcard-import jweb.el.SVGElements — its element names collide with El's.Basic SVG
Create an SVG container with viewBox.
// SVG container
svg(viewBox("0 0 24 24"), width(24), height(24),
path(d("M12 2L2 7l10 5 10-5-10-5z"), fill("currentColor"))
)
// Alternative viewBox syntax
svg(viewBox(0, 0, 100, 100),
circle(cx(50), cy(50), r(40), fill("blue"))
)Shape Elements
Basic shapes for building graphics.
// Circle
svg(viewBox("0 0 100 100"),
circle(cx(50), cy(50), r(40),
fill("blue"),
stroke("black"),
strokeWidth(2)
)
)
// Rectangle
svg(viewBox("0 0 200 100"),
rect(x(10), y(10), width(180), height(80),
fill("red"),
stroke("black"),
strokeWidth(1)
)
)
// Rounded rectangle
svg(viewBox("0 0 200 100"),
rect(x(10), y(10), width(180), height(80),
rx(10), ry(10), // Corner radius
fill("green")
)
)
// Line
svg(viewBox("0 0 100 100"),
line(x1(10), y1(10), x2(90), y2(90),
stroke("black"),
strokeWidth(2)
)
)
// Polyline (connected lines, open)
svg(viewBox("0 0 100 100"),
polyline(points("10,90 50,10 90,90"),
fill("none"),
stroke("purple"),
strokeWidth(2)
)
)
// Polygon (connected lines, closed)
svg(viewBox("0 0 100 100"),
polygon(points("50,10 90,90 10,90"),
fill("yellow"),
stroke("black")
)
)Path Element
Complex shapes using path commands.
// Path with commands
svg(viewBox("0 0 24 24"),
path(
d("M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"),
fill("none"),
stroke("currentColor"),
strokeWidth(2),
strokeLinecap("round"),
strokeLinejoin("round")
)
)
// Path commands:
// M = moveto
// L = lineto
// H = horizontal lineto
// V = vertical lineto
// C = curveto (cubic bezier)
// S = smooth curveto
// Q = quadratic bezier
// T = smooth quadratic bezier
// A = arc
// Z = closepath
// Icon example (checkmark)
svg(viewBox("0 0 24 24"), width(24), height(24),
path(
d("M5 13l4 4L19 7"),
fill("none"),
stroke("green"),
strokeWidth(2)
)
)Grouping & Transforms
Group elements and apply transformations.
// Group elements
svg(viewBox("0 0 200 200"),
g(transform("translate(100, 100)"),
circle(cx(0), cy(0), r(50), fill("blue")),
circle(cx(30), cy(30), r(20), fill("red"))
)
)
// Multiple transforms (space-separated string)
g(transform("translate(50, 50) rotate(45) scale(1.5)"),
rect(x(-25), y(-25), width(50), height(50), fill("purple"))
)
// The transform attribute is a plain string — compose it by hand
"translate(100, 50)" // Move position
"rotate(45)" // Rotate degrees
"scale(1.5)" // Uniform scale
"scale(2, 0.5)" // Non-uniform scale
"skewX(10)" // Skew horizontally
"skewY(10)" // Skew verticallyDefinitions & Reuse
Define reusable elements and gradients. Gradients are svgLinearGradient / svgRadialGradient here — linearGradient and radialGradient are the CSS values.
svg(viewBox("0 0 200 200"),
// Define reusable elements
defs(
// Linear gradient
svgLinearGradient(id("gradient1"),
stop(offset("0%"), stopColor("blue")),
stop(offset("100%"), stopColor("purple"))
),
// Radial gradient
svgRadialGradient(id("glow"),
stop(offset("0%"), stopColor("white")),
stop(offset("100%"), stopColor("blue"))
),
// Reusable symbol
symbol(id("star"), viewBox("0 0 24 24"),
path(d("M12 2l3 7h7l-5.5 4.5L18 21l-6-4-6 4 1.5-7.5L2 9h7z"))
)
),
// Use gradient
rect(x(10), y(10), width(80), height(80),
fill("url(#gradient1)")
),
circle(cx(150), cy(50), r(30), fill("url(#glow)")),
// Reuse symbol
use(href("#star"), x(100), y(100), width(50), height(50))
)Text, Patterns & Filters
Text on the SVG canvas, tiled patterns, and filter effects. svgText is the SVG <text> element — plain text() is gone, and a bare String is still a text node.
// Text with a tspan run and text along a path
svg(viewBox("0 0 200 60"),
defs(
path(id("curve"), d("M10 50 Q100 10 190 50"))
),
svgText(x(10), y(20), fontFamily("system-ui"), fontSize(16),
"Plain, ", tspan(fill("red"), "colored"), " text"
),
svgText(
textPath(href("#curve"), "Text that follows a curve")
)
)
// Tiled pattern fill
svg(viewBox("0 0 100 100"),
defs(
pattern(id("dots"), width(10), height(10),
attr("patternUnits", "userSpaceOnUse"),
circle(cx(5), cy(5), r(2), fill("gray"))
)
),
rect(x(0), y(0), width(100), height(100), fill("url(#dots)"))
)
// Blur and drop-shadow filters
svg(viewBox("0 0 100 100"),
defs(
filter(id("blur"),
feGaussianBlur(attr("in", "SourceGraphic"), stdDeviation(3))),
filter(id("shadow"),
feDropShadow(dx(2), dy(2), stdDeviation(2)))
),
circle(cx(30), cy(50), r(20), fill("blue"), attr("filter", "url(#blur)")),
circle(cx(70), cy(50), r(20), fill("green"), attr("filter", "url(#shadow)"))
)
// Clip and mask
svg(viewBox("0 0 100 100"),
defs(
clipPath(id("circle-clip"), circle(cx(50), cy(50), r(30)))
),
image(href("/photo.jpg"), x(0), y(0), width(100), height(100),
attr("clip-path", "url(#circle-clip)"))
)Animation
Native SMIL animation: animate for values, animateTransform for transforms, animateMotion for movement along a path.
// Fade in
circle(cx(50), cy(50), r(20), fill("blue"),
animate(attr("attributeName", "opacity"), attr("from", "0"), attr("to", "1"), attr("dur", "1s"))
)
// Spin forever
rect(x(40), y(40), width(20), height(20), fill("red"),
animateTransform(attr("attributeName", "transform"), attr("type", "rotate"),
attr("from", "0 50 50"), attr("to", "360 50 50"), attr("dur", "2s"), attr("repeatCount", "indefinite"))
)
// Move along a path
svg(viewBox("0 0 200 100"),
circle(r(6), fill("purple"),
animateMotion(attr("path", "M10 90 Q100 10 190 90"),
attr("dur", "3s"), attr("repeatCount", "indefinite")))
)Icon Example
Create a complete icon.
// Reusable icon method
public static Element icon(String name) {
return switch (name) {
case "check" -> svg(viewBox("0 0 24 24"), width(24), height(24),
path(d("M5 13l4 4L19 7"),
fill("none"), stroke("currentColor"), strokeWidth(2))
);
case "x" -> svg(viewBox("0 0 24 24"), width(24), height(24),
path(d("M6 6l12 12M6 18L18 6"),
fill("none"), stroke("currentColor"), strokeWidth(2))
);
case "menu" -> svg(viewBox("0 0 24 24"), width(24), height(24),
path(d("M3 12h18M3 6h18M3 18h18"),
fill("none"), stroke("currentColor"), strokeWidth(2))
);
default -> fragment();
};
}
// Usage
button(class_("icon-btn"),
icon("check"),
span("Confirm")
)Attributes API
attrs() is the long tail — most attributes, event handlers, styles, refs and state bindings are plain element arguments; attrs() combines what's left (classIf, custom attributes, and other chains) into one fluent builder.
Basic Attributes
// Core attributes
div(attrs()
.id("main")
.class_("container")
.title("Tooltip text")
)
// Shorthand for common attributes
div(id("main")) // Just id
div(class_("container")) // Just class
a(href("/home"), "Home") // Just hrefMultiple Classes
Add classes individually, conditionally, or as a list.
// Add classes
boolean isActive = true;
boolean isDisabled = false;
boolean isOpen = false;
boolean expanded = true;
record Product(boolean isFeatured, int getStock) {}
Product product = new Product(true, 0);
attrs().class_("btn").addClass("primary").addClass("lg")
// Multiple classes at once
attrs().classes("btn", "primary", "lg")
// Conditional classes — the condition comes first, so it reads like an if
attrs()
.class_("btn")
.classIf(isActive, "active") // Add if true
.classIf(isDisabled, "disabled")
// Toggle between classes
attrs().classToggle(isOpen, "open", "closed")
// Complex conditional
attrs()
.class_("card")
.classIf(product.isFeatured(), "featured")
.classIf(product.getStock() == 0, "soldout")
.classToggle(expanded, "expanded", "collapsed")Inline Styles
Three ways to add inline styles.
// 1. A bare style() builder as an argument (recommended)
div(class_("card"),
style()
.display(flex)
.padding(rem(1))
.backgroundColor(white)
.borderRadius(px(8)),
content
)
// 2. Mixed with other attributes — no attrs() bridge or closing call needed
a(style().color(blue).textDecoration(none),
href("/home"),
class_("nav-link"),
"Home"
)
// 3. Pass a pre-built Style object
Style cardStyle = style()
.padding(rem(1.5))
.backgroundColor(white)
.borderRadius(px(8));
div(cardStyle, content)Layout Shortcuts
Quick flexbox and grid setup.
Element card1 = div("Card 1");
Element card2 = div("Card 2");
Element card3 = div("Card 3");
List<String> products = List.of("Widget", "Gadget", "Gizmo");
// Flexbox helpers
attrs().flexCenter() // Flex + center items
attrs().flexColumn("1rem") // Column with gap
attrs().flexRow("1rem") // Row with gap
attrs().flexBetween() // Space-between + center
// Grid helpers
attrs().gridCols(3, "1rem") // 3-column grid with gap
attrs().gridCols(4) // 4-column grid without gap
// Example
div(attrs().flexRow("1rem"),
card1,
card2,
card3
)
div(attrs().gridCols(3, "2rem"),
each(products, p -> div(p))
)Data & ARIA Attributes
Custom data attributes and accessibility.
// Data attributes
attrs()
.data("user-id", "123")
.data("role", "admin")
.data("active", "true")
// Renders: data-user-id="123" data-role="admin" data-active="true"
// ARIA attributes
attrs()
.aria("label", "Close dialog")
.aria("expanded", "true")
.aria("controls", "menu-panel")
// Renders: aria-label="Close dialog" aria-expanded="true" ...
// Role
role("button")
role("navigation")
role("dialog")
// Combining for accessible button — title has no shortcut, so it's the
// one thing attrs() still needs to carry
button(class_("icon-btn"), aria("label", "Close"), attrs().title("Close window"),
span("x")
)Boolean Attributes
Attributes that don't need values.
// Form attributes
input(attrs()
.disabled() // Cannot interact
.readonly() // Can't edit, can submit
.required() // Must fill before submit
.checked() // For checkboxes/radios
.autofocus() // Focus on page load
.multiple() // Allow multiple selections
)
// Media attributes
video(attrs()
.controls() // Show playback controls
.autoplay() // Start playing
.loop() // Loop playback
.muted() // Start muted
)
// Other
details(attrs().open()) // Expanded by default
script(attrs().defer()) // Defer loading
script(attrs().async()) // Async loading
input(attrs().checked()) // Pre-checked checkbox/radioLink & Navigation
Attributes for links and navigation.
// External link (secure)
a(attrs()
.href("https://external.com")
.targetBlank() // target="_blank" rel="noopener noreferrer"
)
// Download link
a(attrs()
.href("/file.pdf")
.download() // Download instead of navigate
)
// Download with filename
a(attrs()
.href("/report-2026.pdf")
.download("annual-report.pdf") // Custom filename
)
// Rel attributes
a(attrs()
.href("/next")
.rel("next") // Pagination hint
)
a(attrs()
.href("https://sponsor.com")
.rel("sponsored") // Paid link (SEO)
)Image Attributes
Comprehensive image handling.
// Basic with alt
img(attrs()
.src("/images/hero.jpg")
.alt("Hero image description")
)
// Lazy loading
img(attrs()
.src("/images/photo.jpg")
.alt("Photo")
.loading("lazy") // Lazy load when near viewport
)
// Dimensions (prevents layout shift)
img(attrs()
.src("/images/product.jpg")
.alt("Product")
.width("800")
.height("600")
)
// Responsive images
img(attrs()
.src("/images/photo.jpg")
.srcset("/images/photo-2x.jpg 2x, /images/photo-3x.jpg 3x")
.alt("Photo")
)
// Picture element for art direction
picture(
source(attrs().media("(min-width: 800px)").srcset("/large.jpg")),
source(attrs().media("(min-width: 400px)").srcset("/medium.jpg")),
img(attrs().src("/small.jpg").alt("Responsive image"))
)Event Handlers
Handlers are plain element arguments — no attrs() bridge needed.
// Click handlers
String id = "42";
button(onClick(call("handleClick")), "Click")
button(onClick(call("deleteItem", id)), "Delete")
// Form events (server-side Java handlers)
void handleSubmit(Event e) {}
void updateValue(String value) {}
void runSearch(String query) {}
void showHint() {}
void hideHint() {}
form(onSubmit(e -> handleSubmit(e)))
input(onChange(e -> updateValue(e.value())))
input(onInput(e -> runSearch(e.value())))
input(onFocus(e -> showHint()))
input(onBlur(e -> hideHint()))
// Mouse events
void showTooltip() {}
void hideTooltip() {}
div(
onMouseEnter(e -> showTooltip()),
onMouseLeave(e -> hideTooltip())
)
// Keyboard events
void handleKey(Event e) {}
void handleKeyUp(Event e) {}
input(onKeyDown(e -> handleKey(e)))
input(onKeyUp(e -> handleKeyUp(e)))
// Raw JS strings when needed — attrs() is still the long tail
form(attrs().set("onsubmit", "return handleSubmit(event)"))
// Using the JS DSL
import static jweb.Js.*;
button(onClick(show("panel")), "Show")
button(onClick(hide("modal")), "Close")
button(onClick(toggle("dropdown")), "Toggle")Custom Attributes
Set any attribute not in the API.
// Generic set method
attrs().set("custom-attr", "value")
// HTMX attributes
attrs()
.set("hx-get", "/api/data")
.set("hx-target", "#result")
.set("hx-swap", "innerHTML")
// Alpine.js
attrs()
.set("x-data", "{ open: false }")
.set("x-show", "open")
.set("@click", "open = !open")
// Any attribute
attrs()
.set("itemscope", "")
.set("itemtype", "https://schema.org/Product")Handles
cls("card") returns a Cls and id("x") an Id — a handle that is an element argument, a selector for the stylesheet and a target for the JavaScript DSL. Declare it once; the class name is never typed twice.
static final Cls CHIP = cls("chip");
static final Cls ON = cls("chip-on");
static final Id SEARCH = id("search");
Element render(boolean active) {
return div(
span(CHIP, when(active, ON), "Java"), // class="chip chip-on"
span(attrs().cls(CHIP).classIf(active, ON), "Go"),
input(SEARCH, type("search"))); // id="search"
}
Stylesheet styles() {
return stylesheet()
.rule(CHIP.hover(), style().opacity(0.9)) // .chip:hover
.rule(SEARCH.focus(), style().outline(none)); // #search:focus
}Conditional Rendering
Show or hide content based on conditions.
when() - Simple Conditional
Render content only when a condition is true.
boolean isLoggedIn = true;
String username = "Ada";
boolean isAdmin = false;
boolean hasNotifications = true;
int count = 3;
Element adminPanel() { return div("Admin"); }
Element notificationBadge(int n) { return span(String.valueOf(n)); }
// Lazy evaluation (recommended for expensive operations)
when(isLoggedIn, () -> span("Welcome back!"))
// Eager evaluation (for simple values)
when(isLoggedIn, span("Welcome, " + username))
// Examples
div(
h1("Dashboard"),
when(isAdmin, () -> adminPanel()),
when(hasNotifications, () -> notificationBadge(count))
)Either/Or
Render different content for true and false conditions with a Java ternary.
boolean isLoggedIn = true;
Element userMenu() { return div("Menu"); }
Element loginButton() { return button("Log in"); }
// If/else pattern
isLoggedIn ? userMenu() : loginButton()
// In context
header(
h1("My App"),
nav(
a(href("/"), "Home"),
isLoggedIn
? a(href("/dashboard"), "Dashboard")
: a(href("/login"), "Sign In")
)
)Multi-Way Choices
One conditional shape — when(condition, element) — covers optional content; Java's switch expression covers branching on a value. There is no separate chain or match() to learn.
enum Role { ADMIN, MODERATOR, USER, GUEST }
Role role = Role.ADMIN;
String statusCode = "success";
boolean showPromo = true;
record User(boolean isPremium, boolean isVerified) {}
User user = new User(true, false);
Element adminPanel() { return div("Admin"); }
Element modPanel() { return div("Moderator"); }
Element userPanel() { return div("User"); }
Element guestPanel() { return div("Guest"); }
Element successMessage() { return div("Success"); }
Element warningMessage() { return div("Warning"); }
Element errorMessage() { return div("Error"); }
Element infoMessage() { return div("Info"); }
Element promoBanner() { return div("Promo"); }
Element premiumBadge() { return span("Premium"); }
Element verifiedBadge() { return span("Verified"); }
Element standardBadge() { return span("Standard"); }
// Multiple conditions
Element panel = switch (role) {
case ADMIN -> adminPanel();
case MODERATOR -> modPanel();
case USER -> userPanel();
default -> guestPanel();
};
// Status display
Element status = switch (statusCode) {
case "success" -> successMessage();
case "warning" -> warningMessage();
case "error" -> errorMessage();
default -> infoMessage();
};
// Without a fallback, just skip rendering when false
when(showPromo, () -> promoBanner())
// User type display — arbitrary predicates read best as a ternary chain
div(
user.isPremium() ? premiumBadge()
: user.isVerified() ? verifiedBadge()
: standardBadge()
)Inline Ternary Conditions
Java's native ternary operator works for simple inline conditions.
boolean isActive = true;
boolean hasError = false;
String errorMessage = "Something went wrong.";
boolean isDark = false;
// Ternary pattern
span(isActive ? "Active" : "Inactive")
// With elements
div(hasError
? span(class_("error"), errorMessage)
: span(class_("success"), "All good!"))
// Styling
div(class_(isDark ? "dark-theme" : "light-theme"))each() - Iteration
Render a list of items.
record User(String getAvatar, String getName, String getEmail, boolean isVerified) {}
List<User> getUsers() { return List.of(new User("/a.png", "Ada", "ada@example.com", true)); }
Element verifiedBadge() { return span("Verified"); }
// Basic iteration
List<String> items = List.of("Apple", "Banana", "Cherry");
ul(
each(items, item -> li(item))
)
// With complex rendering
List<User> users = getUsers();
div(class_("user-list"),
each(users, user ->
div(class_("user-card"),
img(user.getAvatar(), user.getName()),
h3(user.getName()),
p(user.getEmail()),
when(user.isVerified(), () -> verifiedBadge())
)
)
)
// With index (if needed)
ul(
each(IntStream.range(0, items.size()).boxed().toList(), i ->
li(data("index", String.valueOf(i)),
strong((i + 1) + ". "),
items.get(i)
)
)
)Combining Conditionals
Use conditionals together for complex UIs.
record User(String getName) {}
User currentUser = new User("Ada");
Element userDropdown(User u) { return div(u.getName()); }
enum Role { ADMIN, EDITOR, VIEWER }
Role role = Role.ADMIN;
Element adminContent() { return div("Admin content"); }
Element editorContent() { return div("Editor content"); }
Element viewerContent() { return div("Viewer content"); }
boolean showSidebar = true;
record Link(String getUrl, String getTitle) {}
List<Link> quickLinks = List.of(new Link("/docs", "Docs"));
boolean showDebugInfo = false;
Element debugPanel() { return div("Debug"); }
// Complex dashboard
div(class_("dashboard"),
// Header with conditional user display
header(
h1("Dashboard"),
currentUser != null ? userDropdown(currentUser) : a(href("/login"), "Sign In")
),
// Main content with role-based rendering
main(
switch (role) {
case ADMIN -> adminContent();
case EDITOR -> editorContent();
default -> viewerContent();
}
),
// Conditional sidebar
when(showSidebar, () ->
aside(
h3("Quick Links"),
each(quickLinks, link ->
a(href(link.getUrl()), link.getTitle())
)
)
),
// Footer with conditional elements
footer(
p("© 2026"),
when(showDebugInfo, () -> debugPanel())
)
)Null Safety
Handle potentially null values safely.
record Address(String getCity) {}
record User(String getName, String getBio, Address getAddress) {}
User user = new User("Ada", "Building things.", new Address("NYC"));
long id = 1;
Optional<User> findUser(long uid) { return Optional.of(new User("Ada", "Building things.", new Address("NYC"))); }
// Using when() for null checks
when(user != null, () ->
div(
h2("Welcome, " + user.getName()),
p(user.getBio())
)
)
// With Optional
Optional<User> maybeUser = findUser(id);
maybeUser.map(u ->
div(h2(u.getName()))
).orElse(
div(p("User not found"))
)
// Null-safe chaining in rendering
div(
h2(user != null ? user.getName() : "Guest"),
when(user != null && user.getAddress() != null, () ->
p(user.getAddress().getCity())
)
)Fragments
Group elements without adding a wrapper element to the DOM.
boolean showExtra = true;
record User(String getName, String getEmail) {}
// fragment() groups elements without a wrapper
fragment(
h1("Title"),
p("First paragraph"),
p("Second paragraph")
)
// Useful in conditionals
when(showExtra, () -> fragment(
p("Extra info 1"),
p("Extra info 2"),
p("Extra info 3")
))
// Return multiple elements from a method
public Element renderUserInfo(User user) {
return fragment(
span(class_("name"), user.getName()),
span(class_("email"), user.getEmail())
);
}Raw HTML
Insert pre-rendered HTML when needed (use carefully).
String trustedContent = "# Hello\n\nThis is **safe** markdown.";
String markdownToHtml(String md) { return "<p>" + md + "</p>"; }
// raw() inserts HTML directly (be careful with user input!)
raw("<svg>...</svg>")
// Safe usage: only with trusted/sanitized content
String sanitizedMarkdown = markdownToHtml(trustedContent);
div(class_("markdown-body"), raw(sanitizedMarkdown))