Skip to main content

Frontend Quality Guidelines

Code standards, forbidden patterns, and quality requirements for Viben frontend.

Overview​

This document defines the quality standards for Viben frontend code, including linting rules, forbidden patterns, performance requirements, accessibility standards, and testing guidelines.


Linting​

ESLint Configuration​

The project uses ESLint with the following key configurations:

// eslint.config.mjs
export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
{
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
];

Running Linting​

# Check for lint errors
pnpm lint

# Fix auto-fixable issues
pnpm lint:fix

Forbidden Patterns​

TypeScript​

PatternReasonAlternative
any typeType safetyUse unknown or specific types
@ts-ignoreSuppresses errorsFix the type issue or use @ts-expect-error with comment
Type assertions as XBypasses checkingUse type guards or refine types
Non-null assertions !Runtime riskUse optional chaining or null checks
// Bad
const data = response as any;
// @ts-ignore
processData(unknownValue);
element!.focus();

// Good
const data: ApiResponse = response;
// @ts-expect-error - Legacy API returns untyped data, tracked in VIBEN-123
processData(unknownValue as unknown);
element?.focus();

React​

PatternReasonAlternative
Inline object/array creation in JSXRe-rendersuseMemo or constants
Index as keyPoor reconciliationUnique IDs
Direct DOM manipulationBreaks React modeluseRef with state
Nested ternaries in JSXReadabilityEarly returns or variables
// Bad
<List items={items.filter(x => x.active)} />
{items.map((item, index) => <Item key={index} />)}
{a ? (b ? <X /> : <Y />) : <Z />}

// Good
const activeItems = useMemo(() => items.filter(x => x.active), [items]);
<List items={activeItems} />
{items.map((item) => <Item key={item.id} />)}

// Readable conditional
const renderContent = () => {
if (!a) return <Z />;
if (b) return <X />;
return <Y />;
};

State Management​

PatternReasonAlternative
Prop drilling (> 2 levels)MaintainabilityContext or Zustand
State in URL for UI stateURL pollutionLocal state
Sync state with useEffectRace conditionsDerived state
// Bad - syncing state
useEffect(() => {
setFilteredItems(items.filter(x => x.matches(search)));
}, [items, search]);

// Good - derived state
const filteredItems = useMemo(
() => items.filter(x => x.matches(search)),
[items, search]
);

Performance Requirements​

Bundle Size​

MetricLimitHow to Check
Initial JS< 300KB gzippedpnpm build && pnpm analyze
Per-route chunk< 100KB gzippedCheck build output
No duplicate deps0pnpm dedupe

Runtime Performance​

MetricTarget
First Contentful Paint< 1.8s
Time to Interactive< 3.9s
Cumulative Layout Shift< 0.1
Largest Contentful Paint< 2.5s

Code-level Performance​

// Virtualize long lists
import { useVirtualizer } from "@tanstack/react-virtual";

function LargeList({ items }: { items: Item[] }) {
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});

return (
<div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: virtualItem.start,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}

Image Optimization​

// Use Next.js Image for automatic optimization
import Image from "next/image";

<Image
src="/hero.png"
alt="Hero image"
width={800}
height={400}
priority // For above-the-fold images
placeholder="blur"
/>

Accessibility Standards​

WCAG 2.1 AA Compliance​

All components must meet WCAG 2.1 Level AA standards.

Keyboard Navigation​

// All interactive elements must be keyboard accessible
function MenuItem({ onClick, children }: Props) {
return (
<button
onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onClick();
}
}}
tabIndex={0}
role="menuitem"
>
{children}
</button>
);
}

ARIA Requirements​

// Provide proper ARIA attributes
function Modal({ isOpen, onClose, title, children }: Props) {
return (
<dialog
open={isOpen}
aria-labelledby="modal-title"
aria-describedby="modal-description"
aria-modal="true"
role="dialog"
>
<h2 id="modal-title">{title}</h2>
<div id="modal-description">{children}</div>
<button onClick={onClose} aria-label="Close modal">
X
</button>
</dialog>
);
}

Color Contrast​

  • Text contrast ratio: >= 4.5:1 for normal text
  • Large text contrast ratio: >= 3:1
  • Use design system tokens which are pre-validated

Screen Reader Support​

// Live regions for dynamic content
function Notification({ message }: { message: string }) {
return (
<div role="alert" aria-live="polite">
{message}
</div>
);
}

// Visually hidden text for screen readers
function IconButton({ icon, label, onClick }: Props) {
return (
<button onClick={onClick} aria-label={label}>
<Icon name={icon} aria-hidden="true" />
</button>
);
}

Testing Requirements​

Coverage Targets​

TypeTarget Coverage
Unit tests80% statements
Integration testsCritical paths
E2E testsHappy paths + error cases

Unit Testing​

import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

describe("TaskCard", () => {
it("displays task title and status", () => {
render(<TaskCard task={mockTask} />);

expect(screen.getByText(mockTask.title)).toBeInTheDocument();
expect(screen.getByText(mockTask.status)).toBeInTheDocument();
});

it("calls onStatusChange when status is updated", async () => {
const onStatusChange = vi.fn();
render(<TaskCard task={mockTask} onStatusChange={onStatusChange} />);

await userEvent.click(screen.getByRole("button", { name: /change status/i }));
await userEvent.click(screen.getByRole("option", { name: /completed/i }));

expect(onStatusChange).toHaveBeenCalledWith(mockTask.id, "completed");
});
});

Hook Testing​

import { renderHook, waitFor } from "@testing-library/react";

describe("useTask", () => {
it("fetches and returns task data", async () => {
const { result } = renderHook(() => useTask("task-123"));

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.task).toEqual(expect.objectContaining({
id: "task-123",
}));
});

it("handles error state", async () => {
server.use(
rest.get("/api/task/:id", (req, res, ctx) => {
return res(ctx.status(500));
})
);

const { result } = renderHook(() => useTask("task-123"));

await waitFor(() => {
expect(result.current.isError).toBe(true);
});
});
});

E2E Testing​

import { test, expect } from "@playwright/test";

test("user can create and complete a task", async ({ page }) => {
await page.goto("/tasks");

// Create task
await page.click('[data-testid="create-task-button"]');
await page.fill('[data-testid="task-title-input"]', "New Task");
await page.click('[data-testid="submit-task-button"]');

// Verify created
await expect(page.locator('text="New Task"')).toBeVisible();

// Complete task
await page.click('[data-testid="task-checkbox-New Task"]');
await expect(page.locator('[data-testid="task-status-New Task"]'))
.toHaveText("completed");
});

Code Review Checklist​

Before submitting a PR, verify:

  • No TypeScript errors (pnpm typecheck)
  • No lint errors (pnpm lint)
  • Tests pass (pnpm test)
  • No console.log statements
  • No TODO/FIXME without issue reference
  • Components have proper TypeScript types
  • User-facing text is internationalized
  • Interactive elements are keyboard accessible
  • Loading and error states are handled
  • No prop drilling beyond 2 levels