Frontend Component Refactoring - KISS & DRY Principles
Systematic guide for refactoring Vue.js frontend components to eliminate duplication, improve maintainability, and establish consistent UX patterns through proper component architecture and theme separation.
When to Use This Skill
Trigger phrases:
- •"Refactor frontend components"
- •"Too much HTML/CSS duplication"
- •"Inconsistent UI/UX across pages"
- •"Create more Vue components"
- •"Organize component structure"
- •"Separate public and admin themes"
- •"Apply KISS/DRY principles to frontend"
- •"Design system consistency"
Use when you notice:
- •Repeated HTML/CSS patterns across multiple files
- •Inconsistent styling for similar elements (buttons, cards, forms)
- •Copy-pasted component code
- •Mixing public and admin UI patterns
- •Hard-coded colors/spacing instead of theme variables
- •Large monolithic components (>300 lines)
- •Difficult to maintain consistent UX changes
Core Principles
1. KISS (Keep It Simple, Stupid)
- •Components should do ONE thing well
- •Avoid over-engineering with unnecessary abstraction
- •Simple, readable code over clever code
- •Clear naming that explains purpose
2. DRY (Don't Repeat Yourself)
- •Extract repeated HTML patterns into components
- •Use composables for shared logic
- •Centralize theme values (colors, spacing, typography)
- •Shared utilities for common operations
3. Component Hierarchy
frontend/src/components/ ├── base/ # Universal components (buttons, inputs, cards) ├── public/ # Public-facing website components ├── admin/ # Admin panel components └── shared/ # Components used in both contexts (deprecated - migrate to base/)
Component Categorization Strategy
Base Components (components/base/)
Purpose: Universal, context-agnostic building blocks
Examples:
- •
BaseButton.vue- All button variants - •
BaseCard.vue- Card container with optional header/footer - •
BaseInput.vue- Text input with validation display - •
BaseModal.vue- Modal dialog wrapper - •
BaseTable.vue- Data table with sorting/pagination - •
BaseIcon.vue- Icon wrapper (Font Awesome, etc.) - •
BaseBadge.vue- Status badges and labels - •
BaseSpinner.vue- Loading indicators
Characteristics:
- •No hardcoded business logic
- •Highly configurable via props
- •Emit events for parent handling
- •Theme-aware (use CSS variables)
- •Comprehensive prop validation
Example Base Component:
<!-- components/base/BaseButton.vue -->
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
disabled?: boolean
fullWidth?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
loading: false,
disabled: false,
fullWidth: false,
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
</script>
<template>
<button
:class="[
'base-button',
`base-button--${variant}`,
`base-button--${size}`,
{ 'base-button--full-width': fullWidth }
]"
:disabled="disabled || loading"
@click="emit('click', $event)"
>
<BaseSpinner v-if="loading" :size="size" />
<slot v-else />
</button>
</template>
<style scoped>
.base-button {
/* Use CSS variables for theming */
font-family: var(--font-family-base);
border-radius: var(--border-radius-md);
transition: var(--transition-base);
}
.base-button--primary {
background: var(--color-primary);
color: var(--color-primary-contrast);
}
.base-button--secondary {
background: var(--color-secondary);
color: var(--color-secondary-contrast);
}
/* ... more variants */
</style>
Public Components (components/public/)
Purpose: Public website-specific compositions
Examples:
- •
PublicNav.vue- Public site navigation - •
PublicHero.vue- Hero section with animations - •
PublicFooter.vue- Public site footer - •
BlogCard.vue- Blog article card (uses BaseCard) - •
JewelryCard.vue- Jewelry article card (uses BaseCard) - •
ContactForm.vue- Public contact form (uses BaseInput) - •
NewsletterSignup.vue- Newsletter subscription (uses BaseInput)
Characteristics:
- •Compose base components
- •Public-facing branding/styling
- •SEO-optimized
- •Responsive design focused
- •Marketing-oriented UX
Admin Components (components/admin/)
Purpose: Admin panel-specific components
Examples:
- •
AdminNav.vue- Admin navigation with auth state - •
AdminSidebar.vue- Admin panel sidebar - •
AdminDataTable.vue- CRUD data table (uses BaseTable) - •
AdminImageUpload.vue- Image upload with preview (uses base components) - •
AdminFormField.vue- Form field with validation (uses BaseInput) - •
AdminStats.vue- Dashboard statistics widgets
Characteristics:
- •Compose base components
- •Dense information display
- •Efficiency-focused UX
- •Advanced filtering/sorting
- •Bulk operations support
Theme Separation
Public Theme (public-theme.css)
/* frontend/src/styles/themes/public-theme.css */
:root {
/* Brand colors */
--color-primary: #8B7355; /* Warm gold */
--color-primary-contrast: #FFFFFF;
--color-secondary: #2C2C2C; /* Deep charcoal */
--color-accent: #D4AF37; /* Bright gold accent */
/* Typography */
--font-family-heading: 'Playfair Display', serif;
--font-family-base: 'Inter', sans-serif;
--font-size-base: 1rem;
--line-height-base: 1.6;
/* Spacing (generous for readability) */
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--spacing-lg: 2.5rem;
--spacing-xl: 4rem;
/* Layout */
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 16px;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
/* Animations */
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 400ms ease;
}
/* Public-specific utility classes */
.public-container {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
.public-heading {
font-family: var(--font-family-heading);
color: var(--color-secondary);
}
Admin Theme (admin-theme.css)
/* frontend/src/styles/themes/admin-theme.css */
:root {
/* Admin colors (professional, neutral) */
--color-primary: #3B82F6; /* Blue primary */
--color-primary-contrast: #FFFFFF;
--color-secondary: #64748B; /* Slate gray */
--color-success: #10B981; /* Green */
--color-warning: #F59E0B; /* Amber */
--color-danger: #EF4444; /* Red */
/* Typography (optimized for density) */
--font-family-heading: 'Inter', sans-serif;
--font-family-base: 'Inter', sans-serif;
--font-size-base: 0.875rem; /* Smaller for density */
--line-height-base: 1.5;
/* Spacing (tighter for information density) */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Layout (professional) */
--border-radius-sm: 4px;
--border-radius-md: 6px;
--border-radius-lg: 8px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Admin-specific utility classes */
.admin-container {
max-width: 1600px; /* Wider for data tables */
margin: 0 auto;
padding: 0 var(--spacing-md);
}
.admin-heading {
font-family: var(--font-family-heading);
color: var(--color-secondary);
font-weight: 600;
}
Refactoring Workflow
Step 1: Audit Current Components
Goal: Identify duplication and inconsistencies
# Find repeated HTML patterns
cd frontend/src
grep -r "class=\"btn " views/ components/ | wc -l
grep -r "class=\"card " views/ components/ | wc -l
# Find large files (candidates for splitting)
find views components -name "*.vue" -exec wc -l {} + | sort -rn | head -20
# Find inline styles (should use classes)
grep -r "style=\"" views/ components/
Create audit document:
# Frontend Component Audit ## Duplication Found - Button styles: 23 instances across 12 files - Card layouts: 15 instances across 8 files - Form inputs: 34 instances across 15 files ## Large Components - BlogView.vue: 452 lines → split into BlogList + BlogFilters - AdminArticleForm.vue: 389 lines → extract form fields ## Inconsistencies - Button colors: #8B7355, #8b7355, rgb(139, 115, 85) - Spacing: mix of px, rem, and Tailwind classes
Step 2: Create Base Components
Priority order:
- •BaseButton - Most frequently used
- •BaseCard - Common layout element
- •BaseInput - Form consistency
- •BaseModal - Dialog consistency
- •BaseTable - Data display
Template for Base Component:
<script setup lang="ts">
// 1. Define strict TypeScript interfaces
interface Props {
// Required props
// Optional props with defaults
}
// 2. Use withDefaults for default values
const props = withDefaults(defineProps<Props>(), {
// defaults
})
// 3. Define typed emits
const emit = defineEmits<{
eventName: [payload: Type]
}>()
// 4. Minimal logic - delegate to parent
</script>
<template>
<!-- 5. Use CSS variables for all theming -->
<!-- 6. Provide slots for flexibility -->
<!-- 7. Emit events, don't handle business logic -->
</template>
<style scoped>
/* 8. Use CSS custom properties */
/* 9. No hardcoded colors/sizes */
/* 10. BEM naming: .base-component__element--modifier */
</style>
Step 3: Extract Theme Variables
Create theme files:
mkdir -p frontend/src/styles/themes touch frontend/src/styles/themes/public-theme.css touch frontend/src/styles/themes/admin-theme.css touch frontend/src/styles/themes/variables.css # Shared variables
Migration process:
<!-- BEFORE: Hardcoded values -->
<button class="bg-[#8B7355] hover:bg-[#6D5A44] px-4 py-2 rounded-md">
Click me
</button>
<!-- AFTER: Theme variables + Base component -->
<BaseButton variant="primary" size="md" @click="handleClick">
Click me
</BaseButton>
<!-- CSS -->
<style scoped>
/* BEFORE */
.custom-button {
background: #8B7355;
padding: 0.5rem 1rem;
border-radius: 6px;
}
/* AFTER */
.custom-button {
background: var(--color-primary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-md);
}
</style>
Step 4: Refactor Views to Use Base Components
Example refactoring:
<!-- BEFORE: BlogView.vue (452 lines) -->
<template>
<div class="container mx-auto px-4">
<!-- Repeated card structure -->
<div v-for="article in articles" class="bg-white rounded-lg shadow-md p-6 mb-4">
<h2 class="text-2xl font-bold mb-2">{{ article.title }}</h2>
<p class="text-gray-600">{{ article.excerpt }}</p>
<button class="bg-blue-500 text-white px-4 py-2 rounded">Read More</button>
</div>
</div>
</template>
<!-- AFTER: BlogView.vue using components -->
<template>
<div class="public-container">
<BlogCard
v-for="article in articles"
:key="article.id"
:article="article"
@read-more="navigateToArticle"
/>
</div>
</template>
<!-- NEW: BlogCard.vue component -->
<script setup lang="ts">
interface Props {
article: BlogArticle
}
const props = defineProps<Props>()
const emit = defineEmits<{
readMore: [articleId: number]
}>()
</script>
<template>
<BaseCard>
<template #header>
<h2 class="public-heading">{{ article.title }}</h2>
</template>
<p class="text-secondary">{{ article.excerpt }}</p>
<template #footer>
<BaseButton variant="primary" @click="emit('readMore', article.id)">
Read More
</BaseButton>
</template>
</BaseCard>
</template>
Step 5: Apply Theme to Routes
Route-level theme switching:
// frontend/src/router/index.ts
const routes = [
{
path: '/',
component: PublicLayout,
meta: { theme: 'public' },
children: [
{ path: '', component: HomeView },
{ path: 'blog', component: BlogView },
// ... public routes
]
},
{
path: '/admin',
component: AdminLayout,
meta: { theme: 'admin', requiresAuth: true },
children: [
{ path: '', component: AdminDashboard },
{ path: 'articles', component: AdminArticles },
// ... admin routes
]
}
]
// Apply theme via router guard
router.beforeEach((to, from, next) => {
const theme = to.meta.theme || 'public'
document.documentElement.setAttribute('data-theme', theme)
next()
})
Theme CSS organization:
/* frontend/src/styles/main.css */
@import './themes/variables.css'; /* Shared */
@import './themes/public-theme.css'; /* Public overrides */
@import './themes/admin-theme.css'; /* Admin overrides */
/* Theme switching */
[data-theme="public"] {
/* Public theme active */
}
[data-theme="admin"] {
/* Admin theme active */
}
Refactoring Patterns
Pattern 1: Extract Repeated HTML
BEFORE: Duplication
<!-- views/BlogView.vue -->
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold">{{ article.title }}</h2>
<p>{{ article.content }}</p>
</div>
<!-- views/ArticleView.vue -->
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold">{{ article.title }}</h2>
<p>{{ article.description }}</p>
</div>
AFTER: Base Component
<!-- components/base/BaseCard.vue -->
<template>
<div class="base-card">
<slot />
</div>
</template>
<style scoped>
.base-card {
background: var(--color-surface);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-md);
padding: var(--spacing-lg);
}
</style>
<!-- Usage -->
<BaseCard>
<h2 class="heading">{{ article.title }}</h2>
<p>{{ article.content }}</p>
</BaseCard>
Pattern 2: Extract Complex Logic
BEFORE: Logic in Template
<template>
<div>
<button
:class="{
'bg-blue-500': !loading && !disabled,
'bg-gray-400': loading || disabled,
'cursor-not-allowed': loading || disabled,
'hover:bg-blue-600': !loading && !disabled
}"
:disabled="loading || disabled"
@click="handleClick"
>
<span v-if="loading">Loading...</span>
<span v-else>{{ label }}</span>
</button>
</div>
</template>
AFTER: Component + Computed
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
loading?: boolean
disabled?: boolean
variant?: 'primary' | 'secondary'
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
disabled: false,
variant: 'primary'
})
const isDisabled = computed(() => props.loading || props.disabled)
const buttonClasses = computed(() => [
'base-button',
`base-button--${props.variant}`,
{ 'base-button--disabled': isDisabled.value }
])
</script>
<template>
<button
:class="buttonClasses"
:disabled="isDisabled"
@click="$emit('click')"
>
<slot v-if="!loading" />
<BaseSpinner v-else />
</button>
</template>
Pattern 3: Centralize Colors/Spacing
BEFORE: Scattered Values
<style scoped>
/* BlogCard.vue */
.card { padding: 24px; color: #8B7355; }
/* ArticleCard.vue */
.card { padding: 1.5rem; color: rgb(139, 115, 85); }
/* ContactCard.vue */
.card { padding: 20px; color: #8b7355; }
</style>
AFTER: CSS Variables
/* styles/themes/variables.css */
:root {
--spacing-lg: 1.5rem; /* 24px */
--color-primary: #8B7355;
}
/* All components */
<style scoped>
.card {
padding: var(--spacing-lg);
color: var(--color-primary);
}
</style>
Component Organization
Directory Structure
frontend/src/components/
├── base/ # Universal building blocks
│ ├── BaseButton.vue
│ ├── BaseCard.vue
│ ├── BaseInput.vue
│ ├── BaseModal.vue
│ ├── BaseTable.vue
│ ├── BaseSpinner.vue
│ ├── BaseBadge.vue
│ └── BaseIcon.vue
│
├── public/ # Public website components
│ ├── layout/
│ │ ├── PublicNav.vue
│ │ ├── PublicFooter.vue
│ │ └── PublicHero.vue
│ ├── blog/
│ │ ├── BlogCard.vue
│ │ ├── BlogList.vue
│ │ └── BlogFilters.vue
│ ├── jewelry/
│ │ ├── JewelryCard.vue
│ │ ├── JewelryGallery.vue
│ │ └── JewelryFilters.vue
│ └── forms/
│ ├── ContactForm.vue
│ └── NewsletterSignup.vue
│
├── admin/ # Admin panel components
│ ├── layout/
│ │ ├── AdminNav.vue
│ │ ├── AdminSidebar.vue
│ │ └── AdminBreadcrumb.vue
│ ├── tables/
│ │ ├── AdminDataTable.vue
│ │ └── AdminPagination.vue
│ ├── forms/
│ │ ├── AdminFormField.vue
│ │ ├── AdminImageUpload.vue
│ │ └── AdminWYSIWYG.vue
│ └── widgets/
│ ├── AdminStats.vue
│ └── AdminChart.vue
│
└── shared/ # Deprecated - migrate to base/
└── README.md # "Migrate to base/ or public/admin/"
Naming Conventions
Base components: Base{Noun}.vue
- •BaseButton, BaseCard, BaseInput
Public components: {Feature}{Noun}.vue
- •BlogCard, JewelryGallery, ContactForm
Admin components: Admin{Feature}{Noun}.vue
- •AdminDataTable, AdminFormField, AdminStats
Layout components: {Context}Layout.vue
- •PublicLayout, AdminLayout
Testing Strategy
Component Tests
Base components need comprehensive tests:
// tests/unit/BaseButton.spec.ts
import { mount } from '@vue/test-utils'
import BaseButton from '@/components/base/BaseButton.vue'
describe('BaseButton', () => {
it('renders slot content', () => {
const wrapper = mount(BaseButton, {
slots: { default: 'Click me' }
})
expect(wrapper.text()).toBe('Click me')
})
it('emits click event', async () => {
const wrapper = mount(BaseButton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
it('shows loading spinner', () => {
const wrapper = mount(BaseButton, {
props: { loading: true }
})
expect(wrapper.findComponent({ name: 'BaseSpinner' }).exists()).toBe(true)
})
it('disables when disabled prop is true', () => {
const wrapper = mount(BaseButton, {
props: { disabled: true }
})
expect(wrapper.attributes('disabled')).toBeDefined()
})
it('applies variant classes', () => {
const wrapper = mount(BaseButton, {
props: { variant: 'primary' }
})
expect(wrapper.classes()).toContain('base-button--primary')
})
})
Creating a Migration Plan
The skill provides the methodology. You create a project-specific plan.
Step 1: Audit Current State
Duplication Assessment:
# Find duplicate button HTML grep -r "class=.*btn" frontend/src/views/ # Find hardcoded colors grep -r "bg-\|text-\|border-" frontend/src/views/ | wc -l # Find duplicate card structures grep -r "<div class=.*card" frontend/src/views/
Document findings:
- •How many duplicate buttons/cards/inputs?
- •How many hardcoded Tailwind classes?
- •Which views have the most duplication?
Step 2: Prioritize Components
High Priority (Extract First):
- •Components used >5 times
- •Components with accessibility issues
- •Components with UX inconsistencies
Medium Priority:
- •Components used 2-4 times
- •Components with complex logic
- •Components that will grow
Low Priority:
- •One-off components (<100 lines)
- •Components that may change soon
Step 3: Create Phased Checklist
Template Structure:
## Phase 1: Foundation (Week 1) - [ ] Create theme files (public-theme.css, admin-theme.css) - [ ] Create base component directory - [ ] Build [list your base components] - [ ] Write tests for base components ## Phase 2: [Your Domain] Components (Week 2) - [ ] Create [YourDomain]Layout with theme - [ ] Extract [Component1] from [View1] - [ ] Extract [Component2] from [View2] - [ ] Refactor [domain] views to use components - [ ] Test [domain] routes ## Phase 3: [Another Domain] Components (Week 3) - [ ] Similar structure... ## Phase 4: Cleanup (Week 4) - [ ] Remove duplicate HTML/CSS - [ ] Replace hardcoded values with CSS variables - [ ] Document component architecture - [ ] Update CODEBASE_ESSENTIALS.md
Step 4: Estimate Timeline
Complexity factors:
- •Number of components to extract
- •Team size and experience
- •Existing test coverage
- •Design system maturity
Typical timeline: 4-6 weeks for medium-sized project
Step 5: Track Progress
Create a tracking document (example in docs/planning/frontend-component-migration-plan.md) with:
- • Checklist items
- • Success metrics (before/after)
- • Rollback plan
- • Timeline with dates
Update checklist as you go - check off items immediately after validation passes.
Best Practices
DO ✅
- •Use TypeScript for all component props/emits
- •Provide defaults for optional props
- •Use CSS variables for all theming
- •Emit events rather than handling business logic
- •Use slots for flexible composition
- •Write tests for base components
- •Document props with JSDoc comments
- •Follow BEM naming for CSS classes
- •Keep components focused (single responsibility)
- •Use Composition API (
<script setup>)
DON'T ❌
- •Don't hardcode colors/spacing - use CSS variables
- •Don't mix business logic in base components
- •Don't create components for one-time use (unless >100 lines)
- •Don't use inline styles - use classes
- •Don't bypass props with $parent or direct DOM manipulation
- •Don't use any type - define proper interfaces
- •Don't skip validation for props
- •Don't create deep nesting (max 3 levels)
- •Don't ignore accessibility (ARIA labels, keyboard nav)
- •Don't forget responsive design (mobile-first)
Resources
- •Vue 3 Docs: https://vuejs.org/guide/components/
- •Component Design Patterns: https://www.patterns.dev/posts/presentational-container-pattern/
- •CSS Variables Guide: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
- •BEM Naming: http://getbem.com/naming/
- •Tailwind + CSS Variables: https://tailwindcss.com/docs/customizing-colors#using-css-variables
Internal References:
- •CODEBASE_ESSENTIALS.md - Component patterns
- •code-refactoring skill - Safe refactoring workflow
- •developer-checklist skill - Testing requirements
Example Migration Plan:
- •docs/planning/frontend-component-migration-plan.md - gnwebsite-specific implementation
Success Criteria
Completed when:
- • Zero duplicate button/card/input HTML across views
- • All colors/spacing use CSS variables
- • Public and admin routes use separate themes
- • Components organized into base/public/admin
- • All base components have >80% test coverage
- • UX changes can be made in ONE place (theme file)
- • New features reuse existing components
- • CODEBASE_ESSENTIALS.md documents component architecture
Metrics to track:
- •Lines of duplicate HTML/CSS (before/after)
- •Number of hardcoded color values (target: 0)
- •Component reuse percentage
- •Test coverage for base components
- •Time to implement new UX features (should decrease)