From vaadin-claude
Guides creation of visually distinctive Vaadin 25 interfaces with custom CSS and theme overrides. Invoked when user requests design improvements or polish.
npx claudepluginhub vaadin/claude-plugin --plugin vaadin-claudeThis 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"`.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
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;
}
Badge is a preview feature in Vaadin 25.1 — enable it with the badgeComponent feature flag. Available variants: SUCCESS, WARNING, ERROR, FILLED, ICON_ONLY, NUMBER_ONLY.
// Badge component (Vaadin 25.1+ preview — requires badgeComponent feature flag)
Badge pending = new Badge("Pending");
Badge confirmed = new Badge("Confirmed");
confirmed.addThemeVariants(BadgeVariant.SUCCESS);
Badge warning = new Badge("Warning");
warning.addThemeVariants(BadgeVariant.WARNING);
Badge denied = new Badge("Denied");
denied.addThemeVariants(BadgeVariant.ERROR);
// With icon
Badge iconBadge = new Badge("Confirmed", VaadinIcon.CHECK.create());
iconBadge.addThemeVariants(BadgeVariant.SUCCESS);
// With number
Badge counter = new Badge("Inbox", 12);
counter.addThemeVariants(BadgeVariant.FILLED);
For Vaadin versions before 25.1, badges can be created with Span elements using the theme="badge" attribute, but this approach is deprecated.
// 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 grid — uses CSS Grid for responsive layout
Div metrics = new Div(
createCard("Revenue", "$48,200", "+12% from last month"),
createCard("Users", "1,420", "+5% from last month"),
createCard("Orders", "384", "+8% from last month")
);
metrics.addClassName("metrics-grid");
// Companion CSS:
// .metrics-grid {
// display: grid;
// grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
// gap: var(--lumo-space-m);
// width: 100%;
// }
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.