Skip to content

feature: Add CSS Style DSL and Class toggle utilities to uni.dom#390

Merged
xerial merged 3 commits intomainfrom
feature/uni-dom-improvements
Feb 2, 2026
Merged

feature: Add CSS Style DSL and Class toggle utilities to uni.dom#390
xerial merged 3 commits intomainfrom
feature/uni-dom-improvements

Conversation

@xerial
Copy link
Copy Markdown
Member

@xerial xerial commented Feb 2, 2026

Summary

Add CSS Style DSL and Class toggle utilities following React/Vue conventions:

  • style object extends DomElement for dual purpose:
    • CSS style builder: style(style.display := "flex", style.gap := "8px")
    • <style> tag element: style("body { margin: 0; }")
  • Raw style strings: style := "display: flex;" (Tailwind-friendly)
  • Class toggle: cls.toggle("active") when condition
  • 65+ type-safe CSS properties

Usage

import wvlet.uni.dom.all.*

val isActive = Rx.variable(false)

div(
  // Grouped CSS properties (React/Vue style)
  style(style.display := "flex", style.gap := "8px"),
  
  // Raw style string (Tailwind-friendly)
  style := "margin: 10px;",
  
  // Class toggle with static/reactive conditions
  cls := "base",
  cls.toggle("active") when isActive
)

// <style> tag element
head(style("body { margin: 0; }"))

Design

The style object extends DomElement("style", ...) to:

  1. Override HtmlTags.style without type conflicts
  2. Serve as both CSS builder and <style> tag
  3. Allow style.display := "flex" syntax for properties

Test plan

  • CSS styles tests (8 test cases)
  • CSS class toggle tests (8 test cases)
  • All existing tests pass (71 tests in domTest)

🤖 Generated with Claude Code

@github-actions github-actions Bot added doc Improvements or additions to documentation feature New feature labels Feb 2, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @xerial, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the uni.dom library by introducing a robust and flexible system for managing CSS styles and classes. It provides developers with a type-safe DSL for inline styling, alongside a powerful utility for conditionally applying CSS classes, which can react to changes in application state. These additions streamline front-end development, making it easier to create dynamic and well-styled user interfaces within the uni.dom framework.

Highlights

  • Type-Safe CSS Style DSL: Introduced CssStyles with over 60 common CSS properties (e.g., display, gap, color) for type-safe inline styling using a := syntax.
  • Raw CSS String Support: Added css := "..." to allow direct application of raw CSS strings, providing flexibility for complex styles or integration with frameworks like Tailwind CSS.
  • Conditional CSS Class Toggling: Implemented cls.toggle("className") when condition for dynamically adding or removing CSS classes based on a condition.
  • Reactive Class Toggling: The class toggling utility supports both static Boolean conditions and reactive Rx[Boolean] streams, enabling dynamic UI updates.
  • Comprehensive Testing: New dedicated test files (CssStylesTest.scala, CssClassTest.scala) have been added with 7 and 8 test cases respectively, ensuring the robustness of the new styling features.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a type-safe CSS style DSL and utilities for toggling CSS classes, which significantly improves the developer experience for styling components in uni.dom. The implementation is well-structured and accompanied by a comprehensive set of tests. The changes are a great addition to the framework. I've provided a few suggestions to improve consistency in the new CssStyles DSL and to align the design document with the final implementation.

Comment on lines +65 to +67
// Reactive toggle - returns Rx that emits class or empty
def when[A](rx: RxOps[A])(using ev: A =:= Boolean): DomNode =
rx.map(v => when(ev(v)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of the reactive when in CssClass.scala is simpler and more direct than what is described in this plan. The implementation uses Rx[Boolean] directly, which is clearer. Also, the plan's method body returns an Rx but the signature says DomNode, which is a mismatch. The actual implementation correctly wraps the result in Embedded.

It would be good to update this design document to reflect the final implementation for future reference.

Suggested change
// Reactive toggle - returns Rx that emits class or empty
def when[A](rx: RxOps[A])(using ev: A =:= Boolean): DomNode =
rx.map(v => when(ev(v)))
// Reactive toggle - returns a DomNode that reactively updates the class
def when(rx: Rx[Boolean]): DomNode =
Embedded(rx.map(cond => when(cond)))

Comment on lines +76 to +77
object cls extends DomAttributeOf("class"):
def toggle(className: String): ClassToggle = ClassToggle(className)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of cls in HtmlAttrs.scala also includes a := operator for setting the class directly, for consistency with the new style DSL. This design document should be updated to include it.

Suggested change
object cls extends DomAttributeOf("class"):
def toggle(className: String): ClassToggle = ClassToggle(className)
object cls extends DomAttributeOf("class"):
def :=[V: EmbeddableAttribute](v: V): DomNode = apply(v)
def toggle(className: String): ClassToggle = ClassToggle(className)

* `display := "flex"` for type-safe styling.
*/
object css:
def :=(value: String): DomAttribute = DomAttribute("style", value, append = true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with StyleProperty and to make style composition more robust, it's a good practice to ensure the raw CSS string ends with a semicolon. The current implementation passes the raw string as-is, while StyleProperty always adds a semicolon. This can lead to unexpected behavior if a user provides a style string without a trailing semicolon when combining it with other styles.

By ensuring a trailing semicolon, we make the behavior more predictable and less reliant on the browser's cssText parsing leniency.

    def :=(value: String): DomAttribute = {
      val trimmed = value.trim
      val finalValue = if (trimmed.nonEmpty && !trimmed.endsWith(";")) s"${trimmed};" else trimmed
      DomAttribute("style", finalValue, append = true)
    }

@xerial xerial force-pushed the feature/uni-dom-improvements branch from 312ec9a to 2e3a205 Compare February 2, 2026 17:38
This improves developer experience when working with inline styles and
conditional CSS classes, following React/Vue conventions:

- Add `style` object that extends DomElement for dual purpose:
  - CSS style builder: `style(style.display := "flex", style.gap := "8px")`
  - <style> tag element: `style("body { margin: 0; }")`
- Add `style := "..."` for raw style strings (Tailwind-friendly)
- Add `cls.toggle("className") when condition` for conditional classes
- Support both static boolean and reactive Rx[Boolean] conditions
- 65+ type-safe CSS properties

Usage:
```scala
import wvlet.uni.dom.all.*

val isActive = Rx.variable(false)

div(
  style(style.display := "flex", style.gap := "8px"),
  style := "margin: 10px;",  // raw string
  cls := "base",
  cls.toggle("active") when isActive
)
```

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@xerial xerial force-pushed the feature/uni-dom-improvements branch from 2e3a205 to bd1a15f Compare February 2, 2026 18:07
xerial and others added 2 commits February 2, 2026 10:29
- Fix reactive class toggle to use direct attribute binding instead of
  Embedded(rx.map(...)) to avoid inserting text node markers that break
  :empty selectors and DOM child order

- Fix style() overload ambiguity by requiring at least one StyleValue
  argument, resolving conflict with inherited apply(DomNode*) from DomElement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update reactive class toggle to reflect direct attribute binding fix
- Update style apply signature to require at least one argument
- Add Review Learnings section documenting codex review fixes
- Document design evolution from user feedback

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@xerial xerial enabled auto-merge (squash) February 2, 2026 18:37
@xerial xerial merged commit fe49ac4 into main Feb 2, 2026
13 checks passed
@xerial xerial deleted the feature/uni-dom-improvements branch February 2, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Improvements or additions to documentation feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant