Vaadin 25 Migration: LumoUtility Replacement and Tailwind/Aura Best Practices

When migrating from Vaadin 24 (Lumo) to Vaadin 25 (Aura), the documentation provides general guidance on styling, including how to use Tailwind, but it doesn’t specifically address migrating from LumoUtility to Aura. I’ve read the Vaadin docs on the Recommended Styling Approach, which suggests using Theme → Variant → Style Properties → CSS style blocks to minimize custom CSS and avoid future breakage.

Some spacing and layout concepts seem to have moved into base styles / CSS variables (e.g., --vaadin-padding-m), while for other styling needs Tailwind provides the string-based utility classes.

I’ve already found a related forum post describing similar issues with LumoUtility: Lumo → Aura migration.

The Tailwind GitHub issue is still open, and “Typesafe Flow API” is listed as a nice-to-have, so it’s currently unclear if or when it will be implemented: Vaadin Platform #7454.
image

My questions:

  • Is it considered best practice to use Tailwind together with Aura base styles, or for per-component CSS adjustments, is it better to move these changes into CSS files? Would this approach be future-proof, given that Tailwind in Flow might not be fully supported long-term?
  • Should we wait to see if a Flow-side API for Tailwind utilities becomes available, or is that unlikely, meaning we might need to create our own Tailwind utility classes in the meantime?

Some clarity here would really help decide whether to migrate now or postpone to avoid extra work.

1 Like

I can’t speak for Vaadin

But the company behind Tailwind is suffering from AI and they had to layoff 3/4 of their employees.

I stick with Lumo because I know it very well and we created our own theme based on it

  • Tailwind is the only Aura-compatible replacement for Lumo Utility Classes, so if you want to use utility classes with Aura, that’s the recommended approach.
  • We do currently intend to ship a typesafe Java API for Tailwind, but don’t yet know when that will happen.
  • In the meantime, using Tailwind through String classnames is perfectly future-proof – that will still work when the typesafe API is introduced.
  • My impression of the Tailwind situation is that they’re struggling to sell their commercial Plus plan (that includes components and templates), but that is not the same as the Tailwind CSS utility classes. I would say it’s highly unlikely that Tailwind CSS would cease to exist, seeing as how it’s extremely popular.
  • I also think it’s highly unlikely that Vaadin would drop support for Tailwind CSS.
  • Just like LumoUtility, Tailwind is primarily for styling native HTML elements and the Horizontal and Vertical Layouts, rather than proper Vaadin components, since you can’t customize most of the internal elements in components through it. Component customization is best done in stylesheets (and the components’ docs pages have Styling subpages that list the various properties and selectors you can use for it).
  • Also want to mention that moving from Lumo to Aura in V25 is of course completely optional – there are no plans on dropping the Lumo theme.
1 Like

Thanks for the clarification, that really helps.

I appreciate learning that Tailwind is the recommended way to replace LumoUtility for Aura. It’s reassuring to know that using string class names is future-proof, which helps clarify the migration path even if the timeline for a typesafe Flow API is still unclear.

It’s also a good reminder that switching from Lumo to Aura is optional. I only realized after my initial post that Lumo is not deprecated, but I still plan to move to Aura.

Hello,
I was facing the same problem with my application. Since I didn’t know Tailwind and didn’t know the class names and as I don’t like putting those names using Strings in my code I created a class “TailwindUtility” that fits my needs and that I use in my application.

I’m pretty sure this will not fit everybody else’s needs but it’s ok for me and let’s me wait to see what better solution the Vaadin Team will come up with.

I add the code just in case …


import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults;

public class TailwindUtility {

	public interface HasCssClassName {
		String cssClassName();
	}

	public enum Color {
		BLUE, GREEN, FUCHSIA, RED, YELLOW, GRAY;
	}
	
	public class Badge {
		public static String badge() {
			return TailwindUtility.join(Inline.FLEX, Items.CENTER, Rounded.FULL, Font.MEDIUM, Padding.HORIZONTAL_2, Padding.VERTICAL_1, InsetRing.RING, Whitespace.NOWRAP);
		}
		
		public static String color(Color color) {
			return switch(color) {
				case BLUE -> TailwindUtility.join(Background.BLUE_50, TextColor.BLUE_700, InsetRing.BLUE_700_10);
				case GREEN -> TailwindUtility.join(Background.GREEN_50, TextColor.GREEN_700, InsetRing.GREEN_600_20);
				case FUCHSIA -> TailwindUtility.join(Background.FUCHSIA_50, TextColor.FUCHSIA_700, InsetRing.FUCHSIA_700_10);
				case RED -> TailwindUtility.join(Background.RED_50, TextColor.RED_700, InsetRing.RED_600_10);
				case YELLOW -> TailwindUtility.join(Background.YELLOW_50, TextColor.YELLOW_800, InsetRing.YELLOW_600_20);
				case GRAY -> TailwindUtility.join(Background.GRAY_50, TextColor.GRAY_600, InsetRing.GRAY_500_10);
				default -> TailwindUtility.join(Background.BLUE_50, TextColor.BLUE_700, InsetRing.BLUE_700_10);
			};	
		}
	}


	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Inline implements HasCssClassName {
		FLEX("inline-flex")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Items implements HasCssClassName {
		CENTER("items-center")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Rounded implements HasCssClassName {
		FULL("rounded-full")
		, MEDIUM("rounded-md")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Padding implements HasCssClassName {
		ZERO("p-0")

		, HORIZONTAL_2("px-2")
		, HORIZONTAL_4("px-4")
		
		, VERTICAL_1("py-1")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Font implements HasCssClassName {
		MEDIUM("font-medium")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Whitespace implements HasCssClassName {
		NOWRAP("whitespace-nowrap")
		, BREAK_SPACES("whitespace-break-spaces")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Background implements HasCssClassName {
		BLUE_50("bg-blue-50")
		, GREEN_50("bg-green-50")
		, FUCHSIA_50("bg-fuchsia-50")
		, RED_50("bg-red-50")
		, YELLOW_50("bg-yellow-50")
		, GRAY_50("bg-gray-50")
		
		, GRAY_200_50("bg-gray-200/50")
		, YELLOW_500("bg-yellow-500")
		, GREEN_500("bg-green-500")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum TextColor implements HasCssClassName {
		BLUE_700("text-blue-700")
		, GREEN_700("text-green-700")
		, FUCHSIA_700("text-fuchsia-700")
		, RED_700("text-red-700")
		, YELLOW_800("text-yellow-800")
		, GRAY_600("text-gray-600")
		, GRAY_800("text-gray-800")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum InsetRing implements HasCssClassName {
		RING("inset-ring")
		
		, BLUE_700_10("inset-ring-blue-700/10")
		, GREEN_600_20("inset-ring-green-600/20")
		, FUCHSIA_700_10("inset-ring-fuchsia-700/10")
		, RED_600_10("inset-ring-red-600/10")
		, YELLOW_600_20("inset-ring-yellow-600/20")
		, GRAY_500_10("inset-ring-gray-500/10")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Margin implements HasCssClassName {
		TOP_4("mt-4")
		
		, RIGHT_AUTO("mr-auto")
		, RIGHT_1("mr-1")
		, RIGHT_2("mr-2")
		, RIGHT_4("mr-4")
		
		, BOTTOM_2("mb-2")
		
		, LEFT_AUTO("ml-auto")
		, LEFT_1("ml-1")
		, LEFT_2("ml-2")
		, LEFT_4("ml-4")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum FontVariantNumeric implements HasCssClassName {
		TABULAR_NUMS("tabular-nums")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Gap implements HasCssClassName {
		HORIZONTAL_4("gap-x-4")
		, GAP_1("gap-1")
		, GAP_2("gap-2")
		
		, VERTICAL_4("gap-y-4")
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum AlignContent implements HasCssClassName {
		CENTER("content-center")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum JustifyContent implements HasCssClassName {
		BETWEEN("justify-between")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum FontSize implements HasCssClassName {
		EXTRA_SMALL("text-xs")
		, SMALL("text-sm")
		, LARGE("text-lg")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum FontWeight implements HasCssClassName {
		BOLD("font-bold")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum LineClamp implements HasCssClassName {
		CLAMP_3("line-clamp-3")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Overflow implements HasCssClassName {
		HIDDEN("overflow-hidden")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum Cursor implements HasCssClassName {
		POINTER("cursor-pointer")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum FlexDirection implements HasCssClassName {
		COL_REVERSED("flex-col-reversed")
		
		;
		
		@Getter final String cssClassName;
	}

	@RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true)
	public enum TextTransform implements HasCssClassName {
		UPPERCASE("uppercase")
		
		;
		
		@Getter final String cssClassName;
	}


	public static String join(HasCssClassName ... attributes) {
		return Stream.of(attributes).map(TailwindUtility::css).collect(Collectors.joining(" "));
	}
	
	public static String css(HasCssClassName attribute) {
		return attribute.cssClassName();
	}
}

1 Like