react-ui-components
Skill18 starsChanged 4 months agoMITView on GitHub
Building React UI components for SPAs styled with Tailwind CSS v4 and a token-based design system in core/ui that mirrors the Figma Design System and Foundations - Figma variable names as theme tokens, Figma text styles as type utilities, shadcn/ui components copied in and restyled on Radix primitives, the cn helper and cva variants, prop design (className passthrough, native prop spreading, ref as a prop), icons exported from Figma as SVG components, the Icon and IconButton wrappers, slots through children and asChild, theme tokens instead of raw colors or dark: variants, light and dark following the system, user-facing text through react-i18next (namespaces, typed keys, plurals, interpolation, date and number formatting), accessibility basics, and dev-only previews for every design system component. Use this skill whenever writing or changing a component that is not a Root/Screen (buttons, cards, list items, inputs, dialogs, menus, toolbars), adding a design system component, adding an icon, styling with Tailwind, adding translations, or reviewing accessibility. For Root/Screen components, see react-presentation-mvi. Trigger on phrases like "create a component", "Figma component", "SVG", "button", "card", "dialog", "input", "text field", "dropdown", "menu", "shadcn", "Radix", "Tailwind", "className", "cn", "cva", "variant", "icon", "IconButton", "design system", "theme", "color", "dark mode", "token", "translation", "i18n", "t(", "useTranslation", "plural", "format date", "aria-label", "accessibility", "a11y", or "preview".
In this repository67 other files it gives its agents.
- agent-onboardingSkill
- android-backgroundSkill
- android-cliSkill
- android-compose-architectureSkill
- android-compose-componentsSkill
- android-data-layerSkill
- android-di-koinSkill
- android-error-handlingSkill
- android-integration-testingSkill
- android-jacoco-setupSkill
- android-jvm-test-coverageSkill
- android-module-structureSkill
- android-navigationSkill
- android-presentation-mviSkill
- android-preview-figma-verifySkill
- android-ui-screenshot-verifySkill
- android-ui-testingSkill
- android-ui-verifySkill
- android-unit-testingSkill
- android-version-catalogSkill
- architecture-decision-recordSkill
- careful-planningSkill
- compose-adaptive-layoutsSkill
- figma-to-android-vectorSkill
- finalizeSkill
- git-commitSkill
- handoffSkill
- kmp-compose-architectureSkill
- kmp-compose-componentsSkill
- kmp-data-layerSkill
- kmp-di-koinSkill
- kmp-error-handlingSkill
- kmp-integration-testingSkill
- kmp-migration-analyzerSkill
- kmp-module-structureSkill
- kmp-navigationSkill
- kmp-presentation-mviSkill
- kmp-ui-testingSkill
- kmp-unit-testingSkill
- kotlin-coroutinesSkill
- kotlin-flowsSkill
- kotlin-mutation-testingSkill
- openspec-artifact-review-lightSkill
- openspec-artifact-reviewSkill
- project-breakdownSkill
- react-data-layerSkill
- react-error-handlingSkill
- react-integration-testingSkill
- react-presentation-mviSkill
- react-project-structureSkill
- react-routingSkill
- react-screen-architectureSkill
- react-securitySkill
- react-server-stateSkill
- react-unit-testingSkill
- speckit-parallelize-agent-teamsSkill
- speckit-parallelize-subagentsSkill
- spring-api-layerSkill
- spring-di-configurationSkill
- spring-integration-testingSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
14 rules. About 0 are common, 6 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 28
Add a preview section (see Previews).
- Distinctive: line 39
Remove variants the Figma component doesn't have, and add the ones it has.
- Very specific: line 97
If twMerge doesn't resolve a conflict between two custom utilities (a custom type style and a font size, say), register them with …
- Very specific: line 104
No raw values: No hex colors in components, no arbitrary values like bg-[#fff] or p-[13px].
- Very specific: line 110
Every theme token is named after its Figma variable: / becomes -, and a trailing /default is dropped.
- Distinctive: line 125
When Figma adds or renames a variable, update the theme in the same change.
- Very specific: line 141
classNameis theModifier: Every component that renders an element accepts className and merges it onto the outer element with cn. - Very specific: line 144
Booleans: read as a state: isSelected, isLoading.
- Distinctive: line 194
All user-facing text comes from translation files through react-i18next.
- Very specific: line 218
Plurals: use count and _one / _other suffixes.
- Very specific: line 228
Every interactive element has an accessible name: visible text, a <label>, or aria-label for icon-only controls.
- Distinctive: line 231
Use Radix-based components for dialogs, menus, popovers, tabs and tooltips.
- Distinctive: line 232
Don't use color alone to convey state.
- Very specific: line 239
Every core/ui component has a section on the dev-only design system preview page.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
React UI Components
Scope
This skill covers every component that is not a page-level Root or Screen: design system components in src/core/ui/ and feature components in src/features/<feature>/components/.
- A component used by one feature lives in that feature.
- It moves to
core/uiwhen a second feature needs it. It then loses every feature-specific word: all text comes in through props.
The Design System (core/ui)
core/ui is the code version of the Figma file's Design System page (components) and Foundations page (tokens). Figma is the source of truth: a component's variants, sizes, states and slots are the ones in Figma, no more and no fewer.
core/ui is the only place that imports Radix. Features import from @/core/ui/..., which lint enforces (see react-project-structure).
From Figma to core/ui
- Find the component on the Design System page and read it with the Figma MCP (
get_design_context,get_screenshot). If the MCP call fails, stop and report it. Never build a component from a guess. - Build it as described below, with the variants Figma defines.
- Add a preview section (see Previews).
shadcn/ui as the base
shadcn/ui components are copied into the project, not installed as a package. They give us Radix behavior (focus handling, keyboard support, ARIA) and a component structure. Their look is replaced with our tokens.
To add one:
- Generate it with the shadcn CLI (or copy it from the docs) into
src/core/ui/. - Replace every shadcn color and radius class (
bg-primary,text-muted-foreground,rounded-mdfrom their scale) with the project's token classes. - Rename the file to PascalCase and switch to named exports,
typeand our prop conventions below. - Remove variants the Figma component doesn't have, and add the ones it has.
- Add a preview section (see Previews).
Like Material 3 on Android, the library is the starting point for behavior, not for the visual design.
cn and variants
// src/core/ui/cn.ts
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}// src/core/ui/Button.tsx
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-6 type-label transition-colors focus-visible:outline-2 focus-visible:outline-accent disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-accent text-background-surface',
secondary: 'bg-background-surface text-text-primary border border-border-strong',
ghost: 'text-text-primary hover:bg-accent-soft',
danger: 'bg-danger text-background-surface',
},
size: {
md: 'h-10 px-4',
lg: 'h-12 px-5',
},
},
defaultVariants: { variant: 'primary', size: 'md' },
},
)
type ButtonProps = ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
isLoading?: boolean
asChild?: boolean
}
export function Button({ variant, size, isLoading, asChild, className, children, disabled, ...props }: ButtonProps) {
const Component = asChild ? Slot : 'button'
return (
<Component
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled || isLoading}
aria-busy={isLoading || undefined}
{...props}
>
{children}
</Component>
)
}- The classes above show the token names. The real variants, sizes and styles come from the Button in Figma.
- Variants use
cva, as shadcn does. Variant names follow the Figma variant names and describe purpose (primary,danger), not color (blue). - If
twMergedoesn't resolve a conflict between two custom utilities (a custom type style and a font size, say), register them withextendTailwindMergeincn.ts.
Styling with Tokens
- Colors, radii, spacing, breakpoints and type styles come only from the theme. The theme replaces Tailwind's default palette, so
bg-blue-500produces no CSS. - No raw values. No hex colors in components, no arbitrary values like
bg-[#fff]orp-[13px]. Arbitrary values can't be blocked by lint, so review catches them. - No
dark:variants. Each token class points at a CSS variable, and the theme swaps the variable values underprefers-color-scheme: dark.bg-background-surfaceis correct in both themes. The theme always follows the system. There is no in-app theme switch. - The theme lives in
src/core/ui/theme/. Read values with the Figma MCP (get_variable_defs), never from screenshots.
Token names come from Figma
Every theme token is named after its Figma variable: / becomes -, and a trailing /default is dropped.
| Figma variable | Theme variable | Example class |
background/app, background/surface, background/column, background/sidebar | --color-background-app, ... | bg-background-surface |
text/primary, text/muted, text/faint | --color-text-primary, ... | text-text-muted |
border/default, border/strong | --color-border, --color-border-strong | border-border |
accent/default, accent/soft | --color-accent, --color-accent-soft | bg-accent-soft |
danger/default, success/default | --color-danger, --color-success | text-danger |
label/red ... label/slate (20 label colors) | --color-label-red, ... | bg-label-red |
radius/4 ... radius/12, radius/full | --radius-4, ... | rounded-8, rounded-full |
- Spacing uses Figma's steps only: 4, 8, 12, 16, 20, 24 and 32 px. With a 4 px spacing base these are
1,2,3,4,5,6and8(p-3,gap-2). Don't use other steps. - Type styles are utilities named after the Figma text styles:
type-page-title,type-screen-title,type-card-title,type-body,type-label,type-metadata,type-section-label. Each one sets font family (Geist or Geist Mono), size, weight, line height and letter spacing, including the larger phone sizes Figma lists. Don't combinetext-*size,font-*weight andleading-*by hand. - Breakpoints come from Figma Foundations:
tabletfrom 640 px,desktopfrom 1024 px. They are marked as a proposal in Figma, so they are defined only in the theme and nowhere else. - When Figma adds or renames a variable, update the theme in the same change. Never add a token that has no Figma variable.
Prop Design
type NoteCardProps = {
note: Note
isSelected: boolean
onSelectedChange: (selected: boolean) => void
className?: string
}- Props in order: required data first, then state, then callbacks, then
className. classNameis theModifier. Every component that renders an element acceptsclassNameand merges it onto the outer element withcn. The parent controls placement and spacing (margin, width, grid position). The component controls its own look.- Wrapping a native element: take
ComponentProps<'button'>(or the element in question) and spread the rest onto it, sotype,aria-*,data-*and event handlers pass through. refis a regular prop in React 19. NoforwardRef. Input components must passrefto theinputso React Hook Form'sregisterworks.- Booleans read as a state:
isSelected,isLoading. Native names stay native:disabled,required. - Callbacks are
on<Event>:onClick,onSelectedChange. A controlled value isvalue+onValueChange(oropen+onOpenChange), matching Radix. - Pass the smallest thing the component needs. A component that shows a title and a date gets
titleandupdatedAt, or the domain object if it really uses most of it. Never pass a query result object or the whole screen state.
Local state
Components may hold state that only affects how they look: hover, a menu being open, a tooltip, which tab of a self-contained widget is active. Make it controlled (open/onOpenChange) only when a parent needs to drive it. Radix components already manage their own open state when uncontrolled.
Icons
<Icon icon={PlusIcon} /> // default size, hidden from screen readers
<Icon icon={StarIcon} label={t('notes:favorite')} />
<IconButton icon={TrashIcon} label={t('notes:delete')} onClick={...} />Icons are SVGs exported from Figma. There is no icon library.
// src/core/ui/icons/TrashIcon.tsx
export function TrashIcon(props: ComponentProps<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" {...props}>
<path d="..." />
</svg>
)
}- One file per icon in
src/core/ui/icons/, named<Name>Icon.tsxafter the Figma icon. Add an icon when a feature first needs it, exported from Figma with the MCP. Never draw or copy an icon from somewhere else. - Replace every hard-coded fill and stroke color with
currentColor. Keep theviewBoxfrom Figma, remove fixedwidthandheight. - Icons are rendered through
IconorIconButtonfromcore/ui, which limit size to the design system's options. Never render an icon component directly. - An icon next to visible text has no label and is hidden from assistive technology.
- An icon-only button requires
label.IconButtonmakes it a required prop, so a missing label is a type error. - Icon color follows the text color (
currentColor). Set color on the parent with a token class.
Slots
childrenis the main slot.- Named slots are
ReactNodeprops:leading,trailing,actions. - To render a design system component as another element, use Radix's
asChild(<Button asChild><Link to="/notes">...</Link></Button>). That keeps the link a real link with button styling.
Text and Translations
All user-facing text comes from translation files through react-i18next. Text only shown on the dev-only preview page is exempt.
const { t } = useTranslation('notes')
<h2>{t('list.title')}</h2>
<p>{t('list.count', { count: notes.length })}</p>
<p>{t('list.updated', { date: note.updatedAt })}</p>{
"list": {
"title": "Notes",
"count_one": "{{count}} note",
"count_other": "{{count}} notes",
"updated": "Updated {{date, datetime(dateStyle: medium)}}"
}
}- One namespace per feature, named after the feature, plus a shared
commonnamespace. A feature adds its English JSON together with the feature. - Keys are typed. The i18next type declaration is derived from the English resources, so a key that doesn't exist fails
npm run typecheck. - No concatenation.
t('a') + name + t('b')breaks in languages with a different word order. Use interpolation. - Plurals use
countand_one/_othersuffixes. Other languages add the forms they need (_few,_many). - Dates and numbers are formatted with i18next's built-in formatters (
datetime,number,relativetime,list), which useIntland the active language. Outsidet, useIntl.DateTimeFormat(i18n.resolvedLanguage, ...). Never format with a fixed locale or by hand. core/uicomponents don't calltfor content. Labels and messages come in as props. Generic words (a "Close" label on a dialog) may come fromcommon.- Schema messages are translation keys. Zod error messages in form schemas are keys like
'notes:form.titleRequired'.useTranslatedError()fromcore/i18nturns them into text. It's the one place that translates a key that isn't known at compile time. - Rich text with markup in a translation uses the
Transcomponent, neverdangerouslySetInnerHTML.
Accessibility
- Every interactive element has an accessible name: visible text, a
<label>, oraria-labelfor icon-only controls. - Inputs have a visible
<label>connected withhtmlForanduseId. Error text is linked witharia-describedbyand the input getsaria-invalid. - Focus is always visible. Never remove the focus outline without a
focus-visible:replacement. - Use Radix-based components for dialogs, menus, popovers, tabs and tooltips. Don't hand-build them.
- Don't use color alone to convey state. Add an icon or text.
- Disabled buttons that should still explain themselves use
aria-disabledand a tooltip instead ofdisabled.
Previews
Every core/ui component has a section on the dev-only design system preview page. It shows each variant, each size and each meaningful state: default, disabled, loading, with an error, with long text that has to wrap or truncate, and with and without optional slots.
- The preview page is a second Vite HTML entry that the production build doesn't include. It shows the active OS theme, so check both.
- Feature components and Screens don't get previews. Their component tests cover their states (see react-integration-testing).
Checklist: Building a Component
- Lives in the feature, or in
core/uiif a second feature needs it - Read from Figma with the MCP, with exactly the variants, sizes and states Figma defines
- Design system components start from shadcn/Radix and are restyled with tokens
- Only token classes named after Figma variables, no raw colors, no arbitrary values, no
dark: - Accepts
classNamemerged withcnon the outer element, spreads native props where it wraps an element - Variants with
cva, named by purpose - Icons exported from Figma,
currentColor, rendered throughIcon/IconButton, icon-only buttons have a label - All text from translation files, plurals and formatting through i18next
- Accessible name, label, visible focus, keyboard support
core/uicomponents have a preview section covering every variant and state
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

