howells/skills

componentize

Consolidate shared UI, split multi-responsibility files or extract packages. Not for redesign (`chiaroscuro`), typography (`typecase`) or behaviour-preserving cleanup (`unslop`).

First seen May 22, 2026

Installation

$ npx skills add howells/skills --skill componentize

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from howells/skills · top by installs.

npx skills add howells/skills

Browse all from howells/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 24,479 B
  • docs SUMMARY.md 198 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 25 installs

SKILL.md

Componentize

Use this skill to interrogate a codebase for reuse opportunities before writing new UI. The goal is to reduce duplication, establish canonical shared components, and make app code consume those components through props, slots, composition, and clear package boundaries. Do not centralize the whole UI unless the user explicitly asks for a design-system project.

Start

When invoked:

  1. State that you are using the componentize skill.
  2. Identify the repo shape before editing:

- package manager and workspace config - apps and packages - whether turbo.json exists - whether a shared UI package already exists, commonly packages/ui, packages/design-system, packages/components, or libs/ui - UI framework, styling system, icon library, test setup, Storybook or examples

  1. If the user refers to a visible target by location, appearance, or interaction, resolve that target before searching broadly.
  2. Search before creating anything. Use rg, rg --files, and import relationships to find existing components, repeated markup, repeated Tailwind class strings, copied form controls, duplicated shells, and variant drift.
  3. If the user asks for implementation, first state the extraction target, canonical home, public API, call sites to migrate, and validation plan. Then edit narrowly and validate. If the user asks for analysis, return an audit with prioritized extraction candidates.

Core Principle

Prefer adaptation over addition:

  1. Reuse an existing component unchanged.
  2. Replace duplicate markup or one-off local components with the existing component that already owns the pattern.
  3. Add a prop, variant, slot, render prop, or composition point to an existing component.
  4. Extract a shared primitive from two or more local implementations.
  5. Create a new shared component only when no existing component can be honestly adapted.
  6. Create a new shared UI package only when reuse crosses app/package boundaries and the candidate API is domain-independent.

Do not create parallel components that differ only by copy, spacing, icon, color, or minor layout. Fold those differences into a single API unless doing so would make the component vague or overloaded. When an existing component already matches the behavior and accessibility contract, migrate call sites to that component instead of extracting another one.

Atomic Component Standard

Aim for the smallest useful component boundary. Extract primitives before extracting composed product UI, and make composed components depend on primitives rather than duplicating their markup or class strings.

Prefer this hierarchy:

  1. token or class helper: shared spacing, typography, color, or focus-ring rules
  2. primitive: Button, Input, Badge, TooltipTrigger, DialogContent
  3. compound primitive: Field, Menu, Tabs, Select, DataList
  4. composed surface: Toolbar, EmptyState, PageHeader, FilterBar
  5. app-specific composition: route-aware shells, business workflows, copy-heavy panels

Atomic does not mean tiny files for their own sake. A component is atomic when it has one stable responsibility, one accessibility contract, no hidden domain behavior, and an API that can be reused without dragging product assumptions into other call sites.

Variant And Composition Conventions

Follow the component conventions already present in the repo before introducing a new one:

  • If the repo uses shadcn-style primitives, prefer class-variance-authority (cva) plus VariantProps for semantic variants and sizes, cn/twMerge for consumer className merging, and Radix Slot/asChild for polymorphic leaf components such as buttons, links, triggers, and menu items.
  • If the repo uses Base UI from @base-ui-components/react, prefer its render prop composition model. For custom primitives that need the same polymorphism, use Base UI's useRender and mergeProps patterns rather than inventing an asChild clone.
  • If the repo uses older MUI Base-style components, respect its slots and slotProps customization model instead of mixing in Radix Slot or Base UI render patterns.
  • If the repo uses tailwind-variants, Stitches, Vanilla Extract, CSS modules, Panda, or another established variant system, extend that system instead of adding cva for one extraction.
  • If there is no established pattern, use cva only when the component has observed semantic variants or sizes across multiple call sites. Use a plain cn(...) class merge for one-off primitives with no variant matrix.

For polymorphic components:

  • expose asChild only when the implementation uses a Slot-compatible library and the component can safely pass props, events, refs, accessibility attributes, and data attributes to exactly one child
  • expose Base UI-style render only when the repo uses @base-ui-components/react conventions or the component genuinely needs render-prop composition
  • avoid polymorphism for components with required native-only props, internal DOM structure, or accessibility semantics that would become invalid on arbitrary elements
  • document or type the default element and required child/ref behavior in the component API

Server And Client Boundaries

In React Server Component frameworks such as Next.js App Router, prefer Server Components by default and keep client boundaries as small as the interaction requires.

When componentizing in an RSC-aware app:

  • identify files marked with 'use client' and inspect their import trees before extracting or moving UI
  • avoid turning a large route, layout, shell, table, form, or panel into a Client Component only because one nested control needs state, effects, browser APIs, context, or event handlers
  • extract the interactive part into the smallest practical Client Component and render it inside a Server Component that owns data fetching, server-only modules, secrets, routing params, static markup, and non-interactive layout
  • pass serializable data from Server Components into Client Components; allow supported Server Function references, but do not pass ordinary callbacks, unsupported class instances, server-only objects, or other non-serializable data across the boundary
  • use children or explicit slots to visually nest server-rendered UI inside a small client wrapper when the interaction is only a shell such as a modal, disclosure, tabs state, or popover
  • keep providers and third-party client-only wrappers as deep in the tree as possible instead of wrapping an entire app, layout, or page by default
  • preserve server-only and client-only guard imports when they exist, and add them only if the repo already uses that convention or boundary mistakes are a concrete risk

If duplicate code exists because a server file cannot import a client-only component, do not solve it by making everything client-side. Split the shared presentational primitive into an environment-agnostic component when possible, then layer a small Client Component around the interactive behavior.

Reconnaissance

Read the code, not just filenames.

Visual Target Resolution

When the user says something like "componentize the thing on the right", "the left panel", "that header", "the card grid", or "the sidebar":

  1. Inspect the running app if available. Use the browser or screenshot tool preferred by the environment.
  2. Identify the visible region by screen position, nearby text, role, landmark, route, or interaction.
  3. Map the region back to source by checking DOM labels/classes when possible, then searching for distinctive text, aria labels, test ids, component names, route files, and nearby icon names.
  4. If several regions could match, ask one concise clarification question before editing.
  5. Once mapped, name the target explicitly in the audit or implementation checkpoint:

- visible target: "right inspector panel" - source owner: path/to/file.tsx - candidate extraction: InspectorPanel - consumers/call sites found

Do not guess from layout direction alone. The screen target must be tied to source evidence before extraction.

Check:

  • package.json, workspace files, turbo.json, tsconfig*, build scripts, lint scripts, test scripts
  • app routes/pages and feature directories
  • components/, ui/, design-system/, shared/, lib/, and package entrypoints
  • imports from local components, package components, shadcn/radix/headless libraries, icon libraries, and CSS utilities
  • repeated class strings and repeated JSX structures
  • existing components whose props and accessibility contract already cover duplicated local code
  • whether components accept className, children, asChild, variant props, size props, state props, and accessible labels
  • whether the existing stack uses cva, tailwind-variants, Radix Slot/asChild, Base UI render/useRender, MUI Base slots/slotProps, CSS modules, or another variant/composition convention
  • whether the framework uses Server Components, 'use client' directives, server-only/client-only guard imports, server actions, route handlers, or serializable Server-to-Client props

Useful searches:

rg --files -g '*.{tsx,jsx,ts,js}' | rg '(^|/)(components|ui|design-system|shared|app|pages|src)/'
rg "className=['\"][^'\"]{60,}['\"]" -g '*.{tsx,jsx}'
rg 'className=\{|cn\(|clsx\(|cva\(|tv\(|data-slot=' -g '*.{tsx,jsx}'
rg '(Button|Card|Modal|Dialog|Input|Select|Badge|Avatar|Tabs|Table|Toast|Tooltip)' -g '*.{tsx,jsx}'
rg '@radix-ui|@base-ui|@mui/base|class-variance-authority|tailwind-variants|Slot|asChild|useRender|slotProps|variant' -g '*.{tsx,jsx,ts,js}'
rg "['\"]use client['\"]|['\"]use server['\"]|server-only|client-only|useState\\(|useEffect\\(|window\\.|document\\.|localStorage" -g '*.{tsx,jsx,ts,js}'
rg --files -g '*.{tsx,jsx}' | sed 's#.*/##' | sort | uniq -d

Discover source roots before running path-specific searches. Do not assume a fixed set of source directories exists; inspect the repo and search only directories that are actually present, such as app, apps, src, packages, libs, frontend, or web.

For import relationships, start with text search:

rg "^import .* from ['\"](\.|@|~|[A-Za-z])" -g '*.{ts,tsx,js,jsx}'
rg 'export .* from|export \{' -g 'index.{ts,tsx,js,jsx}' -g 'package.json'

Use graph tools such as madge, dependency-cruiser, or package-boundary checkers only when they are already installed or clearly part of the repo. Do not add new graph tooling just to complete an audit unless the user asks.

Turborepo Decision

If the project is a Turborepo:

  • Use an existing UI package when one exists and its purpose matches the work.
  • Prefer packages/ui as the canonical home when there is no established convention.
  • Keep package exports narrow and intentional.
  • Make consuming apps import through the package public API. Deliberate subpath exports such as @repo/ui/button are fine; private source-file imports are not.
  • Add missing package dependencies where imports cross package boundaries.

When creating or changing a UI package, check:

  • package.json exports for JS and type entrypoints
  • CSS/style exports and sideEffects when CSS imports must be preserved
  • whether consumers need source transpilation, package build output, or direct TypeScript exports
  • Tailwind v4 source scanning, including @source for package classes when needed
  • peer dependencies for React, framework packages, Radix/headless libraries, Tailwind helpers, and icons
  • package-level examples, stories, or a consumer import smoke test

If no shared package exists, recommend creating one only when the evidence supports cross-boundary reuse:

  • two or more apps duplicate UI primitives or layouts
  • a component is already copied across packages
  • app code imports across app boundaries
  • a reusable design system is emerging in more than one place
  • repeated UI is blocking consistent accessibility, theming, or interaction behavior
  • the candidate API is domain-independent enough to serve all known consumers

Default to an app-local shared folder when reuse is within one app, duplication is shallow, or the API is still product-specific. Avoid creating a package for a single app unless the local component tree is already large enough that a package boundary would simplify ownership.

Extraction Targets

Prioritize components with high reuse value and low domain coupling:

  • primitives: Button, IconButton, LinkButton, Input, Textarea, Select, Checkbox, Switch, RadioGroup, Badge, Avatar, Tooltip
  • UI surfaces: Card, Panel, EmptyState, ErrorState, LoadingState, Dialog, Sheet, Popover
  • data display: Table, DataList, Stat, MetricCard, Pagination, Tabs
  • forms: Field, FieldLabel, FieldError, FormSection, SearchInput

Conditional targets:

  • layout shells: AppShell, PageHeader, Section, Toolbar, Sidebar, Breadcrumbs

Be cautious with layout shells and any component that carries product-specific data fetching, auth, analytics, feature flags, route assumptions, navigation state, permissions, or business copy. Extract the presentational part and leave domain orchestration in the app.

Component API Rules

Shared components should:

  • accept className unless there is a strong reason not to
  • keep layout margins with callers; components may own internal padding and gaps
  • expose variants for real semantic differences, not every visual one-off; when using cva or an equivalent, keep variant names semantic (intent, tone, size) rather than tied to one caller
  • map every new prop or variant to observed call-site differences; reject speculative options until another concrete consumer needs them
  • prefer children, slots, or composition for complex content
  • keep primitives leaf-like and slot-friendly when the stack supports it; avoid baking icons, labels, links, or layout wrappers into the primitive when those can be composed by the caller
  • forward refs and spread valid DOM props for primitives that wrap an interactive or focusable element
  • merge event handlers, refs, className, style, data attributes, and ARIA props using the repo's existing helper (cn, mergeProps, Slot, or equivalent)
  • preserve accessibility names, focus states, keyboard behavior, and ARIA contracts
  • keep environment-agnostic primitives free of 'use client' unless they directly use client-only APIs; create a thin client wrapper for stateful behavior instead of marking a broad shared component client-side
  • avoid importing app-only modules, route helpers, environment variables, or server-only code
  • keep dependency weight low; app frameworks should be peer dependencies when appropriate

Do not make a component so configurable that it hides five unrelated designs. If variants conflict conceptually, split the component or extract a smaller primitive. Do not use asChild, render, or slot props as an escape hatch for unclear ownership. They are for preserving valid semantics and composition, not for hiding unrelated components behind one API. Do not move a component across a server/client boundary until you know whether its imports, props, and children are valid in the target environment.

Two jobs beyond the UI

The work above is UI-shaped. Splitting an overloaded file and lifting code into its own package use the same reconnaissance and the same rule - prove the duplication before consolidating it - so they live here.

Splitting a god file

A file carrying so many responsibilities that changing one means reading all of them. Length alone is never the reason to split. Split when the file has multiple responsibilities, duplicated logic, unstable change reasons, hard-to-test branches, or a public interface that can be made smaller.

Find candidates with the scanner, then read them - it ranks files heuristically and the judgement is still yours:

python3 scripts/find-god-files.py /path/to/repo
python3 scripts/find-god-files.py /repo --min-score 20 --duplicate-window 10
python3 scripts/find-god-files.py /repo --include-tests --json

It scores length and responsibility signals, flagging files that reach 35 by default; at the default threshold a file of 300 or more lines can be listed on length alone. Inspect the reasons and source before deciding whether it actually mixes responsibilities. Lower --min-score to widen the net.

Classify each confirmed candidate as a god component (rendering, data shaping, effects, mutations, validation and subview control in one file), a god script (argument parsing, I/O, domain logic, formatting and side effects mixed), a god module (non-UI, multiple unrelated responsibilities), or duplication.

Then split in this order, one candidate at a time, running the smallest relevant check after each step:

  1. Pure helpers.
  2. Duplicated logic, before moving any caller.
  3. Hooks and state machines, before child components, where state is tangled.
  4. Leaf subcomponents, before layout shells, where JSX is large.
  5. I/O adapters away from domain logic, in scripts.

Keep public imports stable until tests pass, then clean up barrels and exports.

For components: move data fetching into a hook only when it is reused or removes real branching; move repeated JSX into named domain components rather than a generic Content or Wrapper; keep state near the component that owns the interaction. Avoid many shallow files where each must be opened to understand one small behaviour.

For scripts: separate argument parsing, configuration, transformation, I/O and output formatting; put deterministic logic behind a unit-testable function; keep the entrypoint thin; preserve exit codes and stream behaviour.

For duplication: consolidate only after confirming the copies are meant to stay the same. Coincidentally similar code with different domain meaning stays apart, and shared schemas come out only when they represent the same concept.

Extracting a standalone package

Where the target is a publishable package or its own repository rather than a shared folder, the package decision above still applies and these carry the rest. The full validation checklist is in [references/package-extraction.md](references/package-extraction.md).

Define the boundary before editing. In: the component, hook, utility, its types, styles, assets, fixtures, tests, intrinsic support modules, and examples demonstrating the intended API. Out: routes, auth and session code, database clients, environment access, product copy, analytics, feature flags, unrelated design-system wrappers, and compatibility shims for old import paths. If the extraction exposes hidden coupling, report it before editing and choose the smallest coherent boundary.

Design the public API before creating files: package name, entrypoints, exported symbols, peer versus bundled dependencies, styling contract, asset handling, and server/client runtime constraints. Prefer a narrow API - exporting every internal file because it exists is how a package becomes unmaintainable.

For a publishable npm package: an explicit exports map, emitted or referenced types, a files field, and React, framework and styling libraries marked as peers when consumers must provide them.

Moving to a separate repo copies first and leaves the original intact unless removal was explicitly requested. For extraction within one repo, migrate the in-scope callers and remove their superseded implementation by default. Keep a deliberate staged migration only when requested or required by an existing contract, and record its ownership. A standalone destination repo may intentionally leave the source app unchanged until a separate adoption request.

Never auto-publish, tag, push or create a release without explicit approval, and never change licensing, publishing visibility, registry scope or package ownership silently.

Workflow

1. Inventory

Produce a concise inventory:

  • existing shared UI locations
  • duplicated patterns and where they appear
  • existing components that can replace duplicate code without API changes
  • components that should be adapted with props
  • components that should be extracted
  • components that should remain app-local

2. Choose Canonical Homes

For each candidate, choose one:

  • existing component: adapt API and migrate call sites
  • existing UI package: move or add component there
  • new UI package: create the smallest viable package only when the package threshold is met
  • app-local shared folder: use only when reuse is intra-app
  • no action: duplication is superficial or domain-specific

3. Design The API

Define the public component API before moving files:

  • component name and import path
  • props and variants, including whether variants are implemented with cva, tailwind-variants, CSS modules, or an existing local helper
  • composition/slot model, including whether polymorphism uses Radix/shadcn asChild, Base UI render/useRender, MUI Base slots/slotProps, or no polymorphism
  • server/client boundary, including whether the component stays server-rendered, remains environment-agnostic, or needs a small 'use client' wrapper
  • styling/theming contract
  • accessibility contract
  • examples or tests that prove expected usage

4. Implement In Small Slices

Extract one coherent group at a time. Update imports and call sites immediately so the repo does not keep two canonical versions.

Load chiaroscuro only when the request needs visual direction and polish; load typecase when the duplicated styling is typographic. Deduplication and shared-component decisions stay in this skill, and its componentization workflow is the controlling scope.

5. Validate

Run the narrowest meaningful checks:

  • typecheck for touched packages/apps
  • lint for touched packages/apps
  • relevant tests
  • Storybook/examples build when shared UI examples exist
  • consuming app build when package exports or imports changed
  • consumer import smoke test proving exported JS, types, and styles resolve when a shared package changed
  • rendered UI smoke check for affected pages/components when a runnable app or Storybook exists
  • keyboard/focus checks for interactive primitives such as dialogs, selects, tabs, and buttons
  • bundle or build smoke check when a change moves 'use client' boundaries, imports client-only libraries from server code, or changes package entrypoints used by Server Components

If checks are unavailable or too broad, say exactly what was and was not verified.

Output

When returning an analysis, use this shape:

## Reuse Audit

### Existing Shared Surface
- `path`: purpose, health, import pattern

### Highest-Value Opportunities
| Priority | Pattern | Evidence | Recommendation | Why |
|---|---|---|---|---|

### Proposed Shared API
- `ComponentName`: props, variants, slots, import path

### Package Plan
- Use existing package / create `packages/ui` / keep app-local
- Export strategy
- Dependency changes
- Server/client boundary notes for RSC-aware apps

### Migration Plan
1. Extract/adapt component
2. Migrate call sites
3. Delete duplicate implementations
4. Validate

### Risks
- Coupling, accessibility, styling, or package-boundary concerns

In the Highest-Value Opportunities table, evidence should include representative file:line references, occurrence or call-site count, current owner/home, and whether the action is reuse unchanged, adapt, extract, move, or no action.

Guardrails

  • Do not start by creating new components; search for reuse first.
  • Do not leave duplicated canonical implementations after migration unless the user asks for a staged rollout.
  • Do not move domain behavior into a shared UI package.
  • Do not weaken package boundaries to make imports pass.
  • Do not perform broad redesign while componentizing unless the user asked for visual redesign.
  • Do not introduce a component library dependency without checking the existing stack and tradeoffs.

Completion Check

Before finishing, verify:

  • every new or changed shared component has a clear consumer
  • duplicate implementations were removed or explicitly left for a stated reason
  • imports use the canonical public path
  • props/composition cover known variants without overfitting
  • validation ran or the gap is reported