Implementation Plan
Prioritized Roadmap with Code Examples
Document Date: December 13, 2025
Phase 1: Foundation (Weeks 1-2)
Priority 1: Mobile-Responsive Navigation (CRITICAL)
Effort: 3-5 days | Impact: Very High | ROI: Very High
Tasks:
- Create mobile menu component
- Add hamburger icon transitions
- Implement slide-in animation
- Test on real devices
- Accessibility audit
Implementation: See /03-user-experience-recommendations.md Section 1.1
Acceptance Criteria:
- Navigation works on all screen sizes (320px+)
- Smooth open/close animations
- Keyboard accessible (Tab, Enter, Escape)
- Screen reader compatible
- Touch-friendly tap targets (44px minimum)
Priority 2: Fix Critical Accessibility Issues (HIGH)
Effort: 1 week | Impact: Very High | ROI: Very High
Task 2.1: Add ARIA Labels
// Navigation improvements
<nav aria-label="Main navigation">
<button
aria-label="Open menu"
aria-expanded={menuOpen}
aria-controls="mobile-menu"
>
Menu
</button>
</nav>
// Modal improvements
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">Modal Title</h2>
<p id="modal-description">Modal description</p>
</div>
// Form improvements
<form aria-label="Login form">
<Input
id="email"
label="Email Address"
aria-required="true"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && (
<span id="email-error" role="alert">
{error}
</span>
)}
</form>
Task 2.2: Keyboard Navigation
// Add keyboard handler utility
// /lib/keyboard.ts
export function handleKeyboardNav(event: React.KeyboardEvent) {
switch (event.key) {
case 'Escape':
// Close modals/dropdowns
break
case 'Tab':
// Trap focus in modals
break
case 'Enter':
case ' ':
// Activate buttons
break
}
}
// Modal with focus trap
import { useEffect, useRef } from 'react'
export function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null)
const previousFocus = useRef<HTMLElement | null>(null)
useEffect(() => {
if (isOpen) {
previousFocus.current = document.activeElement as HTMLElement
modalRef.current?.focus()
} else {
previousFocus.current?.focus()
}
}, [isOpen])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'Tab') {
// Implement focus trap
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
// ... focus trap logic
}
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
}
return () => document.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onClose])
// ... rest of component
}
Task 2.3: Color Contrast Fixes
// Before (may fail WCAG)
<span className="text-slate-400">Helper text</span>
// After (WCAG AA compliant)
<span className="text-slate-600">Helper text</span>
// Create contrast checker utility
// /lib/accessibility.ts
export function checkContrast(foreground: string, background: string): {
ratio: number
passes: { aa: boolean; aaa: boolean }
} {
// Implement WCAG contrast calculation
// Return ratio and compliance status
}
// Use in development
if (process.env.NODE_ENV === 'development') {
const result = checkContrast('#64748B', '#FFFFFF')
if (!result.passes.aa) {
console.warn('Color contrast fails WCAG AA')
}
}
Acceptance Criteria:
- All interactive elements have proper ARIA labels
- Full keyboard navigation support
- Focus indicators visible on all elements
- Color contrast ratio ≥ 4.5:1 for text
- Lighthouse accessibility score ≥ 90
Priority 3: Standardize Loading States (HIGH)
Effort: 2-3 days | Impact: Medium | ROI: High
Implementation:
// /components/ui/LoadingState.tsx
export function LoadingState({
type = 'spinner',
message = 'Loading...',
}: {
type?: 'spinner' | 'skeleton' | 'dots'
message?: string
}) {
if (type === 'skeleton') {
return <SkeletonLoader />
}
if (type === 'dots') {
return (
<div className="flex items-center justify-center gap-2">
<div className="w-2 h-2 bg-stem-blue-500 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-stem-blue-500 rounded-full animate-bounce [animation-delay:0.1s]" />
<div className="w-2 h-2 bg-stem-blue-500 rounded-full animate-bounce [animation-delay:0.2s]" />
</div>
)
}
return (
<div className="flex flex-col items-center justify-center p-8">
<svg
className="animate-spin h-12 w-12 text-stem-blue-500"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p className="mt-4 text-lg font-semibold text-slate-700">{message}</p>
</div>
)
}
// Usage in pages
export default function StudentDashboard() {
const { data, isLoading, error } = useQuery({
queryKey: ['assignments'],
queryFn: fetchAssignments,
})
if (isLoading) return <LoadingState type="skeleton" />
if (error) return <ErrorState />
return <div>{/* ... */}</div>
}
Acceptance Criteria:
- Consistent loading patterns across all pages
- Skeleton loaders for content-heavy pages
- Spinner for quick operations
- Loading text indicates what's loading
- Min display time to avoid flicker
Phase 2: Enhancement (Weeks 3-5)
Priority 4: Data Visualization with Highcharts (MEDIUM)
Effort: 2-3 weeks | Impact: Very High | ROI: Medium
Task 4.1: Install and Configure Highcharts
npm install highcharts highcharts-react-official
npm install --save-dev @types/highcharts
// /lib/highcharts-theme.ts
import Highcharts from 'highcharts'
// STEMBlock.ai custom theme
export const stemBlockTheme: Highcharts.Options = {
colors: [
'#2E7CF6', // stem-blue-500
'#8B5CF6', // stem-purple-500
'#10B981', // stem-green-500
'#F59E0B', // stem-orange-500
'#14B8A6', // stem-teal-500
'#EF4444', // stem-red-500
],
chart: {
backgroundColor: 'transparent',
style: {
fontFamily: 'Inter, system-ui, sans-serif',
},
},
title: {
style: {
fontFamily: 'Poppins, system-ui, sans-serif',
fontWeight: '700',
fontSize: '20px',
color: '#1E293B',
},
},
xAxis: {
gridLineColor: '#E2E8F0',
labels: {
style: {
color: '#64748B',
fontSize: '12px',
},
},
lineColor: '#CBD5E1',
},
yAxis: {
gridLineColor: '#E2E8F0',
labels: {
style: {
color: '#64748B',
fontSize: '12px',
},
},
},
legend: {
itemStyle: {
color: '#475569',
fontSize: '14px',
fontWeight: '500',
},
},
plotOptions: {
series: {
borderRadius: 4,
dataLabels: {
style: {
fontSize: '12px',
fontWeight: '600',
textOutline: 'none',
},
},
},
},
tooltip: {
backgroundColor: '#FFFFFF',
borderColor: '#CBD5E1',
borderRadius: 8,
shadow: true,
style: {
color: '#1E293B',
fontSize: '14px',
},
},
}
// Apply theme globally
Highcharts.setOptions(stemBlockTheme)
Task 4.2: Create Chart Components
// /components/charts/ScoreTrendChart.tsx
import Highcharts from 'highcharts'
import HighchartsReact from 'highcharts-react-official'
interface ScoreTrendChartProps {
data: Array<{ date: string; score: number; maxScore: number }>
}
export function ScoreTrendChart({ data }: ScoreTrendChartProps) {
const options: Highcharts.Options = {
chart: {
type: 'line',
height: 300,
},
title: {
text: 'Score Trends Over Time',
},
xAxis: {
categories: data.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })),
title: {
text: 'Assignment Date',
},
},
yAxis: {
title: {
text: 'Score (%)',
},
min: 0,
max: 100,
},
series: [
{
name: 'Your Scores',
type: 'line',
data: data.map(d => Math.round((d.score / d.maxScore) * 100)),
marker: {
radius: 6,
symbol: 'circle',
},
},
{
name: 'Target (80%)',
type: 'line',
data: Array(data.length).fill(80),
dashStyle: 'Dash',
marker: {
enabled: false,
},
enableMouseTracking: false,
},
],
credits: {
enabled: false,
},
}
return (
<div className="bg-white p-6 rounded-xl border-2 border-slate-200">
<HighchartsReact highcharts={Highcharts} options={options} />
</div>
)
}
// /components/charts/SkillsRadarChart.tsx
export function SkillsRadarChart({ skills }: { skills: Record<string, number> }) {
const options: Highcharts.Options = {
chart: {
polar: true,
type: 'area',
height: 400,
},
title: {
text: 'Skills Assessment',
},
xAxis: {
categories: Object.keys(skills),
tickmarkPlacement: 'on',
lineWidth: 0,
},
yAxis: {
gridLineInterpolation: 'polygon',
lineWidth: 0,
min: 0,
max: 100,
},
series: [
{
name: 'Your Skills',
type: 'area',
data: Object.values(skills),
fillOpacity: 0.3,
},
],
credits: {
enabled: false,
},
}
return (
<div className="bg-white p-6 rounded-xl border-2 border-slate-200">
<HighchartsReact highcharts={Highcharts} options={options} />
</div>
)
}
Task 4.3: Update Student Dashboard
// /app/student/dashboard/page.tsx - Add charts
export default function StudentDashboard() {
// ... existing code ...
// Add after "Recent Scores" section
return (
<main>
{/* ... existing sections ... */}
{/* Performance Analytics */}
{submissions.length >= 3 && (
<section className="mb-8">
<h2 className="text-2xl font-display font-bold text-slate-900 mb-6">
Your Performance Analytics 📊
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<ScoreTrendChart
data={submissions.map(s => ({
date: s.submittedAt,
score: s.evaluation?.overallScore || 0,
maxScore: s.assignment.maxScore,
}))}
/>
<SkillsRadarChart
skills={{
'Problem Solving': 85,
'Creativity': 92,
'Technical Skills': 78,
'Documentation': 88,
'Collaboration': 90,
}}
/>
</div>
</section>
)}
{/* ... rest of dashboard ... */}
</main>
)
}
Acceptance Criteria:
- Highcharts theme matches STEMBlock.ai design
- Charts are responsive (mobile-friendly)
- Tooltips show detailed information
- Charts accessible (ARIA labels, keyboard nav)
- Print-friendly versions
- Export functionality (PNG, PDF)
Priority 5: Micro-interactions & Animations (MEDIUM)
Effort: 1 week | Impact: High | ROI: Medium
Task 5.1: Install Framer Motion
npm install framer-motion
Task 5.2: Page Transitions
// /app/template.tsx
'use client'
import { motion } from 'framer-motion'
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
{children}
</motion.div>
)
}
Task 5.3: Stagger Animations
// /components/AnimatedGrid.tsx
import { motion } from 'framer-motion'
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
}
export function AnimatedGrid({ children }: { children: React.ReactNode[] }) {
return (
<motion.div
variants={container}
initial="hidden"
animate="show"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
{React.Children.map(children, (child, index) => (
<motion.div key={index} variants={item}>
{child}
</motion.div>
))}
</motion.div>
)
}
Task 5.4: Success Animations
// /components/SuccessConfetti.tsx
import { motion } from 'framer-motion'
export function SuccessConfetti() {
return (
<div className="fixed inset-0 pointer-events-none overflow-hidden">
{Array.from({ length: 50 }).map((_, i) => (
<motion.div
key={i}
className="absolute w-2 h-2 rounded-full"
style={{
left: `${Math.random() * 100}%`,
top: '-10px',
backgroundColor: ['#2E7CF6', '#8B5CF6', '#10B981', '#F59E0B'][
Math.floor(Math.random() * 4)
],
}}
animate={{
y: [0, window.innerHeight + 100],
x: [0, (Math.random() - 0.5) * 200],
rotate: [0, Math.random() * 360],
opacity: [1, 0],
}}
transition={{
duration: 2 + Math.random() * 2,
delay: Math.random() * 0.5,
ease: 'easeOut',
}}
/>
))}
</div>
)
}
Acceptance Criteria:
- Smooth page transitions
- Card hover animations
- Button press feedback
- Success celebrations (confetti, etc.)
- No animation jank (60fps)
- Respects prefers-reduced-motion
Phase 3: Optimization (Weeks 6-8)
Priority 6: Performance Optimization (MEDIUM)
Effort: 1-2 weeks | Impact: Medium | ROI: High
Task 6.1: Image Optimization
// Use Next.js Image component everywhere
import Image from 'next/image'
<Image
src="/hero-image.png"
alt="Description"
width={1200}
height={600}
priority // For above-fold images
placeholder="blur"
blurDataURL="data:image/..." // Generated at build time
/>
Task 6.2: Code Splitting
// Dynamic imports for heavy components
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(
() => import('@/components/charts/ComplexChart'),
{
loading: () => <LoadingState type="skeleton" />,
ssr: false, // Don't render on server if not needed
}
)
Task 6.3: React Query Optimization
// /lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
retry: 1,
},
},
})
// Prefetch data
await queryClient.prefetchQuery({
queryKey: ['assignments'],
queryFn: fetchAssignments,
})
Acceptance Criteria:
- Lighthouse Performance score ≥ 90
- First Contentful Paint < 1.5s
- Time to Interactive < 3.5s
- Cumulative Layout Shift < 0.1
- Bundle size < 250KB (initial load)
Implementation Schedule
Week 1-2: Foundation
- Day 1-3: Mobile navigation
- Day 4-5: ARIA labels and keyboard nav
- Day 6-7: Color contrast fixes
- Day 8-10: Loading states standardization
Week 3-4: Data Visualization
- Day 1-2: Highcharts setup and theme
- Day 3-5: Create chart components
- Day 6-8: Integrate into dashboards
- Day 9-10: Testing and refinement
Week 5: Micro-interactions
- Day 1-2: Framer Motion setup
- Day 3-4: Page transitions
- Day 5-7: Component animations
- Day 8-10: Polish and testing
Week 6-8: Optimization
- Week 6: Performance optimization
- Week 7: Accessibility final audit
- Week 8: Testing, bug fixes, documentation
Success Metrics
Track these KPIs weekly:
| Metric | Baseline | Target | Current |
|---|---|---|---|
| Mobile Usability | 60% | 95% | - |
| Lighthouse Accessibility | 75 | 95+ | - |
| Lighthouse Performance | 80 | 90+ | - |
| User Task Completion | 75% | 90% | - |
| Time on Task | - | -30% | - |
| Mobile Bounce Rate | - | -25% | - |
Document Version: 1.0 Last Updated: December 13, 2025