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),numprefix orCount/Indexsuffix for numbers (numColumns,activeIndex). - Boolean props use plain adjectives with no prefix:
loading,open,disabled. Avoid inverted booleans:showIconnothideIcon. - Avoid native HTML attribute names. Use
headinginstead oftitle.disabled,required, andreadonlyare intentional exceptions. - Name props from the component's perspective, not the caller's:
hasSubmitButtonnothasSubmitPermission. - Never use
"default"as a named prop value. Set the default viadefineProps'sdefaultoption. - 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.
| Prop | Purpose | Values / Type |
|---|---|---|
variant | Semantic intent / feedback tone | neutral info attention critical positive |
appearance | Visual hierarchy for interactive components | primary secondary tertiary |
shape | Geometric shape variant | circle square pill |
size | Dimension | 2xs xs s m l xl 2xl 3xl |
width | Explicit width constraint | 2xs xs s m l xl 2xl 3xl full |
height | Explicit height constraint | 2xs xs s m l xl 2xl 3xl full |
placement | Floating element anchor position | top bottom left right + -start / -end variants |
modelValue | Primary bound value, enables v-model | any |
label | Visible label for a field or control | string |
placeholder | Input placeholder text | string |
helpText | Helper text below a field | string |
name | Form field name attribute | string |
heading | Heading of a card, modal, or section | string |
subtitle | Secondary heading below heading | string |
error | Validation error for a field | { code?: number; detail: string } |
disabled | Prevents interaction | boolean |
required | Marks a field as required | boolean |
readonly | Allows reading but not editing | boolean |
loading | Shows a loading state | boolean |
open | Controls open/expanded state | boolean |
closable | Shows a dismiss / close control | boolean |
as | Polymorphic root element override | string | 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:
| Slot | Purpose |
|---|---|
default | Primary content area. Always provide unless the component has none |
leading | Icon or element flanking the component's primary content from the outside |
trailing | Icon or element flanking the component's primary content from the outside, on the end side |
prefix | Content rendered inside an input field boundary at the start (e.g. currency symbol) |
suffix | Content rendered inside an input field boundary at the end (e.g. unit label) |
header-start | Left region of a header row |
header-end | Right region of a header row |
trigger | Element that opens an overlay or compound component. Receives { open } scoped state |
footer | Bottom 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.
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.
- Ship a
.stories.tsfile 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. - 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.