
frontend-design
by mrmeg
SKILL.md
name: frontend-design description: Create UI components following this Expo template's design system. Use when building components, screens, or interfaces for this React Native project.
This skill guides creation of UI components for this Expo React Native template. Follow these patterns for consistent, theme-aware, cross-platform components.
Quick Start
Essential imports for any UI component:
import { View, StyleSheet, Platform } from "react-native";
import { useTheme } from "@/client/hooks/useTheme";
import { spacing } from "@/client/constants/spacing";
import type { Theme } from "@/client/constants/colors";
Theming System
useTheme() Hook
Central access point for all theming:
const { theme, scheme, getShadowStyle, getContrastingColor, withAlpha } = useTheme();
// Access semantic colors
backgroundColor: theme.colors.background
color: theme.colors.foreground
borderColor: theme.colors.border
// Cross-platform shadows
const shadow = getShadowStyle("soft"); // "base" | "soft" | "sharp" | "subtle"
// Color utilities
const textColor = getContrastingColor(bgColor, palette.white, palette.black);
const fadedColor = withAlpha(theme.colors.primary, 0.5);
Semantic Colors (99% of usage)
| Color | Purpose |
|---|---|
background / foreground | Page background and primary text |
card / cardForeground | Card surfaces and text |
primary / primaryForeground | Brand color and text on it |
secondary / secondaryForeground | Secondary actions |
muted / mutedForeground | Subtle backgrounds and placeholder text |
destructive / destructiveForeground | Error/danger states |
success / warning | Status colors |
border | Borders and dividers |
overlay | Modal overlays |
Palette (Only for Contrast Calculations)
import { palette } from "@/client/constants/colors";
// ONLY use palette for WCAG contrast calculations
const textColor = getContrastingColor(bgColor, palette.white, palette.black);
// NEVER use palette for regular styling - use theme.colors instead
Spacing System
8px-based scale from @/client/constants/spacing:
| Token | Value | Token | Value |
|---|---|---|---|
xxs | 2px | radiusXs | 2px |
xs | 4px | radiusSm | 4px |
sm | 8px | radiusMd | 8px |
md | 16px | radiusLg | 12px |
lg | 24px | radiusXl | 16px |
xl | 32px | radiusFull | 9999px |
xxl | 48px |
Icon sizes: iconXs (12), iconSm (16), iconMd (24), iconLg (32), iconXl (48)
Typography
StyledText Components
import { SansSerifText, SansSerifBoldText, SerifText } from "@/client/components/ui/StyledText";
// Semantic shortcuts
import { TitleText, HeadingText, BodyText, CaptionText } from "@/client/components/ui/StyledText";
Font families: Lato_400Regular, Lato_700Bold, Merriweather_400Regular, Merriweather_700Bold
Styling Patterns
StyleSheet Factory Pattern
const createStyles = (theme: Theme) =>
StyleSheet.create({
container: {
backgroundColor: theme.colors.card,
borderRadius: spacing.radiusMd,
borderWidth: 1,
borderColor: theme.colors.border,
padding: spacing.md,
},
});
// In component
const { theme } = useTheme();
const styles = createStyles(theme);
CRITICAL: React Native Web Nested Array Fix
React Native Web crashes with nested style arrays. Use these patterns:
Solution 1: Spread Arrays (for simple components)
function Component({ style: styleOverride, ...props }) {
return (
<View
style={[
styles.base,
...(styleOverride && typeof styleOverride !== "function"
? Array.isArray(styleOverride) ? styleOverride : [styleOverride]
: []),
]}
/>
);
}
Solution 2: Plain Objects with Flatten (REQUIRED for @rn-primitives menu components)
function MenuComponent({ style: styleOverride, ...props }) {
return (
<Primitive
style={{
...baseStyle,
...(styleOverride && typeof styleOverride !== "function"
? StyleSheet.flatten(styleOverride)
: {}),
}}
/>
);
}
Platform-Specific Styling
// Pointer cursor for interactive elements on web
...(Platform.OS === "web" && { cursor: "pointer" as any }),
// Z-index for web overlays
...(Platform.OS === "web" && { zIndex: 50 }),
// Remove focus outline on web
...(Platform.OS === "web" && { outlineStyle: "none" as any }),
Component Patterns
Size Variants
type ComponentSize = "sm" | "md" | "lg";
const SIZE_CONFIGS: Record<ComponentSize, { height: number; fontSize: number }> = {
sm: { height: 28, fontSize: 13 },
md: { height: 32, fontSize: 14 },
lg: { height: 36, fontSize: 15 },
};
Preset/Variant Pattern
type ButtonPreset = "default" | "outline" | "ghost" | "link" | "destructive";
Composition Over Props
// PREFERRED - flexible composition
<Button onPress={handler}>
<SansSerifBoldText>Click Me</SansSerifBoldText>
</Button>
// Components accept children, not just text props
@rn-primitives Integration
For complex interactive components, use @rn-primitives:
import * as CheckboxPrimitive from "@rn-primitives/checkbox";
function Checkbox({ checked, onCheckedChange, style: styleOverride }) {
const { theme } = useTheme();
return (
<CheckboxPrimitive.Root
checked={checked}
onCheckedChange={onCheckedChange}
style={{
borderColor: theme.colors.border,
...(Platform.OS === "web" && { cursor: "pointer" as any }),
...(styleOverride && typeof styleOverride !== "function"
? StyleSheet.flatten(styleOverride)
: {}),
}}
>
<CheckboxPrimitive.Indicator>
<Icon as={Check} color={theme.colors.primary} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
Component Development Checklist
- Extract
styleprop separately to avoid nested arrays - Use
useTheme()hook for all colors - Import and use
spacingconstants (never hardcode) - Add
cursor: "pointer"for interactive elements on web - Handle both light and dark themes
- Test on web for nested array crashes
- Run
npx tscto check TypeScript - Add to showcase in
app/(main)/showcase.tsx - Consider
@rn-primitivesfor complex interactive components - Include accessibility attributes (
accessibilityRole, etc.)
Full Component Example
import { View, Pressable, StyleSheet, Platform, type StyleProp, type ViewStyle } from "react-native";
import { useTheme } from "@/client/hooks/useTheme";
import { spacing } from "@/client/constants/spacing";
import { SansSerifText } from "@/client/components/ui/StyledText";
import type { Theme } from "@/client/constants/colors";
interface MyCardProps {
title: string;
description?: string;
onPress?: () => void;
style?: StyleProp<ViewStyle>;
}
export function MyCard({ title, description, onPress, style: styleOverride }: MyCardProps) {
const { theme, getShadowStyle } = useTheme();
const styles = createStyles(theme);
const shadowStyle = StyleSheet.flatten(getShadowStyle("soft"));
return (
<Pressable
onPress={onPress}
accessibilityRole="button"
style={({ pressed }) => [
styles.container,
shadowStyle,
pressed && styles.pressed,
...(styleOverride && typeof styleOverride !== "function"
? Array.isArray(styleOverride) ? styleOverride : [styleOverride]
: []),
]}
>
<SansSerifText style={styles.title}>{title}</SansSerifText>
{description && (
<SansSerifText style={styles.description}>{description}</SansSerifText>
)}
</Pressable>
);
}
const createStyles = (theme: Theme) =>
StyleSheet.create({
container: {
backgroundColor: theme.colors.card,
borderRadius: spacing.radiusMd,
borderWidth: 1,
borderColor: theme.colors.border,
padding: spacing.md,
...(Platform.OS === "web" && { cursor: "pointer" as any }),
},
pressed: {
opacity: 0.8,
},
title: {
fontSize: 16,
fontWeight: "600",
color: theme.colors.cardForeground,
marginBottom: spacing.xs,
},
description: {
fontSize: 14,
color: theme.colors.mutedForeground,
},
});
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です