Shopware Design

Components

Consistency is what lets a library of parts feel like one system. The conventions below apply whenever you add a new component or change an existing one.

Naming and file structure

Every component uses the mt- prefix and kebab-case. The component name, its folder, and its primary file all share the same name.

mt-button/
  mt-button.vue
  mt-button.stories.ts
  mt-button.interactive.stories.ts
  mt-button.spec.ts

Add sub-components/ only when the component uses the compound pattern. Add composables/ when the component extracts logic into composables. Do not create either folder preemptively.

Props

  • Props use camelCase. Plural nouns for arrays (items), singular for objects (item), num prefix or Count/Index suffix for numbers (numColumns, activeIndex).
  • Boolean props use plain adjectives with no prefix: loading, open, disabled. Avoid inverted booleans: showIcon not hideIcon.
  • Avoid native HTML attribute names. Use heading instead of title. disabled, required, and readonly are intentional exceptions.
  • Name props from the component's perspective, not the caller's: hasSubmitButton not hasSubmitPermission.
  • Never use "default" as a named prop value. Set the default via defineProps's default option.
  • Prefer slots over props for content that may contain markup.

Use the standard prop names below consistently. Never substitute alternatives like type, kind, or style.

PropPurposeValues / Type
variantSemantic intent / feedback toneneutral info attention critical positive
appearanceVisual hierarchy for interactive componentsprimary secondary tertiary
shapeGeometric shape variantcircle square pill
sizeDimension2xs xs s m l xl 2xl 3xl
widthExplicit width constraint2xs xs s m l xl 2xl 3xl full
heightExplicit height constraint2xs xs s m l xl 2xl 3xl full
placementFloating element anchor positiontop bottom left right + -start / -end variants
modelValuePrimary bound value, enables v-modelany
labelVisible label for a field or controlstring
placeholderInput placeholder textstring
helpTextHelper text below a fieldstring
nameForm field name attributestring
headingHeading of a card, modal, or sectionstring
subtitleSecondary heading below headingstring
errorValidation error for a field{ code?: number; detail: string }
disabledPrevents interactionboolean
requiredMarks a field as requiredboolean
readonlyAllows reading but not editingboolean
loadingShows a loading stateboolean
openControls open/expanded stateboolean
closableShows a dismiss / close controlboolean
asPolymorphic root element overridestring | Component

Slots

All slot names use kebab-case without exception. Use logical direction names that are RTL-safe rather than physical ones (start/end over left/right).

Standard positional slot names:

SlotPurpose
defaultPrimary content area. Always provide unless the component has none
leadingIcon or element flanking the component's primary content from the outside
trailingIcon or element flanking the component's primary content from the outside, on the end side
prefixContent rendered inside an input field boundary at the start (e.g. currency symbol)
suffixContent rendered inside an input field boundary at the end (e.g. unit label)
header-startLeft region of a header row
header-endRight region of a header row
triggerElement that opens an overlay or compound component. Receives { open } scoped state
footerBottom region of a card, modal, or panel

Use scoped slots to expose internal state that consumers may need to react to:

<slot name="default" :open="open" />

Mark internal-only slots with @private:

<!-- @private -->
<slot name="before-card" />

Events

Events use kebab-case. Two-way binding follows Vue's update:propName pattern. Custom events are verbs or verb-object phrases.

Always declare every event in defineEmits. Undeclared $emit calls are a maintenance hazard and break TypeScript consumers.

const emit = defineEmits<{
  "update:modelValue": [value: string];
  "update:open": [value: boolean];
  "item-activate": [item: Item];
  focus: [event: FocusEvent];
  blur: [event: FocusEvent];
}>();

Pair every two-way prop with a matching update:propName event. Never use a change event as a substitute for update:modelValue.

Never re-emit raw DOM events directly. The one permitted exception is focus and blur on form field components: consumers need the FocusEvent object for custom validation timing and there is no semantic equivalent.

CSS

Classes follow BEM: mt-{component}__element for sub-parts and mt-{component}--modifier for variants and states.

.mt-button {
}
.mt-button__content {
}
.mt-button--primary {
}
.mt-button--disabled {
}

Always use design tokens for colors, spacing, typography, and border radii. Never hardcode values that a token covers. This ensures components respond correctly to theme changes.

Compound components

Use the compound component pattern when a component requires coordinated sub-parts that consumers may want to compose independently. Sub-components are named mt-{parent}-{role}. Shared state lives in a composable under composables/ and is distributed via provide/inject.

mt-modal/
  mt-modal.vue
  sub-components/
    mt-modal-trigger.vue
    mt-modal-close.vue
    mt-modal-action.vue
  composables/
    useModalContext.ts

Export all public sub-components from the package index alongside the parent.

Do
mt-modal + mt-modal-trigger + mt-modal-action composed by the consumer
Don't
one mt-modal with props: showTrigger, triggerLabel, showCloseButton, footerButtonLabel, ...

Accessibility

Every interactive component must be keyboard-navigable and work with a screen reader out of the box. Use semantic HTML elements as the base. Apply ARIA attributes only when native semantics are insufficient.

Form controls must always render a visible, associated label. Loading and disabled states must be communicated via the appropriate HTML attributes (aria-busy, disabled) so assistive technology can announce them without relying on visual cues alone.

For detailed accessibility requirements and patterns, see the Accessibility page.

Documentation

A component is built and verified in Storybook first, then documented on this site.

  1. Ship a .stories.ts file with the component and write its tests. Stories cover all significant variants and states, and at least one interactive story exercises the primary user flow.
  2. Once the component and its stories are in place, document it on this docs site: add a page with live examples, an API reference, and usage guidance. Follow the content standard for the page structure and conventions.

Component lifecycle

Meteor components have one of three lifecycle states: Experimental, Stable, or Deprecated. The state tells consumers how much they can rely on the component's API and behavior.

Experimental

Experimental components are available for early use, but their API and behavior are not stable yet. Use them when they solve your problem, but expect that props, events, slots, visual behavior, or accessibility details may still change.

During this stage, we test new ideas and gather feedback. If a component is not useful enough to become Stable, we may remove it in a future release. Breaking changes can happen without notice beforehand.

Stable

Stable components are ready for production use.

A component can move to Stable when:

  • it is covered by automated tests
  • it uses Meteor Design Tokens
  • it meets WCAG 2.1 AA requirements
  • it has documentation and usage examples

Breaking changes to Stable components are documented at least one patch version before a major release.

Deprecated

Deprecated components are still available, but should no longer be used for new work.

When possible, we provide an alternative and migration guidance. Deprecated components are removed in the next major version.

Adding new components

Meteor does not cover every component every team will ever need. When you hit a gap, follow this process.

1. Build it locally first. Build the component in your own app. Local implementations ship faster and let you validate the design and API in a real product context before committing to a shared interface.

2. Inform the Meteor team. Once the component is stable and in use, let the Meteor team know via a GitHub issue or the dedicated Slack channel. Share what the component does, where it is used, and any design decisions you made along the way.

3. The Meteor team evaluates the fit. The team will assess whether the component belongs in Meteor or should stay local. Not every component needs to be global. Some are too product-specific, too narrow in scope, or not yet stable enough to expose as a shared API.

4. Promotion from local to global. If the same component surfaces independently across multiple products, that is a strong signal it belongs in Meteor. The team tracks these patterns and will take ownership of the migration when the time is right. Your local implementation becomes the foundation for the global version.