frontend_best_practices
Modern frontend development standards for accessibility, testing, TypeScript, and Tailwind CSS. ---
Install / Use
npx skills add tharsananarul/portfolioInstalls into whichever agent you are using.
Cursor Rules
Cursor IDE rules (v2)
Quality Score
Category
Development & EngineeringSupported Platforms
Our assessment of frontend_best_practices
frontend_best_practices scores 67/100 on our quality scale, 3905th of 4,632 Development & Engineering skills we index.
Its Cursor Rules is 19 KB long, well organised into 29 sections with 21 code examples: a thorough specification that gives an agent plenty to work with.
It has no GitHub stars yet, so there is no community track record; judge it on its content.
Maintenance, license and trust
- We could not determine when the repository was last updated.
- No license is declared. By default that means all rights are reserved: you can read it, but reusing or redistributing it is not clearly permitted. Ask the author before building on it commercially.
- Its trust signals score 68/100, with 3 cautions from licensing, adoption, age or documentation. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
frontend_best_practices compared with similar skills
All 4 of these similar skills score higher than frontend_best_practices; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| frontend_best_practices (this skill)by tharsananarul | 67 | 0 | — | Cursor Rules |
| taste-skillby Leonxlnx | 100 | 108.6k | 2d ago | CLAUDE.md |
| headroomby headroomlabs-ai | 100 | 78.7k | today | CLAUDE.md |
| ai-job-searchby ai-job-search-dev | 100 | 69.0k | today | CLAUDE.md |
| claude-howtoby luongnv89 | 100 | 46.4k | today | CLAUDE.md |
Frequently asked questions
- How do I install frontend_best_practices?
- Run
npx skills add tharsananarul/portfolio. The install tabs above show the steps for each supported agent. - Which AI agents does frontend_best_practices work with?
- It is written for Cursor, as a Cursor Rules file. Other agents that read the same format can often use it too.
- Is frontend_best_practices safe to use?
- It declares no license and scores 68/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is frontend_best_practices still maintained?
- We could not determine when the repository was last updated.
Skill content
View source on GitHubFrontend Best Practices
Modern frontend development standards for accessibility, testing, TypeScript, and Tailwind CSS.
Table of Contents
Accessibility (a11y)
Semantic HTML
// BAD - Divs for everything
<div onClick={handleClick}>Click me</div>
<div class="header">...</div>
<div class="nav">...</div>
// GOOD - Semantic elements
<button onClick={handleClick}>Click me</button>
<header>...</header>
<nav>...</nav>
<main>...</main>
<article>...</article>
<aside>...</aside>
<footer>...</footer>
Keyboard Navigation
// Ensure all interactive elements are keyboard accessible
function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
// Focus first focusable element
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
(focusable?.[0] as HTMLElement)?.focus();
// Trap focus within modal
const handleTab = (e: KeyboardEvent) => {
if (e.key === 'Tab' && focusable) {
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleTab);
return () => document.removeEventListener('keydown', handleTab);
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
{children}
</div>
);
}
ARIA Attributes
// Live regions for dynamic content
<div aria-live="polite" aria-atomic="true">
{status && <p>{status}</p>}
</div>
// Loading states
<button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? 'Loading...' : 'Submit'}
</button>
// Form labels
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email}
</p>
)}
// Navigation
<nav aria-label="Main navigation">
<ul>
<li><a href="/" aria-current={isHome ? 'page' : undefined}>Home</a></li>
<li><a href="/about" aria-current={isAbout ? 'page' : undefined}>About</a></li>
</ul>
</nav>
// Toggle buttons
<button
aria-pressed={isEnabled}
onClick={() => setIsEnabled(!isEnabled)}
>
{isEnabled ? 'Enabled' : 'Disabled'}
</button>
// Expandable sections
<button
aria-expanded={isOpen}
aria-controls="content-panel"
onClick={() => setIsOpen(!isOpen)}
>
Show details
</button>
<div id="content-panel" hidden={!isOpen}>
Content here
</div>
Color Contrast
// Ensure 4.5:1 contrast ratio for text (WCAG AA)
// Use tools like @axe-core/react for testing
// tailwind.config.js - Define accessible colors
module.exports = {
theme: {
colors: {
// Primary with proper contrast
primary: {
DEFAULT: '#2563eb', // Blue 600
foreground: '#ffffff',
},
// Error state
error: {
DEFAULT: '#dc2626', // Red 600
foreground: '#ffffff',
},
// Text colors with proper contrast
foreground: '#0f172a', // Slate 900
muted: '#64748b', // Slate 500 - minimum 4.5:1 on white
},
},
};
// Never rely on color alone
<span className="text-red-600">
<ErrorIcon aria-hidden="true" />
<span>Error: Invalid input</span>
</span>
Screen Reader Only Content
// Visually hidden but accessible to screen readers
const srOnly = 'absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0';
// Skip link for keyboard users
<a href="#main-content" className={srOnly + ' focus:not-sr-only focus:absolute focus:top-0'}>
Skip to main content
</a>
// Icon buttons need labels
<button aria-label="Close menu">
<XIcon aria-hidden="true" />
</button>
// Or use visually hidden text
<button>
<XIcon aria-hidden="true" />
<span className={srOnly}>Close menu</span>
</button>
Testing Strategies
Component Testing with Testing Library
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when loading', () => {
render(<Button isLoading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
});
it('shows loading text when loading', () => {
render(<Button isLoading loadingText="Submitting...">Submit</Button>);
expect(screen.getByText('Submitting...')).toBeInTheDocument();
});
});
Hook Testing
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});
Integration Testing
// LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
import { AuthProvider } from '@/contexts/AuthContext';
const mockLogin = jest.fn();
jest.mock('@/lib/auth', () => ({
login: (...args: unknown[]) => mockLogin(...args),
}));
describe('LoginForm', () => {
beforeEach(() => {
mockLogin.mockReset();
});
it('submits form with valid credentials', async () => {
const user = userEvent.setup();
mockLogin.mockResolvedValueOnce({ user: { id: '1', name: 'Test' } });
render(
<AuthProvider>
<LoginForm />
</AuthProvider>
);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith('test@example.com', 'password123');
});
});
it('shows validation errors for empty fields', async () => {
const user = userEvent.setup();
render(
<AuthProvider>
<LoginForm />
</AuthProvider>
);
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
expect(mockLogin).not.toHaveBeenCalled();
});
});
E2E Testing with Playwright
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="product-1"] button');
await page.click('[data-testid="cart-button"]');
});
test('completes checkout with valid payment', async ({ page }) => {
await page.click('text=Proceed to Checkout');
// Fill shipping info
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="address"]', '123 Test St');
await page.fill('[name="city"]', 'Test City');
await page.selectOption('[name="state"]', 'CA');
await page.fill('[name="zip"]', '90210');
await page.click('text=Continue to Payment');
await page.click('text=Place Order');
// Verify success
await expect(page).toHaveURL(/\/order\/confirmation/);
await expect(page.locator('h1')).toHaveText('Order Confirmed!');
});
});
TypeScript Patterns
Component Props
// Use interface for component props
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
// Extend HTML attributes
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
isLoading?: boolean;
}
function Button({ variant = 'primary', isLoading, children, ...props }: ButtonProps) {
return (
<button
{...props}
disabled={props.disabled || isLoading}
className={cn(variants[variant], props.className)}
>
{isLoading ? <Spinner /> : children}
</button>
);
}
// Polymorphic components
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
} & React.ComponentPropsWithoutRef<E>;
function Box<E extends React.ElementType = 'div'>({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
// Usage
<Box as="section" id="hero">Content</Box>
<Box as="article">Article content</Box>
Discriminated Unions
// State machines with exhaustive type checking
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function DataDisplay<T>({ state, render }: {
state: AsyncState<T>;
render: (data: T) => React.ReactNode;
}) {
switch (state.status) {
case 'idle':
return null;
case 'loading':
return <Spinner />;
case 'success':
return <>{render(state.data)}</>;
case 'error':
return <ErrorMessage error={state.error} />;
// TypeScript ensures all cases are handled
}
}
Generic Components
// Generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
if (items.length === 0) {
return <p className="text-muted">{emptyMessage || 'No items'}</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <UserCard user={user} />}
/>
Type Guards
// User-defined type guards
interface User {
id: string;
name: string;
Truncated for display — read the full file on GitHub.
Related Skills
taste-skill
108.6kAgent skills that give coding agents taste: art direction, reference images and a browser check instead of generic AI pages
headroom
78.7kCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
ai-job-search
69.0kThe job search that runs on your machine. AI job application framework built on Claude Code: evaluate postings, tailor CVs, write cover letters, prep interviews. Fork it and own it.
claude-howto
46.4kA visual, example-driven guide to Claude Code — from basic concepts to advanced agents, with copy-paste templates that bring immediate value.
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
