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")

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 JS

Container 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")
)

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")
)
Use the for_ attribute on labels to associate them with inputs by id for accessibility.

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 characters
Modern HTML5 elements work in all modern browsers. Dialog and details have native keyboard support and accessibility built-in.

Enhanced 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 step
Core elements (figure, dl, blockquote, fieldset, etc.) and the popover/picture attributes are all available via El.* static import; PopoverElements and PictureElements add a few action helpers (popoverHideButton, showPopover, ...) on top. There is no FormEnhancements module any more — a specialized input (date, time, range, color, ...) is a plain input() with the matching type attribute.

SVG 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 vertically

Definitions & 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")
)
SVG elements inherit color from CSS using currentColor. Set the parent's color property to style icons.

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 href

Multiple 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/radio

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")
Use class_() instead of class() because 'class' is a reserved word in Java.

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())
    )
)
Use lazy evaluation () -> element() for expensive operations to avoid unnecessary computation when the condition is false.

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))
Never use raw() with user-provided content. It can lead to XSS vulnerabilities.