This document provides comprehensive, step-by-step guidelines for creating new web components in the Vaadin web-components monorepo. These guidelines are designed to be thorough enough for automatic component generation.
- Overview & Prerequisites
- Naming Conventions
- File Structure
- Component Implementation
- Styling
- Theming
- TypeScript Definitions
- Testing
- Documentation
- Accessibility
- Package Configuration
- Common Patterns
- Checklist
- Lit 3: Base class and templating
- TypeScript 5: Type definitions
- Lerna + Yarn Workspaces: Monorepo management
- Web Test Runner: Testing infrastructure
- Mocha, Chai, Sinon: Test framework
- Web Components fundamentals (Shadow DOM, Custom Elements)
- Lit basics (templates, properties, lifecycle)
- TypeScript basics (types, interfaces, generics)
- CSS custom properties
- Accessibility best practices
New components should use the standard Vaadin mixin pattern, which includes:
✅ Required Mixins:
PolylitMixin- required for proper integration with ThemableMixin and other Vaadin mixinsLumoInjectionMixin- required for Lumo theme auto-injectionThemableMixin- provides theme attribute handlingElementMixin- provides base element functionality
✅ Use Lit Patterns:
- Lit's native
static propertieswithreflect,attribute(useattribute: falsefor internal properties) - Lit lifecycle methods:
firstUpdated(),updated(),connectedCallback() - Field initializers for default values
updated()for reacting to property changes
❌ Avoid Polymer-style property options:
value- use field initializers insteadobserver- useupdated()method insteadsync,notify,computed- use Lit patterns
Important Notes:
- Both PolylitMixin and LumoInjectionMixin are required for proper theme support and Vaadin mixin compatibility
- PolylitMixin provides compatibility with ThemableMixin and other Vaadin infrastructure
- All Vaadin components use this pattern:
PolylitMixin(LumoInjectionMixin(LitElement))
- Pattern:
vaadin-{component-name} - Examples:
vaadin-button,vaadin-text-field,vaadin-date-picker - Use kebab-case (lowercase with hyphens)
- Must start with
vaadin-prefix
- Pattern:
@vaadin/{component-name} - Example:
@vaadin/button - No
vaadin-prefix in package name
- Main element:
vaadin-{name}.js - Mixin:
vaadin-{name}-mixin.js - Styles:
vaadin-{name}-base-styles.js - Tests:
{name}.test.tsor{name}.test.js
- Element class: PascalCase of component name
vaadin-button→Buttonvaadin-date-picker→DatePicker
- Mixin:
{ComponentName}Mixin- Example:
ButtonMixin,DatePickerMixin
- Example:
Complete file structure for a new component:
packages/{component-name}/
├── package.json # Package configuration
├── README.md # Component documentation
├── LICENSE # Apache 2.0 or Commercial
├── screenshot.png # Optional: Component screenshot
├── vaadin-{name}.js # Root export (re-exports from src/)
├── vaadin-{name}.d.ts # Root TypeScript definition
├── src/
│ ├── vaadin-{name}.js # Main element class
│ ├── vaadin-{name}.d.ts # Element TypeScript definition
│ ├── vaadin-{name}-mixin.js # Component logic mixin (if needed)
│ ├── vaadin-{name}-mixin.d.ts # Mixin TypeScript definition
│ └── styles/
│ ├── vaadin-{name}-base-styles.js # Base styles (CSS-in-JS)
│ └── vaadin-{name}-base-styles.d.ts # Styles TypeScript definition
└── test/
├── {name}.test.ts # Unit tests
├── typings/
│ └── {name}.types.ts # TypeScript type tests
├── dom/
│ ├── {name}.test.js # DOM snapshot tests
│ └── __snapshots__/
│ └── {name}.test.snap.js # Generated snapshots
└── visual/
├── lumo/
│ └── {name}.test.js # Visual tests for Lumo theme
├── aura/
│ └── {name}.test.js # Visual tests for Aura theme
└── base/
└── {name}.test.js # Visual tests without theme
Note on CSS Files: The structure above shows the base styles location within the component package. Additionally, you must create Lumo theme CSS files in packages/vaadin-lumo-styles/ and Aura theme CSS files in packages/aura/. See the Theming section for complete details on all three CSS file locations.
Development pages (dev pages) are HTML files used for manual testing and demonstration during component development. They are stored in the root-level dev/ directory, not within individual component packages.
Location:
dev/
├── {name}.html # Dev page for the component
├── common.js # Shared imports and utilities
└── ... # Other component dev pages
Dev Page Structure:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{Component Name}</title>
<script type="module" src="./common.js"></script>
<script type="module">
import '@vaadin/{component-name}';
// Import other dependencies as needed
</script>
</head>
<body>
<!-- Component example -->
<vaadin-{name}>Example</vaadin-{name}>
</body>
</html>Purpose:
- Manual testing during development
- Visual verification of component appearance and behavior
- Quick prototyping and experimentation
- Theme testing (switching between Lumo/Aura)
- Demonstration of component features and variants
Best Practices:
- Keep dev pages simple and focused on the component
- Use
common.jsfor shared theme switching and utilities - Include examples of all major variants and states
- Add descriptive headings to organize examples
- Dev pages are not part of the published package
This file re-exports everything from the main element file:
import './src/vaadin-{name}.js';
export * from './src/vaadin-{name}.js';Example (vaadin-button.js):
import './src/vaadin-button.js';
export * from './src/vaadin-button.js';This is the core component file. Follow this exact structure:
/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { html, LitElement } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { {componentName}Styles } from './styles/vaadin-{name}-base-styles.js';
import { {ComponentName}Mixin } from './vaadin-{name}-mixin.js';
/**
* `<vaadin-{name}>` is a [brief description of what the component does].
*
* ```html
* <vaadin-{name}>Example</vaadin-{name}>
* ```
*
* ### Styling
*
* The following shadow DOM parts are available for styling:
*
* Part name | Description
* ----------|-------------
* `label` | The label element
* `{part}` | Description of part
*
* The following state attributes are available for styling:
*
* Attribute | Description
* -------------|-------------
* `disabled` | Set when the element is disabled
* `focused` | Set when the element is focused
* `focus-ring` | Set when the element is keyboard focused
* `has-value` | Set when the element has a value
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
* @fires {Event} change - Fired when the value changes.
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
*
* @customElement vaadin-{name}
* @extends HTMLElement
* @mixes {ComponentName}Mixin
* @mixes ElementMixin
* @mixes ThemableMixin
*/
class {ComponentName} extends {ComponentName}Mixin(ElementMixin(ThemableMixin(LumoInjectionMixin(LitElement)))) {
static get is() {
return 'vaadin-{name}';
}
static get styles() {
return {componentName}Styles;
}
static get properties() {
return {
/**
* Property description.
* Can span multiple lines.
*
* @attr {boolean} disabled
*/
disabled: {
type: Boolean,
reflect: true,
},
/**
* The value of the component.
*/
value: {
type: String,
},
};
}
/** @protected */
render() {
return html`
<div class="vaadin-{name}-container">
<span part="label">
<slot></slot>
</span>
</div>
`;
}
/** @protected */
firstUpdated() {
super.firstUpdated();
// Set default role if not provided
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button');
}
}
/** @protected */
updated(changedProperties) {
super.updated(changedProperties);
// React to property changes
if (changedProperties.has('disabled')) {
this._onDisabledChanged(this.disabled, changedProperties.get('disabled'));
}
}
/** @private */
_onDisabledChanged(disabled, oldDisabled) {
// Handle disabled state change
if (disabled) {
this.setAttribute('tabindex', '-1');
this.setAttribute('aria-disabled', 'true');
} else {
this.setAttribute('tabindex', '0');
this.removeAttribute('aria-disabled');
}
}
}
defineCustomElement({ComponentName});
export { {ComponentName} };Use mixins to encapsulate component logic, especially when:
- Logic is shared across multiple components
- Component needs complex functionality
- You want to separate concerns
/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { ActiveMixin } from '@vaadin/a11y-base/src/active-mixin.js';
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
import { TabindexMixin } from '@vaadin/a11y-base/src/tabindex-mixin.js';
/**
* A mixin providing common {component} functionality.
*
* @polymerMixin
* @mixes ActiveMixin
* @mixes FocusMixin
* @mixes TabindexMixin
*/
export const {ComponentName}Mixin = (superClass) =>
class {ComponentName}MixinClass extends ActiveMixin(TabindexMixin(FocusMixin(superClass))) {
constructor() {
super();
// Bind event handlers
this.__onSomeEvent = this.__onSomeEvent.bind(this);
// Add event listeners
this.addEventListener('click', this.__onSomeEvent);
}
/**
* Override lifecycle methods as needed.
*
* @protected
*/
firstUpdated() {
super.firstUpdated();
// Set default role if not provided
if (!this.hasAttribute('role')) {
this.setAttribute('role', '{appropriate-role}');
}
}
/**
* React to property changes.
*
* @protected
*/
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('someProperty')) {
this._onSomePropertyChanged(this.someProperty);
}
}
/**
* Private or protected methods.
*
* @private
*/
__onSomeEvent(event) {
// Implementation
}
};The order of mixins matters! Use this exact chain for new components:
class Component extends
ComponentMixin( // Component-specific logic (outermost)
ElementMixin( // Element base functionality
ThemableMixin( // Theming support
PolylitMixin( // Polymer compatibility (REQUIRED)
LumoInjectionMixin( // Lumo theme auto-injection (REQUIRED)
LitElement // Lit base class (innermost)
)
)
)
)
)For field components, add field-specific mixins:
class DatePicker extends
DatePickerMixin( // Component-specific logic
InputControlMixin( // Input control functionality
ElementMixin(
ThemableMixin(
PolylitMixin( // Polymer compatibility (REQUIRED)
LumoInjectionMixin( // Lumo theme auto-injection (REQUIRED)
LitElement
)
)
)
)
)
)Important Notes:
- Both PolylitMixin and LumoInjectionMixin are required - PolylitMixin provides compatibility with ThemableMixin and Vaadin infrastructure
- PolylitMixin must come BEFORE LumoInjectionMixin in the chain
- All Vaadin components use this pattern for proper theme support and mixin integration
Properties follow the standard Lit pattern:
static get properties() {
return {
/**
* Property description goes here.
* Can be multi-line.
*
* @attr {string} my-property
*/
myProperty: {
type: String, // Type: String, Number, Boolean, Array, Object
reflect: true, // Sync property to attribute
attribute: 'my-property', // Custom attribute name (optional, defaults to kebab-case)
},
/**
* Internal property (not reflected).
*/
_internalProp: {
type: Boolean,
attribute: false, // Disable attribute (not registered in observedAttributes)
},
};
}Property Configuration Options:
type: Constructor (String, Number, Boolean, Array, Object)reflect: Boolean - sync property value to attributeattribute: String | false - custom attribute name, or false to disable attribute (use false for internal properties)converter: Object | Function - custom converter for attribute/property conversionhasChanged: Function - custom comparison function
Note: While Lit supports state: true for internal reactive properties, the Vaadin codebase typically uses attribute: false instead to prevent properties from being registered in observedAttributes.
Setting Default Values:
class MyComponent extends ... {
// Use field initializers for defaults
myProperty = '';
count = 0;
items = [];
// For complex defaults that need to be unique per instance
constructor() {
super();
this.complexObject = { key: 'value' };
}
}Reacting to Property Changes:
Use Lit's updated() lifecycle method instead of observers:
/** @protected */
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('myProperty')) {
this._onMyPropertyChanged(this.myProperty, changedProperties.get('myProperty'));
}
if (changedProperties.has('value') || changedProperties.has('disabled')) {
this._updateState();
}
}
/** @private */
_onMyPropertyChanged(newValue, oldValue) {
// React to property change
console.log(`myProperty changed from ${oldValue} to ${newValue}`);
}Standard Events:
// Fire native event
this.dispatchEvent(new Event('change', { bubbles: true }));
// Fire custom event with detail
this.dispatchEvent(
new CustomEvent('value-changed', {
detail: { value: this.value },
bubbles: true,
composed: true,
}),
);Document events in JSDoc:
The @fires tag on the class-level JSDoc is used for documentation purposes:
/**
* @fires {Event} input - Fired when the value is changed by the user.
* @fires {Event} change - Fired when the user commits a value change.
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
* @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
*/Register events for the Polymer Analyzer with @event:
The Polymer Analyzer (used to generate web-types.json, which is required for React wrapper
generation) discovers events from two sources:
notify: trueon a property declaration automatically generates a{property}-changedevent (e.g.,valuewithnotify: truegeneratesvalue-changed). No@eventannotation is needed for these.@eventJSDoc annotations inside the class body (component class or mixin) are needed for all other events. Without@event, the event won't appear in web-types even if@firesis present on the class JSDoc.
Note: @fires on the class-level JSDoc is not read by the Polymer Analyzer.
class MyComponentMixinClass extends ... {
// ... methods that dispatch events ...
/**
* Fired when the user commits a value change.
* @event change
*/
};Lit provides several lifecycle methods. Use these for initialization and updates:
Key Lifecycle Methods:
class MyComponent extends ... {
/**
* Called whenever the component is connected to the DOM.
* Use for setting up event listeners, etc.
*/
connectedCallback() {
super.connectedCallback();
window.addEventListener('resize', this._onResize);
}
/**
* Called whenever the component is disconnected from the DOM.
* Use for cleanup (removing event listeners, etc.)
*/
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('resize', this._onResize);
}
/**
* Called after the first render.
* Use for initialization that depends on the DOM being rendered.
*/
firstUpdated() {
super.firstUpdated();
// Set ARIA role if not provided
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button');
}
}
/**
* Called after every render when properties have changed.
* Use for reacting to property changes.
*/
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('disabled')) {
this._updateDisabled();
}
}
}Lifecycle Order:
constructor()connectedCallback()- when added to the DOMfirstUpdated()- after first renderupdated()- after every renderdisconnectedCallback()- when removed from DOM
Use controllers for reusable behaviors:
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
class MyComponent extends ... {
/** @protected */
firstUpdated() {
super.firstUpdated();
// Tooltip support
this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);
// Slot observation
this._slotController = new SlotController(
this,
'prefix',
'vaadin-icon',
(node) => {
// Factory function for creating default node
const icon = document.createElement('vaadin-icon');
icon.setAttribute('icon', 'lumo:dropdown');
return icon;
}
);
this.addController(this._slotController);
}
}Available Controllers:
TooltipController: Manage slotted tooltipsSlotController: Observe and manage slotsSlotChildObserveController: Observe slot childrenMediaQueryController: React to media query changesOverflowController: Detect overflow
Component base styles using Lit's css template:
/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import '@vaadin/component-base/src/styles/style-props.js';
import { css } from 'lit';
export const {componentName}Styles = css`
:host {
/* Layout */
display: inline-flex;
align-items: center;
box-sizing: border-box;
/* Sizing */
padding: var(--vaadin-{name}-padding, var(--vaadin-padding-m));
gap: var(--vaadin-{name}-gap, var(--vaadin-gap-s));
/* Typography */
font-family: var(--vaadin-{name}-font-family, inherit);
font-size: var(--vaadin-{name}-font-size, inherit);
font-weight: var(--vaadin-{name}-font-weight, 500);
line-height: var(--vaadin-{name}-line-height, inherit);
/* Colors */
color: var(--vaadin-{name}-text-color, var(--vaadin-text-color));
background: var(--vaadin-{name}-background, var(--vaadin-background-container));
/* Border */
border: var(--vaadin-{name}-border-width, 1px) solid
var(--vaadin-{name}-border-color, var(--vaadin-border-color-secondary));
border-radius: var(--vaadin-{name}-border-radius, var(--vaadin-radius-m));
/* Interaction */
cursor: var(--vaadin-clickable-cursor);
user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
:host([hidden]) {
display: none !important;
}
/* State: focused */
:host(:is([focus-ring], :focus-visible)) {
outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
outline-offset: 1px;
}
/* State: disabled */
:host([disabled]) {
pointer-events: none;
cursor: var(--vaadin-disabled-cursor);
opacity: 0.5;
}
/* Theme variants */
:host([theme~='primary']) {
--vaadin-{name}-background: var(--vaadin-text-color);
--vaadin-{name}-text-color: var(--vaadin-background-color);
--vaadin-{name}-border-color: transparent;
}
`;CSS Custom Properties Naming:
- Use
--vaadin-{component}-{property}pattern - Provide fallbacks to shared design tokens:
var(--vaadin-{component}-color, var(--vaadin-text-color)) - Common token categories:
- Colors:
--vaadin-text-color,--vaadin-background-color - Spacing:
--vaadin-padding-{size},--vaadin-gap-{size} - Borders:
--vaadin-border-color-{variant},--vaadin-radius-{size} - Typography:
--vaadin-font-size-{size},--vaadin-font-weight-{level}
- Colors:
Shadow Parts:
- Use descriptive part names:
label,input-field,prefix,suffix - Document all parts in JSDoc
State Attributes:
- Use attributes for state:
disabled,focused,focus-ring,has-value,invalid - Always document state attributes
Components must support both Lumo and Aura themes. The Vaadin component library includes two official themes with different design philosophies:
- Lumo: The original Vaadin theme, optimized for business applications with a focus on clarity and efficiency
- Aura: A modern theme with contemporary aesthetics, using advanced CSS features and design patterns
IMPORTANT: Components require CSS files in three separate locations:
Location: packages/{component-name}/src/styles/vaadin-{name}-base-styles.js
These are the unstyled, functional base styles that every component needs regardless of theme. They define:
- Basic layout and structure
- CSS custom properties with fallbacks
- Shadow part styling
- State attributes (
:host([disabled]),:host([focus-ring]), etc.) - Forced colors mode support
Example: For vaadin-button, create:
packages/button/src/styles/vaadin-button-base-styles.js
This file is imported directly in the component's main class via the static get styles() method.
Location:
- Public CSS:
packages/vaadin-lumo-styles/components/{name}.css - Implementation CSS:
packages/vaadin-lumo-styles/src/components/{name}.css
These styles provide the Lumo theme appearance for the component. They override and extend the base styles with Lumo-specific design tokens and styling.
Example: For vaadin-button, create:
packages/vaadin-lumo-styles/components/button.css (public, with injection markers)
packages/vaadin-lumo-styles/src/components/button.css (actual Lumo styles)
Location: packages/aura/src/components/{name}.css
These styles provide the Aura theme appearance for the component. They use modern CSS features and override base styles with Aura-specific design tokens.
Example: For vaadin-button, create:
packages/aura/src/components/button.css
The Aura component file must also be imported in packages/aura/aura.css to be included in the theme bundle.
Component Package (packages/button/)
└── src/styles/vaadin-button-base-styles.js ← Base functional styles (CSS-in-JS)
↓ Referenced in component class
Lumo Theme Package (packages/vaadin-lumo-styles/)
├── components/button.css ← Public entry with injection markers
└── src/components/button.css ← Lumo theme styles (actual CSS)
↓ Auto-injected via LumoInjectionMixin
Aura Theme Package (packages/aura/)
└── src/components/button.css ← Aura theme styles
↓ Imported in aura.css
Key Points:
- Base styles = In component package (CSS-in-JS using Lit's
csstemplate) - Lumo styles = In
vaadin-lumo-stylespackage (CSS files) - Aura styles = In
aurapackage (CSS files) - Each serves a different purpose and must all be created for a complete component
File Structure:
packages/vaadin-lumo-styles/
├── components/
│ └── {name}.css # Public CSS (imports from src/)
└── src/
└── components/
└── {name}.css # Actual theme styles
Public CSS file (components/{name}.css):
/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
@import '../src/components/{name}.css';
:root,
:host {
--_lumo-vaadin-{name}-inject: 1;
--_lumo-vaadin-{name}-inject-modules: lumo_components_{name};
}Note: The injection markers (--_lumo-vaadin-{name}-inject and --_lumo-vaadin-{name}-inject-modules) are required for the Lumo theme injection system to work properly. The module name in --_lumo-vaadin-{name}-inject-modules must match the @media query name in the source CSS file.
Theme styles (src/components/{name}.css):
/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
@media lumo_components_{name} {
:host {
/* Sizing */
padding: var(--vaadin-{name}-padding, var(--lumo-space-m));
gap: var(--vaadin-{name}-gap, var(--lumo-space-s));
/* Style */
font-family: var(--lumo-font-family);
font-size: var(--vaadin-{name}-font-size, var(--lumo-font-size-m));
font-weight: var(--vaadin-{name}-font-weight, 500);
color: var(--vaadin-{name}-text-color, var(--lumo-primary-text-color));
background: var(--vaadin--{name}-background, var(--lumo-contrast-5pct));
border: var(--vaadin-{name}-border, none);
border-radius: var(--vaadin-{name}-border-radius, var(--lumo-border-radius-m));
}
/* Keyboard focus */
:host([focus-ring]) {
box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
}
/* Disabled */
:host([disabled]) {
color: var(--lumo-disabled-text-color);
background: var(--lumo-contrast-5pct);
}
/* Theme variants */
:host([theme~='primary']) {
background: var(--lumo-primary-color);
color: var(--lumo-primary-contrast-color);
font-weight: 600;
}
}File Structure:
packages/aura/
├── aura.css # Main theme entry point
├── package.json # Package configuration
├── postcss.config.js # PostCSS configuration
└── src/
├── color.css # Color system
├── color-scheme.css # Light/dark mode support
├── palette.css # Color palette
├── shadow.css # Shadow tokens
├── size.css # Size tokens
├── surface.css # Surface system
├── typography.css # Typography tokens
└── components/
└── {name}.css # Component-specific styles
Important Notes about Aura:
- Aura is a modern theme using cutting-edge CSS features
- Requires browser support for:
:where,:is,light-dark(),oklch(),color-mix(), relative color syntax - Uses PostCSS for build processing
- Component styles are imported in
aura.cssmain file - Uses element selectors (
:is(vaadin-{name})) instead of:hostfor better cascade control
Aura Theme CSS Example:
/* Aura uses modern CSS features and advanced color manipulation */
:where(:root),
:where(:host) {
--vaadin-{name}-shadow: var(--aura-shadow-xs);
}
/* Use :is() for better selector specificity control */
:is(vaadin-{name}) {
box-shadow: var(--vaadin-{name}-shadow);
}
/* Default variant styling */
:is(vaadin-{name}):not([theme~='primary']) {
--vaadin-{name}-text-color: var(--aura-accent-text-color);
--vaadin-{name}-border-color: var(--aura-accent-border-color);
}
/* Primary variant with :where() for lower specificity */
:is(vaadin-{name}):where([theme~='primary']) {
outline-offset: 2px;
--vaadin-{name}-font-weight: var(--aura-font-weight-semibold);
--vaadin-{name}-text-color: var(--aura-accent-contrast-color);
--vaadin-{name}-background: var(--aura-accent-color);
--vaadin-{name}-shadow: var(--aura-shadow-s);
}
/* Hover state */
@media (any-hover: hover) {
:is(vaadin-{name}):hover:not([disabled])::before {
opacity: 0.03;
}
:is(vaadin-{name})[theme~='primary']:hover:not([disabled])::before {
opacity: 0.12;
}
}Aura Design System Tokens:
Aura provides a comprehensive set of CSS custom properties:
Colors:
--aura-accent-color: Primary accent color--aura-accent-text-color: Text color for accent elements--aura-accent-contrast-color: Contrast color for accent backgrounds--aura-accent-border-color: Border color for accent elements--aura-accent-surface: Surface color with accent tint--aura-neutral-light/--aura-neutral-dark: Neutral color palette--aura-background-color: Background color (light-dark aware)
Typography:
--aura-font-family: Default font family--aura-font-weight-normal: 400--aura-font-weight-medium: 500--aura-font-weight-semibold: 600--aura-font-weight-bold: 700
Surface System:
--aura-surface-level: Surface elevation level (0-10)--aura-surface-opacity: Surface opacity
Sizing:
- Uses the same
--vaadin-size-*and--vaadin-padding-*tokens as Lumo
Best Practices for Aura:
- Use
:is()for component selectors to maintain consistent specificity - Use
:where()for variants to allow easier overriding - Leverage
light-dark()for automatic dark mode support - Use
oklch()color space for better color manipulation - Use relative color syntax (
from currentColor) for dynamic color adjustments - Always check browser support for modern CSS features
- Use
@supportsqueries for progressive enhancement
/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';
import type { ActiveMixinClass } from '@vaadin/a11y-base/src/active-mixin.js';
import type { FocusMixinClass } from '@vaadin/a11y-base/src/focus-mixin.js';
import type { TabindexMixinClass } from '@vaadin/a11y-base/src/tabindex-mixin.js';
/**
* A mixin providing common {component} functionality.
*/
export declare function {ComponentName}Mixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<ActiveMixinClass> &
Constructor<FocusMixinClass> &
Constructor<TabindexMixinClass> &
Constructor<{ComponentName}MixinClass> &
T;
/**
* Additional interface members for the mixin (if needed).
*/
export declare class {ComponentName}MixinClass {
// Add any public methods/properties from the mixin
}/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { {ComponentName}Mixin } from './vaadin-{name}-mixin.js';
/**
* Fired when the value changes.
*/
export type {ComponentName}ChangeEvent = Event & {
target: {ComponentName};
};
/**
* Fired when the `value` property changes.
*/
export type {ComponentName}ValueChangedEvent = CustomEvent<{ value: string }>;
/**
* Interface for event map (for TypeScript users).
*/
export interface {ComponentName}EventMap {
change: {ComponentName}ChangeEvent;
'value-changed': {ComponentName}ValueChangedEvent;
}
/**
* Interface for custom events (for TypeScript users).
*/
export interface {ComponentName}CustomEventMap {
'value-changed': {ComponentName}ValueChangedEvent;
}
/**
* `<vaadin-{name}>` is a [detailed description of what the component does].
*
* ```html
* <vaadin-{name}>Example</vaadin-{name}>
* ```
*
* ### Styling
*
* The following shadow DOM parts are available for styling:
*
* Part name | Description
* ----------|-------------
* `label` | The label element
*
* The following state attributes are available for styling:
*
* Attribute | Description
* -------------|-------------
* `disabled` | Set when the element is disabled
* `focused` | Set when the element is focused
*
* The following custom CSS properties are available for styling:
*
* Custom CSS property |
* :---------------------------------|
* | `--vaadin-{name}-background` |
* | `--vaadin-{name}-border-color` |
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*/
declare class {ComponentName} extends {ComponentName}Mixin(ElementMixin(ThemableMixin(HTMLElement))) {
/**
* Property description.
*/
someProperty: string;
addEventListener<K extends keyof {ComponentName}EventMap>(
type: K,
listener: (this: {ComponentName}, ev: {ComponentName}EventMap[K]) => void,
options?: AddEventListenerOptions | boolean,
): void;
removeEventListener<K extends keyof {ComponentName}EventMap>(
type: K,
listener: (this: {ComponentName}, ev: {ComponentName}EventMap[K]) => void,
options?: EventListenerOptions | boolean,
): void;
}
declare global {
interface HTMLElementTagNameMap {
'vaadin-{name}': {ComponentName};
}
}
export { {ComponentName} };export * from './src/vaadin-{name}.js';/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { CSSResult } from 'lit';
export declare const {componentName}Styles: CSSResult;import { expect } from '@vaadin/chai-plugins';
import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands';
import { fire, fixtureSync, nextRender, nextUpdate } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import '../vaadin-{name}.js';
import type { {ComponentName} } from '../vaadin-{name}.js';
describe('vaadin-{name}', () => {
let element: {ComponentName};
describe('custom element definition', () => {
let tagName: string;
beforeEach(() => {
element = fixtureSync('<vaadin-{name}></vaadin-{name}>');
tagName = element.tagName.toLowerCase();
});
it('should be defined in custom element registry', () => {
expect(customElements.get(tagName)).to.be.ok;
});
it('should have a valid static "is" getter', () => {
expect((customElements.get(tagName) as any).is).to.equal(tagName);
});
});
describe('properties', () => {
beforeEach(async () => {
element = fixtureSync('<vaadin-{name}></vaadin-{name}>');
await nextRender();
});
it('should have default value', () => {
expect(element.someProperty).to.equal('');
});
it('should reflect property to attribute', async () => {
element.someProperty = 'test';
await nextUpdate(element);
expect(element.getAttribute('some-property')).to.equal('test');
});
});
describe('events', () => {
beforeEach(async () => {
element = fixtureSync('<vaadin-{name}></vaadin-{name}>');
await nextRender();
});
it('should fire change event', async () => {
const spy = sinon.spy();
element.addEventListener('change', spy);
// Trigger change
element.someProperty = 'new-value';
await nextUpdate(element);
expect(spy.calledOnce).to.be.true;
});
it('should fire value-changed custom event with detail', async () => {
const spy = sinon.spy();
element.addEventListener('value-changed', spy);
element.someProperty = 'new-value';
await nextUpdate(element);
expect(spy.calledOnce).to.be.true;
expect(spy.firstCall.args[0].detail.value).to.equal('new-value');
});
});
describe('keyboard', () => {
beforeEach(async () => {
element = fixtureSync('<vaadin-{name}></vaadin-{name}>');
await nextRender();
element.focus();
});
it('should handle Enter key', async () => {
const spy = sinon.spy();
element.addEventListener('click', spy);
await sendKeys({ down: 'Enter' });
expect(spy.calledOnce).to.be.true;
});
});
describe('disabled', () => {
beforeEach(async () => {
element = fixtureSync('<vaadin-{name}></vaadin-{name}>');
await nextRender();
});
it('should set aria-disabled when disabled', async () => {
element.disabled = true;
await nextUpdate(element);
expect(element.getAttribute('aria-disabled')).to.equal('true');
});
it('should not fire events when disabled', async () => {
const spy = sinon.spy();
element.addEventListener('click', spy);
element.disabled = true;
await nextUpdate(element);
element.click();
expect(spy.called).to.be.false;
});
});
describe('accessibility', () => {
beforeEach(async () => {
element = fixtureSync('<vaadin-{name}></vaadin-{name}>');
await nextRender();
});
it('should have default role', () => {
expect(element.getAttribute('role')).to.equal('button');
});
it('should have tabindex', () => {
expect(element.tabIndex).to.equal(0);
});
it('should be focusable', () => {
element.focus();
expect(document.activeElement).to.equal(element);
});
});
});import { expect } from '@vaadin/chai-plugins';
import { sendKeys } from '@vaadin/test-runner-commands';
import { fixtureSync } from '@vaadin/testing-helpers';
import '../../src/vaadin-{name}.js';
describe('vaadin-{name}', () => {
let element;
beforeEach(() => {
element = fixtureSync('<vaadin-{name}>Label</vaadin-{name}>');
});
describe('host', () => {
it('default', async () => {
await expect(element).dom.to.equalSnapshot();
});
it('disabled', async () => {
element.disabled = true;
await expect(element).dom.to.equalSnapshot();
});
it('focused', async () => {
element.focus({ focusVisible: false });
await expect(element).dom.to.equalSnapshot();
});
it('focus-ring', async () => {
await sendKeys({ press: 'Tab' });
await expect(element).dom.to.equalSnapshot();
});
});
describe('shadow', () => {
it('default', async () => {
await expect(element).shadowDom.to.equalSnapshot();
});
});
});import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands';
import { fixtureSync } from '@vaadin/testing-helpers';
import { visualDiff } from '@web/test-runner-visual-regression';
import '../../../src/vaadin-{name}.js';
describe('{name}', () => {
let div, element;
beforeEach(() => {
div = document.createElement('div');
div.style.display = 'inline-block';
div.style.padding = '10px';
element = fixtureSync('<vaadin-{name}>Label</vaadin-{name}>', div);
});
afterEach(async () => {
await resetMouse();
});
describe('basic', () => {
it('basic', async () => {
await visualDiff(div, 'basic');
});
it('disabled', async () => {
element.disabled = true;
await visualDiff(div, 'disabled');
});
it('focus-ring', async () => {
await sendKeys({ press: 'Tab' });
await visualDiff(div, 'focus-ring');
});
});
describe('theme', () => {
it('primary', async () => {
element.setAttribute('theme', 'primary');
await visualDiff(div, 'theme-primary');
});
});
});import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands';
import { fixtureSync } from '@vaadin/testing-helpers';
import { visualDiff } from '@web/test-runner-visual-regression';
import '@vaadin/vaadin-lumo-styles/src/props/index.css';
import '@vaadin/vaadin-lumo-styles/components/{name}.css';
import '../../../vaadin-{name}.js';
describe('{name}', () => {
let div, element;
beforeEach(() => {
div = document.createElement('div');
div.style.display = 'inline-block';
div.style.padding = '10px';
element = fixtureSync('<vaadin-{name}>Label</vaadin-{name}>', div);
});
afterEach(async () => {
await resetMouse();
});
describe('basic', () => {
it('basic', async () => {
await visualDiff(div, 'basic');
});
it('disabled', async () => {
element.disabled = true;
await visualDiff(div, 'disabled');
});
it('focus-ring', async () => {
await sendKeys({ press: 'Tab' });
await visualDiff(div, 'focus-ring');
});
});
describe('theme', () => {
it('primary', async () => {
element.setAttribute('theme', 'primary');
await visualDiff(div, 'theme-primary');
});
it('primary hover', async () => {
element.setAttribute('theme', 'primary');
await sendMouseToElement({ type: 'move', element });
await visualDiff(div, 'theme-primary-hover');
});
});
});import { resetMouse, sendKeys, sendMouseToElement } from '@vaadin/test-runner-commands';
import { fixtureSync } from '@vaadin/testing-helpers';
import { visualDiff } from '@web/test-runner-visual-regression';
import '@vaadin/aura/aura.css';
import '../../../vaadin-{name}.js';
describe('{name}', () => {
let div, element;
beforeEach(() => {
div = document.createElement('div');
div.style.display = 'inline-block';
div.style.padding = '10px';
element = fixtureSync('<vaadin-{name}>Label</vaadin-{name}>', div);
});
afterEach(async () => {
await resetMouse();
});
describe('basic', () => {
it('basic', async () => {
await visualDiff(div, 'basic');
});
it('disabled', async () => {
element.disabled = true;
await visualDiff(div, 'disabled');
});
it('focus-ring', async () => {
await sendKeys({ press: 'Tab' });
await visualDiff(div, 'focus-ring');
});
});
describe('theme', () => {
it('primary', async () => {
element.setAttribute('theme', 'primary');
await visualDiff(div, 'theme-primary');
});
it('primary hover', async () => {
element.setAttribute('theme', 'primary');
await sendMouseToElement({ type: 'move', element });
await visualDiff(div, 'theme-primary-hover');
});
});
});import '../../vaadin-{name}.js';
import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
import type { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import type { {ComponentName}MixinClass } from '../../src/vaadin-{name}-mixin.js';
const assertType = <TExpected>(value: TExpected) => value;
const element = document.createElement('vaadin-{name}');
// Properties
assertType<string>(element.someProperty);
// Mixins
assertType<ElementMixinClass>(element);
assertType<ThemableMixinClass>(element);
assertType<{ComponentName}MixinClass>(element);
// Events
element.addEventListener('change', (event) => {
assertType<Event>(event);
});
element.addEventListener('value-changed', (event) => {
assertType<CustomEvent>(event);
assertType<string>(event.detail.value);
});Ensure these aspects are tested:
Functionality:
- Custom element registration
- Default property values
- Property changes and observers
- Property reflection to attributes
- Event firing (native and custom)
- Method behaviors
- State management
Accessibility:
- Default ARIA role
- ARIA attributes (aria-disabled, aria-label, etc.)
- Keyboard navigation
- Focus management
- Screen reader announcements
Interactions:
- Mouse events (click, hover, mousedown)
- Keyboard events (Enter, Space, Arrow keys, Tab)
- Touch events
- Disabled state prevents interactions
Visual:
- Default appearance (Base styles)
- Default appearance (Lumo)
- Default appearance (Aura)
- Theme variants (primary, tertiary, etc.) - both themes
- State variations (disabled, focused, active, hover) - both themes
- With slotted content (icons, etc.) - both themes
DOM Structure:
- Light DOM snapshots
- Shadow DOM snapshots
- State attribute changes
# @vaadin/{name}
[Brief one-line description of the component].
[Documentation + Live Demo ↗](https://vaadin.com/docs/latest/components/{name})
[](https://www.npmjs.com/package/@vaadin/{name})
\`\`\`html
<vaadin-{name}>Example</vaadin-{name}>
\`\`\`
[<img src="https://raw.githubusercontent.com/vaadin/web-components/main/packages/{name}/screenshot.png" width="296" alt="Screenshot of vaadin-{name}">](https://vaadin.com/docs/latest/components/{name})
## Installation
Install the component:
\`\`\`sh
npm i @vaadin/{name}
\`\`\`
Once installed, import the component in your application:
\`\`\`js
import '@vaadin/{name}';
\`\`\`
## Contributing
Read the [contributing guide](https://vaadin.com/docs/latest/contributing) to learn about our development process, how to propose bugfixes and improvements, and how to test your changes to Vaadin components.
## License
Apache License 2.0
Vaadin collects usage statistics at development time to improve this product.
For details and to opt-out, see https://github.com/vaadin/vaadin-usage-statistics.Class-level documentation:
/**
* `<vaadin-{name}>` is a [detailed description of what the component does].
*
* ```html
* <vaadin-{name}>Example</vaadin-{name}>
* ```
*
* ### Styling
*
* The following shadow DOM parts are available for styling:
*
* Part name | Description
* ----------|-------------
* `label` | The label element
*
* The following state attributes are available for styling:
*
* Attribute | Description
* -------------|-------------
* `disabled` | Set when the element is disabled
* `focused` | Set when the element is focused
*
* The following custom CSS properties are available for styling:
*
* Custom CSS property |
* :---------------------------------|
* | `--vaadin-{name}-background` |
* | `--vaadin-{name}-border-color` |
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
* @fires {Event} change - Fired when the value changes.
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
*
* @customElement vaadin-{name}
* @extends HTMLElement
* @mixes {ComponentName}Mixin
* @mixes ElementMixin
* @mixes ThemableMixin
*/Property documentation:
/**
* The value of the component.
* Can be any valid string.
*
* @attr {string} value
*/Method documentation:
/**
* Validates the component value.
* Returns true if valid, false otherwise.
*
* @return {boolean} True if valid
* @protected
*/Role:
- Set appropriate default role in
firstUpdated():
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button'); // or appropriate role
}Common ARIA attributes:
aria-disabled: Set when disabledaria-label/aria-labelledby: For accessible namearia-describedby: For additional descriptionsaria-expanded: For expandable elementsaria-checked: For checkable elementsaria-selected: For selectable elementsaria-pressed: For toggle buttonsaria-haspopup: For elements that open popups
Common patterns:
Enter/Space: ActivateTab/Shift+Tab: NavigateEscape: Close/Cancel- Arrow keys: Navigate within component
Implementation:
/**
* @param {KeyboardEvent} event
* @protected
* @override
*/
_onKeyDown(event) {
super._onKeyDown(event);
if (event.altKey || event.shiftKey || event.ctrlKey || event.metaKey) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.click();
}
}Focusable:
- Set
tabindex="0"by default (use TabindexMixin) - Make disabled elements non-focusable:
tabindex="-1"
Focus indication:
- Support both
:focusand:focus-visible - Add
focus-ringattribute for keyboard focus - Use FocusMixin for consistent focus behavior
Always sync state to attributes for styling:
static get properties() {
return {
disabled: {
type: Boolean,
reflect: true,
},
};
}{
"name": "@vaadin/{name}",
"version": "25.1.0-alpha0",
"publishConfig": {
"access": "public"
},
"description": "vaadin-{name}",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/vaadin/web-components.git",
"directory": "packages/{name}"
},
"author": "Vaadin Ltd",
"homepage": "https://vaadin.com/components",
"bugs": {
"url": "https://github.com/vaadin/web-components/issues"
},
"main": "vaadin-{name}.js",
"module": "vaadin-{name}.js",
"type": "module",
"files": [
"src",
"vaadin-*.d.ts",
"vaadin-*.js",
"web-types.json",
"web-types.lit.json"
],
"keywords": [
"Vaadin",
"{descriptive-keyword}",
"web-components",
"web-component"
],
"dependencies": {
"@open-wc/dedupe-mixin": "^1.3.0",
"@vaadin/a11y-base": "25.1.0-alpha0",
"@vaadin/component-base": "25.1.0-alpha0",
"@vaadin/vaadin-themable-mixin": "25.1.0-alpha0",
"lit": "^3.0.0"
},
"devDependencies": {
"@vaadin/aura": "25.1.0-alpha0",
"@vaadin/chai-plugins": "25.1.0-alpha0",
"@vaadin/test-runner-commands": "25.1.0-alpha0",
"@vaadin/testing-helpers": "^2.0.0",
"@vaadin/vaadin-lumo-styles": "25.1.0-alpha0",
"sinon": "^21.0.0"
},
"web-types": [
"web-types.json",
"web-types.lit.json"
]
}For Pro components, change license:
{
"license": "SEE LICENSE IN https://vaadin.com/commercial-license-and-service-terms"
}Apache 2.0 (copy from existing component):
Apache License
Version 2.0, January 2004
...
Commercial (for Pro components):
Vaadin Commercial License and Service Terms
See https://vaadin.com/commercial-license-and-service-terms for the full license.
Apache 2.0:
/**
* @license
* Copyright (c) {year} - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/Commercial:
/**
* @license
* Copyright (c) 2000 - 2026 Vaadin Ltd.
*
* This program is available under Vaadin Commercial License and Service Terms.
*
*
* See https://vaadin.com/commercial-license-and-service-terms for the full
* license.
*/Use when:
- Component is primarily interactive (clickable)
- Has simple state (disabled, active, focused)
- No complex data management
Mixins:
- ButtonMixin or ActiveMixin
- FocusMixin
- TabindexMixin
Example structure: See packages/button/
Use when:
- Component accepts user input
- Needs validation
- Part of a form
Mixins:
- InputControlMixin (used by combo-box, date-picker, time-picker, etc.)
- ValidateMixin
- InputConstraintsMixin
Additional requirements:
- Use InputContainer component
- Implement label, helper text, error message slots
- Follow field styling conventions
Example structure: See packages/date-picker/, packages/combo-box/, or packages/time-picker/
Use when:
- Component displays content in overlay
- Needs positioning logic
- Modal or non-modal behavior
Base class:
- Use
OverlayMixinfrompackages/overlay/ - Use
PositionMixinfrompackages/overlay/if needed
Additional requirements:
- Use overlay positioning system
- Implement focus trap
- Handle Escape key
- Manage backdrop
Example structure: See packages/dialog/ or packages/popover/
Use when:
- Component displays list of items
- Supports data provider
- Virtual scrolling needed
Controllers:
- DataProviderController
- Virtualizer (for large lists)
Example structure: See packages/combo-box/ or packages/grid/
Use this checklist when creating a new component:
- Created
packages/{name}/directory - Created root export file:
vaadin-{name}.js - Created root TypeScript definition:
vaadin-{name}.d.ts - Created src directory with main element
- Created mixin file (if needed)
- Created base styles file
- Created TypeScript definitions for all files
- Created test directory structure
- Created package.json
- Created README.md
- Copied LICENSE file
- Element class extends correct mixin chain
-
static get is()returns correct tag name -
static get styles()returns styles -
static get properties()defines all properties -
render()method returns Lit template - Shadow parts named and documented
- State attributes added and documented
- Events fired correctly
- Event types documented in JSDoc
- Controllers initialized in
firstUpdated() -
defineCustomElement()called at end
- Base styles file created in
packages/{component-name}/src/styles/vaadin-{name}-base-styles.js - Base styles TypeScript definition created in
src/styles/vaadin-{name}-base-styles.d.ts - Base styles use CSS custom properties with fallbacks
- All custom properties have fallbacks to design tokens
- Forced colors mode styles added
- State selectors use attributes (
:host([disabled])) - Shadow parts styled
- Base styles imported in component via
static get styles()
- Base CSS: Created in component package at
packages/{component-name}/src/styles/vaadin-{name}-base-styles.js - Lumo CSS: Public file created at
packages/vaadin-lumo-styles/components/{name}.csswith injection markers - Lumo CSS: Implementation file created at
packages/vaadin-lumo-styles/src/components/{name}.css - Lumo styles wrapped in
@media lumo_components_{name}query - Lumo injection markers match media query name
- Aura CSS: Created at
packages/aura/src/components/{name}.css - Aura styles use modern CSS syntax (
:is(),:where(),light-dark(),oklch()) - Aura component CSS imported in
packages/aura/aura.css - Theme variants implemented in both Lumo and Aura
- All three CSS files created (base, Lumo, Aura)
- Both themes tested for visual consistency
- Mixin TypeScript definition created
- Element TypeScript definition created
- Event types defined
- EventMap interface defined
- Global interface augmentation added
- Styles TypeScript definition created
- All exports properly typed
- Unit tests cover functionality
- Unit tests cover properties
- Unit tests cover events
- Unit tests cover keyboard interactions
- Unit tests cover accessibility
- DOM snapshot tests created
- Visual tests for Lumo created in
test/visual/lumo/ - Visual tests for Aura created in
test/visual/aura/ - Visual tests for base styles created in
test/visual/base/ - TypeScript type tests created
- All tests pass
- Visual regression tests pass for base styles and both themes
- README.md completed
- Class JSDoc comment complete
- Shadow parts documented
- State attributes documented
- Custom properties documented
- Events documented with @fires
- Properties documented
- Methods documented
- Default ARIA role set
- ARIA attributes added as needed
- Keyboard navigation implemented
- Focus management correct
- Disabled state handled correctly
- Screen reader tested (if possible)
- package.json has all required fields
- Dependencies correct (runtime vs dev)
- License correct (Apache vs Commercial)
- License headers on all files
- Version matches monorepo version
- Added to Lumo theme package
- Added to Aura theme package
- ESLint passes
- TypeScript compilation passes
- All tests pass
- Visual regression tests pass
-
yarn lintpasses -
yarn lint:typespasses -
yarn test --group {name}passes -
yarn test:snapshots --group {name}passes -
yarn test:base --group {name}passes -
yarn test:lumo --group {name}passes -
yarn test:aura --group {name}passes - Component works in demo/dev environment with both themes
- Documentation reviewed
- Accessibility verified
- Both Lumo and Aura themes properly applied and tested
Core:
@vaadin/component-base: Base mixins and utilities@vaadin/a11y-base: Accessibility mixins@vaadin/vaadin-themable-mixin: Theming system
Field Components:
@vaadin/field-base: Field-specific mixins@vaadin/input-container: Input container component
Other:
@vaadin/overlay: Overlay positioning@vaadin/lit-renderer: Renderer support
lit: v3.0.0 or higher@open-wc/dedupe-mixin: Mixin deduplication
@vaadin/chai-plugins: Chai assertion plugins@vaadin/test-runner-commands: Test utilities@vaadin/testing-helpers: Test helperssinon: Spy/stub/mock library
- Always use both PolylitMixin and LumoInjectionMixin for new components
- Always use the correct mixin order:
ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))) - Use
firstUpdated()andupdated()instead ofready()and Polymer observers - Use field initializers for default values instead of
valueproperty option - Use
reflect: trueinstead ofreflectToAttribute: true
- Always follow existing patterns from similar components
- Never skip accessibility requirements
- Always test in both themes (Lumo and Aura)
- Always verify Aura-specific CSS features are supported by target browsers
- Never commit without running linters and tests
- Always document breaking changes
- Never use
anyin TypeScript definitions - Always provide fallbacks for CSS custom properties
- Never hardcode colors or sizes (use design tokens)
- Always consider both light and dark modes (especially important for Aura)
When using PolylitMixin with Lit patterns (recommended):
value: defaultValue→ Use field initializer:myProp = defaultValue;observer: '_onPropChanged'→ Useupdated()withchangedProperties.has('myProp')reflectToAttribute: true→ Usereflect: truesync: true→ Not needed with Litnotify: true→ Manually fire events in setters orupdated()computed: '_compute(a, b)'→ Useupdated()or gettersready()→ UsefirstUpdated()for one-time initialization
Note: PolylitMixin is still required for Vaadin mixin compatibility, but prefer Lit property patterns over Polymer-style options.
| Aspect | Lumo | Aura |
|---|---|---|
| CSS Approach | @media queries for encapsulation |
Element selectors with :is() |
| Browser Support | Broad (legacy browsers) | Modern browsers only |
| Color System | HSL-based with custom properties | oklch() with relative colors |
| Dark Mode | Separate theme variant | Built-in with light-dark() |
| Specificity | Host-based (:host) |
Element-based (:is()) |
| File Structure | Separate public/src files | Direct component files |
| Design Philosophy | Business applications | Modern, consumer-facing |
Last updated: 2026-01-22
Note: This guide uses the standard Vaadin mixin pattern with both PolylitMixin and LumoInjectionMixin. Both are required for proper theme support and Vaadin infrastructure compatibility.