# UI — React Component Implementation with SCSS and Accessibility
## Purpose
This skill guides you through creating accessible React components with SCSS styling, following modern best practices for functional components, hooks, semantic HTML, and WCAG AA accessibility standards.
## When to Use This Skill
Use when:
- Creating a new React component (UI element, page, or layout)
- Building accessible, keyboard-navigable interfaces
- Implementing responsive designs with SCSS
- Adding interactive elements with proper state management
Do NOT use when:
- Working with class components (legacy codebases only)
- Building non-React UIs (Vue, Angular, etc.)
- Creating headless/API-only components
## Required Inputs
1. **Component Name**: PascalCase name (e.g., "UserCard", "NavigationMenu")
2. **Component Type**: Presentational, Container, or Page
3. **Props**: List of expected props with types (optional: use PropTypes or TypeScript)
4. **State Requirements**: Does it need local state, effects, or context?
**Defaults:**
- Component location: `src/components/{ComponentName}/`
- Styling: SCSS with BEM naming
- Accessibility: WCAG AA compliance
## Steps
### 1. Create Component Directory Structure
```bash
mkdir -p src/components/UserCard
touch src/components/UserCard/UserCard.jsx
touch src/components/UserCard/user-card.scss
touch src/components/UserCard/UserCard.test.jsx
```
**Note**: Use kebab-case for SCSS files, PascalCase for component files.
### 2. Create Base Component File
Create `src/components/UserCard/UserCard.jsx`:
```javascript
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './user-card.scss';
function UserCard({ userId, onUserClick }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
if (userId) {
fetchUser();
}
}, [userId]);
const handleClick = () => {
if (user && onUserClick) {
onUserClick(user);
}
};
if (loading) {
return (
Loading user...
);
}
if (error) {
return (
);
}
if (!user) {
return (
);
}
return (
e.key === 'Enter' && handleClick()}
role="button"
tabIndex={0}
aria-label={`User card for ${user.name}`}
>
{user.name}
{user.email}
{user.role && (
{user.role}
)}
);
}
UserCard.propTypes = {
userId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onUserClick: PropTypes.func,
};
UserCard.defaultProps = {
onUserClick: null,
};
export default UserCard;
```
### 3. Create SCSS Styling with BEM
Create `src/components/UserCard/user-card.scss`:
```scss
.user-card {
display: flex;
gap: 1rem;
padding: 1.5rem;
background-color: var(--color-background, #ffffff);
border: 1px solid var(--color-border, #e5e7eb);
border-radius: var(--border-radius, 0.5rem);
cursor: pointer;
transition: all 0.2s ease-in-out;
// Hover and focus states for accessibility
&:hover {
border-color: var(--color-primary, #3b82f6);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
&:focus {
outline: 2px solid var(--color-focus, #3b82f6);
outline-offset: 2px;
}
// Avatar element
&__avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
// Content container
&__content {
flex: 1;
min-width: 0; // Prevent overflow
}
// Name element
&__name {
margin: 0 0 0.25rem 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text-primary, #111827);
}
// Email element
&__email {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
color: var(--color-text-secondary, #6b7280);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// Badge modifier
&__badge {
display: inline-block;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
color: var(--color-badge-text, #ffffff);
background-color: var(--color-badge-bg, #3b82f6);
border-radius: 9999px;
}
// Loading state modifier
&--loading {
justify-content: center;
align-items: center;
min-height: 120px;
cursor: default;
}
&__loading-text {
color: var(--color-text-secondary, #6b7280);
font-style: italic;
}
// Error state modifier
&--error {
border-color: var(--color-error, #ef4444);
background-color: var(--color-error-background, #fef2f2);
}
&__error-message {
margin: 0;
color: var(--color-error, #ef4444);
font-weight: 500;
}
// Empty state modifier
&--empty {
opacity: 0.6;
cursor: default;
}
// Responsive adjustments
@media (max-width: 640px) {
flex-direction: column;
text-align: center;
&__avatar {
margin: 0 auto;
}
&__email {
white-space: normal;
}
}
// RTL support
[dir='rtl'] & {
text-align: right;
}
}
```
### 4. Create Component Tests
Create `src/components/UserCard/UserCard.test.jsx`:
```javascript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import UserCard from './UserCard';
describe('UserCard', () => {
const mockUser = {
id: 1,
name: 'Alice Johnson',
email: 'alice@example.com',
avatar: '/avatars/alice.jpg',
role: 'Admin',
};
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
jest.restoreAllMocks();
});
test('renders loading state initially', () => {
global.fetch.mockImplementation(() => new Promise(() => {})); // Never resolves
render();
expect(screen.getByRole('status')).toBeInTheDocument();
expect(screen.getByText(/loading user/i)).toBeInTheDocument();
});
test('renders user data after fetch', async () => {
global.fetch.mockResolvedValue({
ok: true,
json: async () => mockUser,
});
render();
await waitFor(() => {
expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
expect(screen.getByText('Admin')).toBeInTheDocument();
});
});
test('renders error state on fetch failure', async () => {
global.fetch.mockRejectedValue(new Error('Network error'));
render();
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText(/network error/i)).toBeInTheDocument();
});
});
test('calls onUserClick when clicked', async () => {
global.fetch.mockResolvedValue({
ok: true,
json: async () => mockUser,
});
const handleClick = jest.fn();
render();
await waitFor(() => {
expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
});
const card = screen.getByRole('button');
await userEvent.click(card);
expect(handleClick).toHaveBeenCalledWith(mockUser);
});
test('is keyboard accessible', async () => {
global.fetch.mockResolvedValue({
ok: true,
json: async () => mockUser,
});
const handleClick = jest.fn();
render();
await waitFor(() => {
expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
});
const card = screen.getByRole('button');
card.focus();
await userEvent.keyboard('{Enter}');
expect(handleClick).toHaveBeenCalledWith(mockUser);
});
test('has proper ARIA labels', async () => {
global.fetch.mockResolvedValue({
ok: true,
json: async () => mockUser,
});
render();
await waitFor(() => {
const card = screen.getByLabelText(/user card for alice johnson/i);
expect(card).toBeInTheDocument();
});
});
});
```
### 5. Import and Use Component
In your parent component or page (e.g., `src/pages/Users.jsx`):
```javascript
import UserCard from '../components/UserCard/UserCard';
function Users() {
const handleUserClick = (user) => {
console.log('User clicked:', user);
// Navigate to user details, open modal, etc.
};
return (
);
}
export default Users;
```
### 6. Add Global CSS Variables (if not already defined)
Update `src/styles/variables.scss` or `src/index.scss`:
```scss
:root {
// Colors
--color-primary: #3b82f6;
--color-background: #ffffff;
--color-border: #e5e7eb;
--color-text-primary: #111827;
--color-text-secondary: #6b7280;
--color-error: #ef4444;
--color-error-background: #fef2f2;
--color-focus: #3b82f6;
--color-badge-bg: #3b82f6;
--color-badge-text: #ffffff;
// Spacing
--border-radius: 0.5rem;
// Dark mode (optional)
@media (prefers-color-scheme: dark) {
--color-background: #1f2937;
--color-border: #374151;
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
}
}
```
### 7. Run Component Tests
```bash
npm test -- UserCard.test.jsx
```
Expected output:
```
PASS src/components/UserCard/UserCard.test.jsx
UserCard
✓ renders loading state initially (45ms)
✓ renders user data after fetch (123ms)
✓ renders error state on fetch failure (98ms)
✓ calls onUserClick when clicked (156ms)
✓ is keyboard accessible (142ms)
✓ has proper ARIA labels (134ms)
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
```
## Expected Outputs
After completing these steps, you will have:
1. **Component Directory**: `src/components/UserCard/`
2. **Component File**: `UserCard.jsx` (functional component with hooks)
3. **Styles**: `user-card.scss` (BEM naming, responsive, accessible)
4. **Tests**: `UserCard.test.jsx` (unit tests with accessibility checks)
5. **Integration**: Component imported and used in parent component
## Validation
### 1. Visual Check
Run the development server:
```bash
npm run dev
```
Navigate to the page using the component and verify:
- Component renders correctly
- Hover states work
- Responsive design adapts to mobile
- Dark mode works (if implemented)
### 2. Accessibility Check
Use browser DevTools (Lighthouse):
```bash
# Open DevTools → Lighthouse → Accessibility
```
Expected score: 90+ (WCAG AA compliance)
### 3. Keyboard Navigation
Test keyboard accessibility:
- Tab to component: Should show focus outline
- Press Enter: Should trigger onClick
- Screen reader: Should announce ARIA labels
### 4. Test Coverage
```bash
npm test -- --coverage UserCard.test.jsx
```
Expected coverage: 80%+ for component file
## Related Skills
- `/skills/tables/` — Sortable, filterable data tables
- `/skills/rtl-hebrew/` — RTL layout and Hebrew text
- `/skills/testing-e2e/` — End-to-end component testing
- `/skills/api-express/` — API endpoints for data fetching
## See Also
- [Cursor Rule: UI Patterns](../../.cursor/rules/020-ui-react-scss-a11y.md)
- [Cursor Rule: Code Style](../../.cursor/rules/010-code-style-js.md)
- [Details: React Best Practices](./details/README.md)
- [Details: Component Examples](./details/examples.md)
- [Details: Accessibility Checklist](./details/checklist.md)
- [Details: Common Mistakes](./details/anti-patterns.md)
- [React Documentation](https://react.dev/)
- [WCAG Guidelines](https://www.w3.org/WAI/WCAG22/quickref/)
---
**Last Updated**: 2025-12-31
**React Version**: 18.2+
**Node Version**: 18+
**Maintained by**: Development Policy Library Project