skills/frontend-component-style/SKILL.md
Frontend component file structure, naming, and layer separation for new or refactored components.
npx skillsauth add arndvs/ctrlshft frontend-component-styleInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Output "Read Frontend Component Style skill." to chat to acknowledge you read this file.
Use when CREATING a new component, REFACTORING an existing one, SPLITTING a large file, EXTRACTING data or logic, or DECIDING where a piece of code should live. Triggers on phrases like 'build a component', 'scaffold this', 'sketch a layout', 'prototype this', 'extract this into', 'split this component', 'is this file too big', 'where should this live', 'promote to production'. Do NOT use for small edits, bug fixes, or style tweaks — path-gated rules cover those.
This skill answers two structural questions: where does each piece of code live, and what is each piece named (including the names of related types and interfaces). Styling and runtime concerns (Tailwind tokens, dark-mode variants, server/client split, animation) and accessibility requirements (ARIA semantics, keyboard navigation, focus management, reduced-motion handling, CLS-safe variants) are owned by the relevant path-gated rules listed at the bottom — trust them; don't duplicate, but do preserve those requirements when creating or refactoring components. TypeScript typing patterns themselves remain in rules/typescript-conventions.md.
Single self-contained TSX files and four-layer split files are opposite structures. Always pick mode FIRST.
Explicit word in user request
Context signals (only if no explicit word)
prototypes/, sandbox/, experiments/, demos/ → Prototypeapp/, src/components/, src/features/, lib/ → Production*-content.ts / format-*.ts files → ProductionAsk (if neither signal is decisive — DO NOT GUESS)
"Is this a prototype/sketch (single file with inline data) or production code (separated into data, logic, primitives, composed)?"
The first reply after invoking this skill MUST start with one line:
Mode: prototype
or
Mode: production
The choice persists for the rest of the conversation. The user overrides with one word ("make it production", "this is a prototype actually").
When to use: sketching an idea, exploring a layout, throwaway experiments, components destined for CMS handoff where speed matters more than long-term maintainability.
.tsx filecomponentData JSON object at the top// user-dashboard-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
// ===== Component Data =====
const componentData = {
title: "User Dashboard",
metrics: [
{ id: "users", label: "Active Users", value: "5,234", trend: "+12%" },
{ id: "revenue", label: "Monthly Revenue", value: "$12,345", trend: "+8%" },
],
};
// ===== Helper Functions =====
const formatTrend = (trend: string) => {
const isPositive = trend.startsWith("+");
return {
value: trend,
className: isPositive ? "text-green-500" : "text-red-500",
icon: isPositive ? "↑" : "↓",
};
};
// ===== Component =====
const UserDashboardCard = () => (
<Card>
<CardHeader><CardTitle>{componentData.title}</CardTitle></CardHeader>
<CardContent>
{componentData.metrics.map((metric) => (
<MetricRow key={metric.id} {...metric} />
))}
</CardContent>
</Card>
);
// ===== Subcomponents =====
interface MetricRowProps {
label: string;
value: string;
trend: string;
}
const MetricRow = ({ label, value, trend }: MetricRowProps) => {
const trendData = formatTrend(trend);
return (
<div>
<span>{label}</span>
<span>{value}</span>
<span className={trendData.className}>{trendData.icon} {trendData.value}</span>
</div>
);
};
export default UserDashboardCard;
When to use: code going into the main app, components that will be tested, reused, or maintained by others.
| Layer | Job | File suffix | Example |
|---|---|---|---|
| Data | Static content, CMS text, config | *-content.ts | dashboard-metrics-content.ts |
| Logic | Pure functions, formatters, transformers | verb-noun .ts | format-metric-trend.ts |
| Primitive | Single-element UI, no internal state | descriptive .tsx | trend-badge.tsx |
| Composed | Assembles primitives into a section | descriptive .tsx | metric-card.tsx |
Page-level view → Composed → Primitive → Logic + Data
components/ui/*, framework helpers); never import other feature Primitives or Composed components// dashboard-metrics-content.ts
export type DashboardMetric = {
id: string;
label: string;
value: string;
trend: number;
};
export const dashboardMetrics: DashboardMetric[] = [
{ id: "active-users", label: "Active Users", value: "5,234", trend: 12 },
{ id: "monthly-revenue", label: "Monthly Revenue", value: "$12,345", trend: 8 },
];
// format-metric-trend.ts
export type TrendDirection = "up" | "down" | "flat";
export const getTrendDirection = (trend: number): TrendDirection => {
if (trend > 0) return "up";
if (trend < 0) return "down";
return "flat";
};
export const formatTrendLabel = (trend: number): string =>
trend === 0 ? "No change" : `${trend > 0 ? "+" : ""}${trend}%`;
// trend-badge.tsx
import { getTrendDirection, formatTrendLabel } from "@/lib/format-metric-trend";
interface TrendBadgeProps {
trend: number;
}
const TrendBadge = ({ trend }: TrendBadgeProps) => {
const direction = getTrendDirection(trend);
return <span data-direction={direction}>{formatTrendLabel(trend)}</span>;
};
export default TrendBadge;
// metric-card.tsx
import { Card, CardContent } from "@/components/ui/card";
import TrendBadge from "@/components/trend-badge";
import type { DashboardMetric } from "@/content/dashboard-metrics-content";
interface MetricCardProps {
metric: DashboardMetric;
}
const MetricCard = ({ metric }: MetricCardProps) => (
<Card>
<CardContent>
<span>{metric.label}</span>
<span>{metric.value}</span>
<TrendBadge trend={metric.trend} />
</CardContent>
</Card>
);
export default MetricCard;
// dashboard-metrics-section.tsx (page-level view)
import { dashboardMetrics } from "@/content/dashboard-metrics-content";
import MetricCard from "@/components/metric-card";
const DashboardMetricsSection = () => (
<section>
{dashboardMetrics.map((metric) => (
<MetricCard key={metric.id} metric={metric} />
))}
</section>
);
export default DashboardMetricsSection;
The four-layer rule is a constraint on dependencies, not a minimum file count. A 30-line component that passes the SRP test stays in one file. Don't extract:
Extract when there is a second consumer, non-trivial logic, or a real testing need.
.tsx for components, .ts for logic and datacard.tsx, utils.ts, helpers.ts, mgr.ts, widget.tsxurl, id, api)MetricTrendBadge, InvoiceLineItemRow, PlanUpgradeCalloutCard, Item, Widget, Section, DisplayComponentformatCurrency, getTrendDirection, buildInvoiceRowshandlePlanUpgrade, submitBillingForm, downloadInvoicePdfhandlePlanUpgrade not handleClick, handleInvoiceDownload not handleSubmit<ComponentName>PropsTrendDirection, PlanTier, InvoiceStatusProps (collides), ICard (Hungarian), T (meaningless)1. Imports
2. Types and interfaces
3. Content data (only if this file owns data)
4. Helper functions / hooks (Prototype mode only)
5. Component or function body
6. Subcomponents (Prototype mode only)
7. Default export
A reviewer should always know where to look for each kind of thing.
Describe what this does in one sentence without using "and".
If you can't, split.
| Pattern | Problem | Fix |
|---|---|---|
| const data = { ... } inline in production code | Hides content inside presentation | Move to <feature>-content.ts |
| utils.ts with many unrelated functions | Impossible to navigate | One function (or one tightly related group) per file, named after what it does |
| <Card /> as a component name | No hint of what it renders | <UserBillingCard />, <MetricSummaryCard /> |
| handleClick / handleSubmit on a specific component | No hint of what is being acted on | handlePlanUpgrade, handleInvoiceDownload |
| Formatting logic inside JSX | Mixes concerns, hard to test | Extract to a named function in a logic file |
| Mixed isLoading / isError / isEmpty in one render block | Tangled conditional logic | Each state gets its own named branch or component |
| Production component with inline JSON data | Should have been promoted | Run the Promote workflow below |
Triggered by phrases like "promote to production", "clean this up", "extract this properly", "this is going live".
componentData object to <feature>-content.ts with a typed export.<verb>-<noun>.ts files (e.g. format-metric-trend.ts).Mode: production in your next reply so the rest of the conversation uses production rules.These auto-load when you edit matching files. Do not duplicate their content here. Trust them.
| Rule | Scope |
|---|---|
| rules/dark-mode.md | **/*.{tsx,jsx,css,scss} — DMDS tokens, dark variants |
| rules/tailwind-shadcn.md | **/*.{tsx,jsx} — Tailwind grouping, shadcn imports, responsive |
| rules/server-vs-client-components.md | **/app/**/*.{tsx,jsx} — server-first, error handling |
| rules/framer-motion.md | **/*.{tsx,jsx} — animation philosophy, reduced motion |
| rules/typescript-conventions.md | **/*.{ts,tsx} — props/types, parameter style |
| rules/frontend-conventions.md | **/*.{ts,tsx,js,jsx,mjs,cjs,css,scss,html,svelte,vue} — browser baseline |
If a question is covered by a path-gated rule (Tailwind syntax, dark-mode tokens, when to use 'use client'), defer to the rule. This skill answers structure and naming only.
development
Use when implementing UI, checking dark/light mode, or validating animations — adds a visual feedback loop via browser screenshots so frontend changes are verified, not assumed.
development
Use when Claude Code sessions had many manual approval ("press 1") prompts or when auditing hook permissions; identifies which Bash commands required approval.
tools
Use after merging a PR or during periodic cleanup to archive plan-mode files by linking them to merged PRs.
testing
Use when stress-testing a plan against the project's domain model — grills the design, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise.