Load PROACTIVELY when task involves designing or building UI components. Use when user says "build a component", "create a form", "add a modal", "design the layout", or "refactor this page". Covers component composition and hierarchy, prop design and typing, render optimization (memo, useMemo, useCallback), compound component patterns, controlled vs uncontrolled forms, file organization, and accessibility for React, Vue, and Svelte.
Guides UI component design and implementation with composition patterns, state management, and performance optimization for React, Vue, and Svelte.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/component-patterns.mdscripts/validate-components.shscripts/
validate-components.sh
references/
component-patterns.md
This skill guides you through designing and implementing UI components using GoodVibes precision and analysis tools. Use this workflow when building React, Vue, or Svelte components with proper composition, state management, and performance optimization.
Load this skill when:
Trigger phrases: "build component", "create UI", "component structure", "render optimization", "state lifting", "component composition".
Before building components, understand existing patterns in the codebase.
Use discover to find all component files and understand the organization pattern.
discover:
queries:
- id: react_components
type: glob
patterns: ["**/*.tsx", "**/*.jsx"]
- id: vue_components
type: glob
patterns: ["**/*.vue"]
- id: svelte_components
type: glob
patterns: ["**/*.svelte"]
verbosity: files_only
What this reveals:
Use discover to find composition patterns, state management, and styling approaches.
discover:
queries:
- id: composition_patterns
type: grep
pattern: "(children|render|slot|as\\s*=)"
glob: "**/*.{tsx,jsx,vue,svelte}"
- id: state_management
type: grep
pattern: "(useState|useReducer|reactive|writable|createSignal)"
glob: "**/*.{ts,tsx,js,jsx,vue,svelte}"
- id: styling_approach
type: grep
pattern: "(className|styled|css|tw`|@apply)"
glob: "**/*.{tsx,jsx,vue,svelte}"
- id: performance_hooks
type: grep
pattern: "(useMemo|useCallback|memo|computed|\\$:)"
glob: "**/*.{ts,tsx,js,jsx,vue,svelte}"
verbosity: files_only
What this reveals:
Use precision_read with symbol extraction to understand component exports.
precision_read:
files:
- path: "src/components/Button/index.tsx" # Example component
extract: symbols
symbol_filter: ["function", "class", "interface", "type"]
verbosity: standard
What this reveals:
Read 2-3 well-structured components to understand implementation patterns.
precision_read:
files:
- path: "src/components/Button/Button.tsx"
extract: content
- path: "src/components/Form/Form.tsx"
extract: content
output:
max_per_item: 100
verbosity: standard
Consult references/component-patterns.md for the organization decision tree.
Common patterns:
Decision factors:
See references/component-patterns.md for detailed comparison.
Pattern selection guide:
| Pattern | Use When | Framework Support |
|---|---|---|
| Children props | Simple wrapper components | React, Vue (slots), Svelte |
| Render props | Dynamic rendering logic | React (legacy) |
| Compound components | Related components share state | React, Vue, Svelte |
| Higher-Order Components | Cross-cutting concerns | React (legacy) |
| Hooks/Composables | Logic reuse | React, Vue 3, Svelte |
| Slots | Template-based composition | Vue, Svelte |
Modern recommendation:
Component-level state:
Application-level state:
See references/component-patterns.md for state management decision tree.
Start with TypeScript interfaces for props.
React Example:
import { ReactNode } from 'react';
interface ButtonProps {
/** Button content */
children: ReactNode;
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
/** Size preset */
size?: 'sm' | 'md' | 'lg';
/** Loading state */
isLoading?: boolean;
/** Disabled state */
disabled?: boolean;
/** Click handler */
onClick?: () => void;
/** Additional CSS classes */
className?: string;
}
Best practices:
?any typesVue 3 Example:
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
variant: {
type: String as PropType<'primary' | 'secondary' | 'ghost' | 'danger'>,
default: 'primary',
},
size: {
type: String as PropType<'sm' | 'md' | 'lg'>,
default: 'md',
},
isLoading: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
});
Svelte Example:
// Button.svelte (Svelte 4)
<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' | 'danger' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let isLoading = false;
export let disabled = false;
</script>
// Button.svelte (Svelte 5 - using $props rune)
<script lang="ts">
let { variant = 'primary', size = 'md', isLoading = false, disabled = false }: {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
disabled?: boolean;
} = $props();
</script>
Follow framework-specific patterns for component implementation.
React with Composition:
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
import { Spinner } from './Spinner';
const buttonVariants = {
variant: {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900',
ghost: 'hover:bg-gray-100 text-gray-700',
danger: 'bg-red-600 hover:bg-red-700 text-white',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = 'primary',
size = 'md',
isLoading = false,
disabled = false,
className,
...props
},
ref
) => {
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
'transition-colors focus-visible:outline-none focus-visible:ring-2',
'disabled:pointer-events-none disabled:opacity-50',
buttonVariants.variant[variant],
buttonVariants.size[size],
className
)}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<>
<Spinner className="mr-2 h-4 w-4" aria-hidden />
<span>Loading...</span>
</>
) : (
children
)}
</button>
);
}
);
Button.displayName = 'Button';
Key patterns:
forwardRef to expose DOM ref...props for flexibilityCreate component directory with proper file structure.
Standard structure:
components/
Button/
Button.tsx # Component implementation
Button.test.tsx # Unit tests
Button.stories.tsx # Storybook stories (if using)
index.tsx # Barrel export
types.ts # Type definitions (if complex)
Implementation with precision tools:
precision_write:
files:
- path: "src/components/Button/Button.tsx"
content: |
import { forwardRef } from 'react';
// ... [full implementation]
- path: "src/components/Button/index.tsx"
content: |
export { Button } from './Button';
export type { ButtonProps } from './Button';
- path: "src/components/Button/Button.test.tsx"
content: |
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
// ... [test cases]
verbosity: count_only
Local state (component-only):
Lifted state (parent manages):
Global state (app-level):
React - Local State:
import { useState } from 'react';
interface SearchResult {
id: string;
title: string;
description: string;
}
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const handleSearch = async () => {
const data = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
setResults(await data.json());
};
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</>
);
}
React - Lifted State:
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
return (
<>
<SearchInput query={query} onQueryChange={setQuery} />
<SearchResults results={results} />
</>
);
}
React - Derived State:
interface Item {
id: string;
category: string;
name: string;
}
interface FilteredListProps {
items: Item[];
filter: string;
}
function FilteredList({ items, filter }: FilteredListProps) {
// Don't store filtered items in state - derive them
const filteredItems = items.filter(item => item.category === filter);
return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
Use Context for deeply nested props:
import { createContext, useContext } from 'react';
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
Use analysis tools to detect performance problems.
mcp__plugin_goodvibes_frontend-engine__trace_component_state:
component_file: "src/components/Dashboard.tsx"
target_component: "Dashboard"
mcp__plugin_goodvibes_frontend-engine__analyze_render_triggers:
component_file: "src/components/ExpensiveList.tsx"
What these reveal:
Memoize expensive calculations:
import { useMemo } from 'react';
interface DataItem {
id: string;
[key: string]: unknown;
}
interface DataTableProps {
data: DataItem[];
filters: Record<string, any>;
}
function DataTable({ data, filters }: DataTableProps) {
const filteredData = useMemo(() => {
return data.filter(item => matchesFilters(item, filters));
}, [data, filters]);
return <table>{/* render filteredData */}</table>;
}
Memoize callback functions:
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Prevent Child re-render when count changes
const handleClick = useCallback(() => {
onClick();
}, []);
return <Child onClick={handleClick} />;
}
Memoize components:
import { memo } from 'react';
interface ExpensiveChildProps {
data: DataItem[];
}
const ExpensiveChild = memo(function ExpensiveChild({ data }: ExpensiveChildProps) {
// Only re-renders if data changes
return <div>{/* complex rendering */}</div>;
});
For large lists, use virtualization libraries.
import { useVirtualizer } from '@tanstack/react-virtual';
interface VirtualListProps<T = unknown> {
items: T[];
}
function VirtualList({ items }: VirtualListProps) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
// Note: Inline styles acceptable here for virtualization positioning
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
React lazy loading:
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
Use proper HTML elements instead of divs.
// Bad
<div onClick={handleClick}>Click me</div>
// Good
<button onClick={handleClick}>Click me</button>
Add ARIA labels for screen readers.
interface DialogProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
}
function Dialog({ isOpen, onClose, children }: DialogProps) {
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Dialog Title</h2>
<div id="dialog-description">{children}</div>
<button onClick={onClose} aria-label="Close dialog">
X
</button>
</div>
);
}
Ensure all interactive elements are keyboard-accessible.
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === 'Escape') setIsOpen(false);
if (e.key === 'Enter' || e.key === ' ') setIsOpen(!isOpen);
};
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-expanded={isOpen}
aria-haspopup="true"
>
Menu
</button>
{isOpen && <div role="menu">{/* menu items */}</div>}
</div>
);
}
Use frontend analysis tools to check accessibility.
mcp__plugin_goodvibes_frontend-engine__get_accessibility_tree:
component_file: "src/components/Dialog.tsx"
Verify TypeScript compilation.
precision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal
Use the validation script to ensure quality.
bash plugins/goodvibes/skills/outcome/component-architecture/scripts/validate-components.sh .
See scripts/validate-components.sh for the complete validation suite.
If using Storybook or similar, run visual tests.
precision_exec:
commands:
- cmd: "npm run test:visual"
expect:
exit_code: 0
verbosity: minimal
DON'T:
any types for component propsDO:
useCallbackDiscovery Phase:
discover: { queries: [components, patterns, state, styling], verbosity: files_only }
precision_read: { files: [example components], extract: symbols }
Implementation Phase:
precision_write: { files: [Component.tsx, index.tsx, types.ts], verbosity: count_only }
Performance Analysis:
trace_component_state: { component_file: "src/...", target_component: "Name" }
analyze_render_triggers: { component_file: "src/..." }
Accessibility Check:
get_accessibility_tree: { component_file: "src/..." }
Validation Phase:
precision_exec: { commands: [{ cmd: "npm run typecheck" }] }
Post-Implementation:
bash scripts/validate-components.sh .
For detailed patterns, framework comparisons, and decision trees, see references/component-patterns.md.
Consider using these complementary GoodVibes skills:
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.