Imported from vaadin/expense-manager (
.agents/skills/figma-to-vaadin/SKILL.md). Install upstream withnpx skills add vaadin/expense-manager --skill figma-to-vaadin. Copyright stays with the author.
Figma to Vaadin Implementation
Scope
This skill produces Vaadin Flow (Java) code that reproduces the layout and component structure of a Figma design. It does not configure global theme tokens, brand colors, or typography — that belongs to a separate theme configuration skill.
The main failure mode this skill guards against is jumping straight to code from a guess. Gather enough context — the design, its annotations, and the real Vaadin API — before writing anything.
Project overrides
This is a local copy, adapted to this repo. Upstream is silent on all of the following;
here they are hard rules. They override anything else in this skill, and they sit on top of
docs/theming-layouts.md and
CLAUDE.md, which are the binding authority.
- No
LumoUtility, no Tailwind.layout-approachisvaadin-css, permanently. Vaadin layout Java APIs for structure; scoped, role-named CSS classes for the rest. - No
LUMO_*theme variants. This app runs Aura, where theLUMO_*constants are the legacy naming. Use the theme-agnostic ones —ButtonVariant.PRIMARY,TERTIARY,ERROR,SUCCESS,WARNING,SMALL,LARGE. Thetertiary-inline,contrastandiconvariants are Lumo-only and do nothing under Aura; use plainTERTIARY(findings F-013, F-017). - No
--lumo-*CSS custom properties. They are undefined under Aura, so they render nothing — silently, with no error. Use--aura-*and--vaadin-*tokens; look the exact names up withget_theme_css_properties theme=aurarather than guessing. - Aura has no
Npctopacity scale. There is no--aura-red-10pctequivalent to Lumo's--lumo-error-color-10pct. Derive a tint withcolor-mix(in srgb, var(--aura-red) 15%, transparent). - Custom styling lands in one place:
src/main/resources/META-INF/resources/styles.css, as scoped, kebab-case, role-named classes (order-summary-card, notblue-background). Not ingetStyle().set(...), and not in a new stylesheet per view. - Vaadin docs tools come from the
vaadin-skillsplugin, not aVaadinMCP server entry — this repo deliberately has none.search_vaadin_docs,get_full_document,get_component_java_apiandget_theme_css_propertiesare available under the plugin's tool prefix; that a server namedVaadinis missing is not a setup failure.
Workflow
Create TODOs from these steps and follow them in order.
1. Fetch design context
get_design_context on the given node is the primary source — it has the most detailed
component information; check data-name for component type, and note theme/variant hints and
text styles. If the response is truncated (very large or deeply nested frames), fall back to
get_metadata for the layer hierarchy, then call get_design_context on the specific child
nodes you need.
2. Check component annotations
For each component instance, apply these in order: recommended Vaadin component, theme variants, accessibility requirements, implementation notes, documentation links. Annotations override guesses from layer names.
If a Figma component still doesn't map clearly to one Vaadin component after checking annotations, ask: "Should this be a [ComponentA] or [ComponentB]? The Figma shows [description]." Don't guess.
3. Research each component (mandatory)
Never rely on memorized Vaadin knowledge — API surfaces and feature-flag status change between versions.
search_vaadin_docsto find candidates, recordfile_pathget_full_documentfor every component before implementing — search results are previews, not enough on their ownget_component_java_apifor the exact Java method signatures — use this whenever you need to know which methods a component exposes (slot setters, theme variants, sizing)
If a compile error suggests a method doesn't exist, re-read the component's Java API docs
before guessing at a fix. Don't search local .m2 jars for source, and don't run anything to
"just try it" — the docs are the authoritative source.
4. Resolve project preferences, once
This project has already resolved all four preferences in .agent-context
at the repo root. Read that file and use its values — do not ask the user, and do not
re-derive them by auto-detection. Only ask if a key you need is genuinely absent from it.
layout-approach: vaadin-css
architecture: composed-components
sample-data: use-existing-data
verification: verify
| Preference | Values | Auto-detect | Otherwise |
|---|---|---|---|
layout-approach |
vaadin-css only |
Fixed by project standard — lumo-utility and tailwind are forbidden here. |
Never ask; the value is pinned in .agent-context. |
architecture |
single-view / composed-components |
No reliable signal | "Should I build this as one view class with private helper methods, or split it into reusable components (e.g. a separate detail/edit form that fires its own save/cancel events)?" |
sample-data |
generate-sample / use-existing-data |
Check whether the project already has a repository, service, or entity matching the data shown in the design | "Should I generate small sample data for this view, or is there existing data/service in the project I should wire it to instead?" |
verification |
skip / verify / verify-and-fix |
No reliable signal | "After implementing, should I skip testing, run visual verification against the Figma design, or run visual verification and automatically apply one round of fixes based on the findings?" |
Read these two project documents before writing any layout code — they are the binding authority on layout and styling in this repo, and they override anything in this skill:
docs/theming-layouts.md— the layout & spacing standard: which Vaadin layout Java API covers which need, the--vaadin-gap-*/--vaadin-padding-*token scale, when to fall back to a scoped CSS class, CSS class naming,Scrollerinstead ofoverflow: auto.CLAUDE.md— the Aura-not-Lumo theming rules and the project's overall orientation.
Where this skill and those documents disagree, those documents win. This skill carries no
bundled layout references; the upstream references/layouts-*.md files were deliberately not
copied into this repo (see Provenance).
5. Implement
- Use Vaadin components, not generic HTML; prefer the component API over the element/style API
(e.g.
textField.setReadOnly(true), not.getElement().setAttribute("readonly", "")) - Apply theme variants via Java API (
addThemeVariants) - Use the layout patterns from
docs/theming-layouts.md(Vaadin layout Java APIs first; scoped, role-named CSS classes with--vaadin-*/--aura-*tokens for the rest) - Pick correct heading levels from text styles
- Add accessibility attributes where needed (e.g.
setAriaLabelon icon-only buttons)
If architecture: composed-components — split the view into a container plus reusable
sub-components (e.g. a details/edit form as its own class). Sub-components fire custom
ComponentEvents (e.g. SaveEvent, CancelEvent) that the container listens for and acts on,
rather than the container reaching into the sub-component's fields directly.
If sample-data: generate-sample:
- Define it in a
privatehelper method (e.g.createSampleOrders()) - 3–5 items max, or enough to match what the design visually shows (e.g. a scrolling grid) if that density is core to the layout
- Realistic values (
"Alice Johnson", not"Item 1") - Add
// Sample data — replace with real service callcomment - Prefer
List.of(...)for immutable collections
If sample-data: use-existing-data, wire the view to the existing repository/service/entity
instead of inventing new sample data.
5b. Conform to the design spec
Where the project keeps a design spec, it is the contract for anything this step styles:
take tokens and states from the component's file rather than choosing values. A difference
is a bug in this code. If a component you are building has no spec, or the design has
moved, run figma-survey — it owns the spec. Do not write or edit a spec file to match
what you just built.
Done when every component this step styled matches its spec, or the mismatch is reported.
6. Test
This skill's own job — writing code — is done by the end of Step 5. Don't run terminal
commands, open a browser, or take screenshots yourself; what happens next depends on the
verification preference resolved in Step 4:
skip— stop here.verify— invoke thefigma-visual-verificationskill, passing it the Figma URL (orfileKey/nodeId) used for this view and the route it was implemented at. Present its prioritized findings to the user as-is; don't act on them yet.verify-and-fix— invokefigma-visual-verificationthe same way, then apply exactly one round of fixes addressing its findings, highest severity first. Tell the user what was changed and why. Don't loop back into a second verification pass automatically — if the user wants to confirm the fixes, that's a new verification run.
.agent-context pins verification: verify for this project — report only, never auto-fix.
Universal component patterns
These apply regardless of the styling approach.
// ✅ Component API over element/style API
textField.setReadOnly(true);
button.addThemeVariants(ButtonVariant.TERTIARY); // not LUMO_TERTIARY — see Project overrides
iconButton.setAriaLabel("Close");
input.setLabel("Label"); // HasLabel API, not a separate Span
// ✅ Sizing via component API
layout.setSizeFull();
layout.setWidth("600px");
// ❌ Never use the style API for things the component API handles
textField.getElement().setAttribute("readonly", "");
button.getElement().getStyle().set("background", "transparent");
layout.getStyle().set("width", "600px");
avatar.getStyle().set("--vaadin-avatar-size", "48px");
Gotchas
VerticalLayout defaults:
- Padding ON — call
setPadding(false)if not wanted - Width 100% of parent
alignItemsSTART — children do not stretch horizontally; callsetAlignItems(STRETCH)orsetWidthFull()per child to fill the widthjustifyContentModecontrols the vertical (main) axis
HorizontalLayout defaults:
-
Padding OFF
-
Width shrinks to content — call
setWidthFull()if it should fill the parent -
alignItemsSTRETCH — children stretch vertically to fill the layout height (aButtonnext to aTextFieldwill silently grow) -
justifyContentModecontrols the horizontal (main) axis -
A layout child's minimum size defaults to its content size; this causes unexpected scrollbars in
Scroller/TabSheet; fix withcomponent.setMinWidth("0")orsetMinHeight("0") -
For purely visual containers prefer
FlexLayout— it avoids all of the above defaults -
flex-shrinkis on by default — a fixed-size child shrinks when placed next to asetWidthFull()sibling; calllayout.setFlexShrink(component, 0)to prevent it, or uselayout.setFlexGrow(fullSizeComponent, 1)instead ofsetWidthFull()to avoid the conflict altogether -
setWidthFull()on a child in a content-huggingHorizontalLayoutexpands the layout rather than fitting it; usesetAlignItems(STRETCH)instead -
A layout child's minimum size defaults to its content size; this causes unexpected scrollbars in
Scroller/TabSheet; fix withcomponent.setMinWidth("0")orsetMinHeight("0"). The same default also applies one level up: a component likeMasterDetailLayoutorScrollerplaced as theexpand()ed child of aVerticalLayout(or a CSS Grid area) can resist shrinking below its content's natural height even withsetSizeFull(). If a view overflows the page instead of scrolling internally, addsetMinHeight("0")to that expanded child itself, not just to aScrollernested further inside it -
RadioButtonGroup/CheckboxGroupdefault orientation is theme-dependent: horizontal in Lumo, vertical in Aura. If the Figma layer is named/laid out horizontally and the project uses Aura, addaddThemeVariants(RadioGroupVariant.AURA_HORIZONTAL)/CheckboxGroupVariant.AURA_HORIZONTAL— otherwise the group silently renders as a vertical stack -
Feature-flag status changes between versions — don't assume a component needs one from memory; check
search_vaadin_docs("feature flags")thenget_full_documenton the result -
Never use CSS
marginto space out a Vaadin layout component from its container — margin sits outside the component's measured box, which breakssetSizeFull()/expand()height math (a component can measure "correct" while still visually overflowing its parent). Add spacing instead via padding on a wrapping layout, or by targeting the component's own shadow-DOM part with::part(...)(e.g.vaadin-master-detail-layout::part(detail) { padding: ...; }) -
When writing custom CSS, use real theme CSS custom properties — look them up with the Vaadin MCP (
get_theme_css_properties) rather than inventing a plausible-sounding variable name with a hardcodedvar(--name, fallback)fallback. If the name doesn't actually exist, the fallback silently becomes the real value and never tracks the theme (e.g.var(--vaadin-background-color-secondary, #f9fafb)— that property doesn't exist; the real one is--vaadin-background-container) -
VerticalLayout/HorizontalLayout/FlexLayoutalready setbox-sizing: border-boxthemselves, so padding on them is safe by default. Only plain elements — a custom CSS rule targeting aDiv, another non-layout component, or a shadow-DOM::part(...)— needbox-sizing: border-boxadded explicitly when the rule also setspadding; without it, padding adds to the element's declared width/height instead of being carved out of it, so a component sized withsetWidth()/setSizeFull()ends up visually larger than intended
Quick reference: Figma → Vaadin
| Figma | Vaadin |
|---|---|
| Vertical auto layout | VerticalLayout |
| Horizontal auto layout | HorizontalLayout |
| Free / absolute layout | FlexLayout |
| Form / labelled fields | FormLayout |
| Master-detail | MasterDetailLayout |
| Button | Button |
| Text Field | TextField |
| Grid / Table | Grid |
| Avatar | Avatar |
| Card | Card (v24.8+) |
| Badge / status label | Badge |
| Text layer | com.vaadin.flow.component.html.Span |
| Heading 3 | com.vaadin.flow.component.html.H3 |
Provenance
- Upstream: https://github.com/juuso-vaadin/figma-to-vaadin-skill
- Source path:
skills/figma-to-vaadin/SKILL.md - Commit:
3a9289c(3a9289c15df9e7a7659f0d92fee204ad1dc65c14) - Copied: 2026-08-26 — by hand, as a project-owned file. Not managed by
skills.sh/skills-lock.json; that lock file is CLI-managed againstmattpocock/skillswith per-entry hashes, and this skill is locally modified. - Locally modified: yes
- The three
references/layouts-*.mdfiles were not copied.layouts-lumo-utility.mdandlayouts-tailwind.mddescribe approachesdocs/theming-layouts.mdforbids outright;layouts-vaadin-css.mdis a near-duplicate of that document, and a near-duplicate is where divergence hides. - Step 4's layout-approach mapping now reads
docs/theming-layouts.mdandCLAUDE.mdas the binding authority, and treats the four preferences as already resolved in.agent-context. - Added the Project overrides section (no
LumoUtility, noLUMO_*variants, no--lumo-*properties, no AuraNpctscale, onestyles.css, Vaadin docs tools from thevaadin-skillsplugin). ButtonVariant.LUMO_TERTIARYin the universal-patterns example replaced withButtonVariant.TERTIARY; theAvatarVariant.LUMO_LARGEline dropped (no verifiable theme-agnostic equivalent in the 25.2 docs).- Step 6 now invokes
figma-visual-verification(this repo's renamed copy of upstream'svaadin-visual-verification). compatibility:no longer claims a Vaadin MCP server is required.- Added Step 5b — Conform to the design spec, since upstream has no design-spec
concept at all: it ends at code plus verification, which is why the same "is this a
card?" question can be answered differently by every view. This project authors the
spec in
figma-surveyand treats it as a contract, so implementation conforms and never edits it.
- The three
- Not copied at all: upstream's
figma-to-lumo-theme— this app is Aura, andCLAUDE.mdforbids--lumo-*.