From vaadin-development
Guide Claude on creating visually distinctive, polished Vaadin 25 interfaces that go beyond default theme styling. This skill should be used when the user asks to "make it look good", "improve the design", "style the view", "make it visually appealing", "add polish", "design a UI", "create a beautiful interface", or when building a new view where visual quality matters. Also trigger when the user wants to add animations, visual effects, or build polished component compositions in a Vaadin application.
npx claudepluginhub marcushellberg/vaadin-development-plugin --plugin vaadin-developmentThis skill uses the workspace's default tool permissions.
Use the Vaadin MCP tools (`search_vaadin_docs`, `get_component_styling`, `get_component_java_api`) to look up the latest documentation whenever uncertain about a specific API detail. Always set `vaadin_version` to `"25"` and `ui_language` to `"java"`.
Enforces precise, minimal UI design for dashboards, admin panels, and SaaS apps by avoiding generic AI patterns and guiding product-specific choices.
Generates distinctive, production-grade frontend code for web components, pages, and apps with bold, creative UX designs in styles like brutalist or retro-futuristic, avoiding generic AI aesthetics.
Generates distinctive production-grade frontend UIs for web components, pages, and apps with bold creative designs avoiding generic AI aesthetics.
Share bugs, ideas, or general feedback.
Use the Vaadin MCP tools (search_vaadin_docs, get_component_styling, get_component_java_api) to look up the latest documentation whenever uncertain about a specific API detail. Always set vaadin_version to "25" and ui_language to "java".
This skill guides creation of distinctive, polished Vaadin interfaces that go beyond default theme styling. The goal is production-grade code with genuine attention to aesthetic detail — not generic defaults.
Before writing code, understand the context and commit to a clear aesthetic direction:
CRITICAL: Choose a clear direction and execute it consistently. Every color, spacing, and typography choice should reinforce the same aesthetic. A cohesive simple design always beats an inconsistent elaborate one.
Then implement working code (Java + CSS) that is:
All visual customization starts with your chosen theme — Aura or Lumo. Both provide:
@StyleSheet with view-scoped CSS files.Lumo additionally provides:
LumoUtility.*) for layout, spacing, colors. Fast to apply from Java. Not available with Aura.Always work top-down: change a theme property before writing component-specific CSS, and use a theme variant before writing custom styles.
For full details on theme selection, loading, and all design tokens, see the theming skill.
Typography sets the tone of the entire interface.
Customizing the font family (works with both themes):
/* In styles.css */
@font-face {
font-family: 'Your Font';
src: url('./fonts/your-font.woff2') format('woff2');
font-display: swap;
}
html {
/* Aura */
--aura-font-family: 'Your Font', sans-serif;
/* Lumo */
--lumo-font-family: 'Your Font', sans-serif;
}
Tuning the type scale:
Aura — adjust one value and the entire scale is computed:
html {
--aura-base-font-size: 15; /* unitless number, represents px */
}
Lumo — override individual tokens:
html {
--lumo-font-size-xxxl: 2.5rem;
--lumo-font-size-xxl: 1.75rem;
--lumo-font-size-xl: 1.375rem;
--lumo-font-size-l: 1.125rem;
--lumo-font-size-m: 1rem;
--lumo-font-size-s: 0.875rem;
--lumo-font-size-xs: 0.8125rem;
--lumo-font-size-xxs: 0.75rem;
}
Creating typographic hierarchy from Java (Lumo utility classes):
// Lumo theme only — requires Lumo.UTILITY_STYLESHEET
H2 title = new H2("Dashboard");
title.addClassNames(
LumoUtility.FontSize.XXLARGE,
LumoUtility.FontWeight.BOLD,
LumoUtility.TextColor.HEADER
);
Span subtitle = new Span("Weekly performance overview");
subtitle.addClassNames(
LumoUtility.FontSize.MEDIUM,
LumoUtility.TextColor.SECONDARY
);
For Aura, use CSS classes or inline styles instead of LumoUtility.
Key principle: Establish a clear hierarchy — one dominant heading style, one body style, one secondary/caption style. Use font size, weight, and color together to differentiate levels. Avoid using more than 3-4 distinct text styles in a single view.
Color is the fastest way to give a Vaadin app a distinctive identity.
Aura — customizing the accent and palette:
html {
--aura-accent-color-light: hsl(220, 80%, 50%);
--aura-accent-color-dark: hsl(220, 85%, 65%);
--aura-background-color-light: hsl(220, 20%, 98%);
--aura-background-color-dark: hsl(220, 20%, 12%);
--aura-blue: hsl(220, 80%, 50%);
--aura-green: hsl(150, 60%, 40%);
--aura-red: hsl(0, 75%, 55%);
}
Lumo — overriding the color palette:
html {
/* Primary color — used for buttons, links, focus rings */
--lumo-primary-color: hsl(220, 80%, 50%);
--lumo-primary-color-50pct: hsla(220, 80%, 50%, 0.5);
--lumo-primary-color-10pct: hsla(220, 80%, 50%, 0.1);
--lumo-primary-text-color: hsl(220, 80%, 45%);
--lumo-primary-contrast-color: #fff;
/* Surface colors — backgrounds, cards, dialogs */
--lumo-base-color: hsl(220, 20%, 98%);
/* Error, success, warning */
--lumo-error-color: hsl(0, 75%, 55%);
--lumo-success-color: hsl(150, 60%, 40%);
--lumo-warning-color: hsl(40, 95%, 50%);
}
Dark mode:
Both themes support the @ColorScheme annotation. For custom dark mode colors, see the theming skill for theme-specific selectors ([theme~="dark"] for Lumo, -light/-dark suffixed properties for Aura).
Accent and semantic colors from Java (Lumo utility classes):
// Lumo theme only
badge.addClassNames(
LumoUtility.Background.PRIMARY,
LumoUtility.TextColor.PRIMARY_CONTRAST
);
warningCard.addClassNames(
LumoUtility.Background.WARNING_10PCT,
LumoUtility.TextColor.WARNING
);
Key principle: Commit to a cohesive palette. Pick one strong primary/accent color and use the theme's variant system (Lumo's opacity variants, Aura's computed variants) for secondary uses. A dominant primary with restrained accent colors outperforms an evenly distributed rainbow.
Consistent spacing creates visual rhythm and professionalism.
Aura — adjust the base size:
html {
--aura-base-size: 18; /* unitless, range 12–24 */
--aura-base-radius: 8; /* unitless, range 0–10 */
}
Lumo — override individual spacing tokens:
html {
--lumo-space-xs: 0.25rem;
--lumo-space-s: 0.5rem;
--lumo-space-m: 1rem;
--lumo-space-l: 1.5rem;
--lumo-space-xl: 2.5rem;
}
Applying spacing from Java (Lumo utility classes):
// Lumo theme only
card.addClassNames(
LumoUtility.Padding.LARGE,
LumoUtility.Gap.MEDIUM
);
section.addClassNames(
LumoUtility.Padding.Horizontal.LARGE,
LumoUtility.Padding.Vertical.XLARGE
);
Compact/dense variants:
// Lumo theme — compact variants
grid.addThemeVariants(GridVariant.LUMO_COMPACT, GridVariant.LUMO_NO_BORDER);
textField.addThemeVariants(TextFieldVariant.LUMO_SMALL);
button.addThemeVariants(ButtonVariant.LUMO_SMALL);
// Aura theme — compact variants
grid.addThemeVariants(GridVariant.AURA_COMPACT, GridVariant.AURA_NO_BORDER);
// Note: Aura does not have SMALL variants for TextField/Button
Key principle: Pick a density and stick with it. Data-dense dashboards should be consistently compact. Spacious marketing-style views should be consistently airy. Mixing densities looks unintentional.
Create depth and visual hierarchy through elevation.
Aura — surface level system:
.elevated-card {
background: var(--aura-surface-color);
--aura-surface-level: 2;
}
.sunken-area {
background: var(--aura-surface-color);
--aura-surface-level: -1;
}
Lumo — explicit shadow tokens:
html {
--lumo-box-shadow-xs: 0 1px 2px 0 rgba(0,0,0,0.05);
--lumo-box-shadow-s: 0 2px 4px -1px rgba(0,0,0,0.1);
--lumo-box-shadow-m: 0 4px 8px -2px rgba(0,0,0,0.1);
--lumo-box-shadow-l: 0 8px 16px -4px rgba(0,0,0,0.15);
--lumo-box-shadow-xl: 0 16px 32px -8px rgba(0,0,0,0.2);
}
// Lumo theme only
card.addClassNames(
LumoUtility.BoxShadow.SMALL,
LumoUtility.BorderRadius.MEDIUM
);
Border radius customization:
/* Lumo */
html {
--lumo-border-radius-s: 4px;
--lumo-border-radius-m: 8px;
--lumo-border-radius-l: 16px;
}
/* Aura — adjust the base radius instead */
html {
--aura-base-radius: 8; /* computes all radius values */
}
Subtle borders for separation (Lumo utility classes):
// Lumo theme only
section.addClassNames(
LumoUtility.Border.BOTTOM,
LumoUtility.BorderColor.CONTRAST_10
);
Key principle: Use elevation consistently to communicate hierarchy. Cards float above the surface, dialogs float above cards. Don't put heavy shadows on flat elements or flat shadows on floating elements.
Vaadin's server-side model means animations should be CSS-driven. Keep them subtle and purposeful.
View transitions with CSS:
/* Fade-in for view content */
.fade-in {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Staggered reveal for list items */
.stagger-item {
animation: fadeIn 0.3s ease-out backwards;
}
.stagger-item:nth-child(1) { animation-delay: 0.05s; }
.stagger-item:nth-child(2) { animation-delay: 0.1s; }
.stagger-item:nth-child(3) { animation-delay: 0.15s; }
.stagger-item:nth-child(4) { animation-delay: 0.2s; }
.stagger-item:nth-child(5) { animation-delay: 0.25s; }
content.addClassName("fade-in");
// Staggered card reveal
for (Component card : cards) {
card.addClassName("stagger-item");
}
Hover and interaction effects:
.interactive-card {
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.interactive-card:hover {
box-shadow: 0 4px 8px -2px rgba(0,0,0,0.1); /* or use theme token */
transform: translateY(-2px);
}
Key principle: One or two well-crafted animations (like a staggered page-load reveal) create more impact than scattered micro-interactions everywhere. Prefer CSS transitions on hover/focus over complex keyframe animations. Keep durations under 400ms.
// Lumo theme only — uses LumoUtility classes
public static Div createCard(String title, String value, String description) {
Div card = new Div();
card.addClassNames(
LumoUtility.Background.BASE,
LumoUtility.BorderRadius.MEDIUM,
LumoUtility.BoxShadow.SMALL,
LumoUtility.Padding.LARGE,
LumoUtility.Display.FLEX,
LumoUtility.FlexDirection.COLUMN,
LumoUtility.Gap.SMALL
);
Span titleSpan = new Span(title);
titleSpan.addClassNames(
LumoUtility.FontSize.SMALL,
LumoUtility.TextColor.SECONDARY,
LumoUtility.FontWeight.MEDIUM
);
Span valueSpan = new Span(value);
valueSpan.addClassNames(
LumoUtility.FontSize.XXLARGE,
LumoUtility.FontWeight.BOLD,
LumoUtility.TextColor.HEADER
);
Span descSpan = new Span(description);
descSpan.addClassNames(
LumoUtility.FontSize.SMALL,
LumoUtility.TextColor.TERTIARY
);
card.add(titleSpan, valueSpan, descSpan);
return card;
}
For Aura, use CSS classes with Aura's surface system instead of LumoUtility:
.metric-card {
background: var(--aura-surface-color);
--aura-surface-level: 2;
border-radius: var(--vaadin-border-radius-m);
padding: var(--vaadin-padding);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
public static Span createBadge(String text, BadgeVariant variant) {
Span badge = new Span(text);
badge.getElement().getThemeList().add("badge " + variant.theme);
return badge;
}
public enum BadgeVariant {
DEFAULT(""),
SUCCESS("success"),
ERROR("error"),
WARNING("warning"),
CONTRAST("contrast"),
PRIMARY("primary"),
PILL("pill"),
SMALL("small");
final String theme;
BadgeVariant(String theme) { this.theme = theme; }
}
Combine variants: "badge success small pill" for a small success pill badge. Badges work with both Aura and Lumo.
// Lumo theme only — uses LumoUtility classes
VerticalLayout dashboard = new VerticalLayout();
dashboard.setPadding(true);
dashboard.setSpacing(false);
dashboard.addClassNames(
LumoUtility.Gap.LARGE,
LumoUtility.Background.CONTRAST_5
);
// Metric cards row
HorizontalLayout metrics = new HorizontalLayout();
metrics.setWidthFull();
metrics.addClassNames(LumoUtility.Gap.MEDIUM);
metrics.add(
createCard("Revenue", "$48,200", "+12% from last month"),
createCard("Users", "1,420", "+5% from last month"),
createCard("Orders", "384", "+8% from last month")
);
metrics.getChildren().forEach(c -> ((HasSize) c).setWidthFull());
dashboard.add(createSectionHeader("Overview"), metrics);
When theme properties and theme variants aren't enough, use CSS.
Shadow DOM and ::part() selectors — Vaadin components use shadow DOM. Style internal parts with ::part():
/* Style Grid header cells */
vaadin-grid::part(header-cell) {
background-color: var(--lumo-contrast-5pct); /* Lumo */
/* or for Aura: --aura-surface-level: 0; */
font-weight: 600;
}
/* Style Dialog overlay */
vaadin-dialog-overlay::part(overlay) {
border-radius: 16px;
}
/* Style TextField input */
vaadin-text-field::part(input-field) {
border-radius: 8px;
}
Component-scoped styles — apply CSS to a specific view:
@StyleSheet("styles/views/dashboard-view.css")
@Route("dashboard")
public class DashboardView extends VerticalLayout {
// ...
}
Key principle: Always prefer theme custom properties over hardcoded values in CSS. Use your active theme's tokens for colors, spacing, and sizing. This keeps styles consistent with the theme and makes dark mode, density changes, and future redesigns trivial.
ButtonVariant, GridVariant, TextFieldVariant, etc. before writing custom CSS. The variant you need probably exists.For component ::part() selectors, CSS animation recipes, and color palette recipes, see references/design-patterns.md. For complete theme token tables and variant comparisons, see the theming skill's references/theming-patterns.md.