スキル一覧に戻る
mattppal

modular-design

by mattppal

0🍴 0📅 2026年1月20日
GitHubで見るManusで実行

SKILL.md


name: modular-design description: Guide for creating modular, reusable UI components with proper architecture. Use when creating new UI components, organizing a component library, implementing design system patterns, or building reusable UI building blocks. Applies to tasks involving component architecture, atomic design, compound components, or styling abstractions.

Modular Design Patterns

This skill provides guidance for building beautiful, maintainable UI components using atomic design principles and Tamagui's composition patterns. It covers component architecture, file organization, and reusable patterns.

Core Philosophy

  1. Composition over inheritance - Build complex UIs from simple, composable pieces
  2. Single responsibility - Each component does one thing well
  3. Props down, events up - Data flows predictably
  4. Style at the edges - Keep logic and styling separate
  5. Progressive disclosure - Simple defaults, advanced options available

Atomic Design Structure

Organize components into layers based on complexity:

components/
├── atoms/           # Basic building blocks (Button, Input, Text)
├── molecules/       # Simple combinations (FormField, SearchBar)
├── organisms/       # Complex sections (Header, Card, Form)
├── templates/       # Page layouts (MainLayout, AuthLayout)
└── index.ts         # Public exports

Atoms

Smallest building blocks that can't be broken down further:

// components/atoms/Label.tsx
import { Text, styled } from 'tamagui'

export const Label = styled(Text, {
  name: 'Label',
  fontSize: '$2',
  fontWeight: '500',
  color: '$color',

  variants: {
    required: {
      true: {
        // Add asterisk via ::after on web, or handle in component
      },
    },
    error: {
      true: {
        color: '$red10',
      },
    },
  } as const,
})

Molecules

Combinations of atoms that form functional units:

// components/molecules/FormField.tsx
import { YStack } from 'tamagui'
import { Label } from '../atoms/Label'
import { Input } from '../atoms/Input'
import { ErrorMessage } from '../atoms/ErrorMessage'

interface FormFieldProps {
  label: string
  error?: string
  required?: boolean
  children: React.ReactNode
}

export function FormField({ label, error, required, children }: FormFieldProps) {
  return (
    <YStack gap="$2">
      <Label required={required}>{label}</Label>
      {children}
      {error && <ErrorMessage>{error}</ErrorMessage>}
    </YStack>
  )
}

Organisms

Complete, self-contained UI sections:

// components/organisms/LoginForm.tsx
import { YStack, Button } from 'tamagui'
import { FormField } from '../molecules/FormField'
import { Input } from '../atoms/Input'

export function LoginForm({ onSubmit, isLoading }) {
  return (
    <YStack gap="$4" padding="$4">
      <FormField label="Email" required>
        <Input placeholder="email@example.com" />
      </FormField>
      <FormField label="Password" required>
        <Input secureTextEntry placeholder="Password" />
      </FormField>
      <Button onPress={onSubmit} disabled={isLoading}>
        {isLoading ? 'Signing in...' : 'Sign In'}
      </Button>
    </YStack>
  )
}

Compound Components

Build components with sub-components that work together via context:

// components/organisms/Card/index.tsx
import { createStyledContext, styled, withStaticProperties, YStack, Text } from 'tamagui'
import type { SizeTokens } from 'tamagui'

// 1. Create shared context
const CardContext = createStyledContext({
  size: '$4' as SizeTokens,
})

// 2. Create styled frame
const CardFrame = styled(YStack, {
  name: 'Card',
  context: CardContext,
  backgroundColor: '$background',
  borderRadius: '$4',
  borderWidth: 1,
  borderColor: '$borderColor',
  overflow: 'hidden',

  variants: {
    size: {
      '...size': (size, { tokens }) => ({
        padding: tokens.space[size],
      }),
    },
    elevated: {
      true: {
        shadowColor: '$shadowColor',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 8,
        elevation: 3,
      },
    },
  } as const,

  defaultVariants: {
    size: '$4',
  },
})

// 3. Create sub-components
const CardHeader = styled(YStack, {
  name: 'CardHeader',
  context: CardContext,
  paddingBottom: '$2',
  borderBottomWidth: 1,
  borderBottomColor: '$borderColor',
})

const CardTitle = styled(Text, {
  name: 'CardTitle',
  context: CardContext,
  fontSize: '$5',
  fontWeight: '600',
  color: '$color',
})

const CardContent = styled(YStack, {
  name: 'CardContent',
  context: CardContext,
  paddingTop: '$2',
})

const CardFooter = styled(YStack, {
  name: 'CardFooter',
  context: CardContext,
  paddingTop: '$4',
  borderTopWidth: 1,
  borderTopColor: '$borderColor',
  flexDirection: 'row',
  justifyContent: 'flex-end',
  gap: '$2',
})

// 4. Export with static properties
export const Card = withStaticProperties(CardFrame, {
  Props: CardContext.Provider,
  Header: CardHeader,
  Title: CardTitle,
  Content: CardContent,
  Footer: CardFooter,
})

// Usage:
// <Card size="$4" elevated>
//   <Card.Header>
//     <Card.Title>Title</Card.Title>
//   </Card.Header>
//   <Card.Content>Content here</Card.Content>
//   <Card.Footer>
//     <Button>Action</Button>
//   </Card.Footer>
// </Card>

The asChild Pattern

Pass styles and behavior to a child component:

import { Button } from 'tamagui'
import { Link } from 'expo-router'

// Button styles apply to Link
<Button asChild>
  <Link href="/settings">Settings</Link>
</Button>

// Common uses:
// - Wrapping navigation links with styled buttons
// - Applying trigger styles to custom elements
// - Dialog/Popover triggers

How it works:

  • asChild clones the child element
  • Merges parent's styles and props onto child
  • Child must accept onPress/onClick and style props
// Custom component supporting asChild
import { Pressable } from 'react-native'
import { styled, GetProps } from 'tamagui'

const StyledPressable = styled(Pressable, {
  // styles...
})

type Props = GetProps<typeof StyledPressable> & {
  asChild?: boolean
  children: React.ReactNode
}

export function CustomButton({ asChild, children, ...props }: Props) {
  if (asChild && React.isValidElement(children)) {
    return React.cloneElement(children, props)
  }
  return <StyledPressable {...props}>{children}</StyledPressable>
}

Size Scaling System

Tamagui's size system scales multiple properties from a single prop:

const ScaledButton = styled(View, {
  name: 'ScaledButton',

  variants: {
    size: {
      '...size': (size, { tokens }) => {
        const sizeValue = tokens.size[size] ?? size
        return {
          height: sizeValue,
          paddingHorizontal: tokens.space[size],
          borderRadius: tokens.radius[size] ?? tokens.radius.$4,
        }
      },
    },
  } as const,

  defaultVariants: {
    size: '$4',
  },
})

// size="$2" → small height, small padding, small radius
// size="$6" → large height, large padding, large radius

Size token convention:

TokenTypical Use
$2Compact/dense UI
$3Small elements
$4Default/medium (use as default)
$5Large elements
$6Extra large/hero

Variant Patterns

Boolean Variants

variants: {
  disabled: {
    true: {
      opacity: 0.5,
      pointerEvents: 'none',
    },
  },
  active: {
    true: {
      backgroundColor: '$primary',
      color: '$background',
    },
  },
} as const

Enum Variants

variants: {
  variant: {
    solid: {
      backgroundColor: '$primary',
      color: '$background',
    },
    outline: {
      backgroundColor: 'transparent',
      borderWidth: 1,
      borderColor: '$primary',
      color: '$primary',
    },
    ghost: {
      backgroundColor: 'transparent',
      color: '$primary',
    },
  },
} as const

Functional Variants

variants: {
  // Spread tokens with custom logic
  size: {
    '...size': (size, { tokens }) => ({
      padding: tokens.space[size],
      fontSize: tokens.fontSize[size],
    }),
  },
  // Direct value mapping
  width: (val) => ({ width: val }),
  // Conditional logic
  truncate: (lines: number) => ({
    numberOfLines: lines,
    overflow: 'hidden',
  }),
} as const

Component File Structure

Single Component

components/atoms/Button/
├── index.tsx        # Main component + exports
├── Button.tsx       # Component implementation (if complex)
├── Button.test.tsx  # Tests
└── types.ts         # TypeScript types (if many)

Compound Component

components/organisms/Dialog/
├── index.tsx           # Main export with withStaticProperties
├── DialogContext.tsx   # Shared context
├── DialogFrame.tsx     # Main container
├── DialogTitle.tsx     # Title sub-component
├── DialogContent.tsx   # Content sub-component
├── DialogActions.tsx   # Actions sub-component
└── types.ts            # Shared types

Export Patterns

Barrel Exports

// components/atoms/index.ts
export { Button } from './Button'
export { Input } from './Input'
export { Label } from './Label'
export type { ButtonProps, InputProps, LabelProps } from './types'

// components/index.ts
export * from './atoms'
export * from './molecules'
export * from './organisms'

Named vs Default Exports

// Prefer named exports for components
export function Button() {}  // Good
export default Button        // Avoid

// Exception: page components in Expo Router
export default function HomeScreen() {}  // Required by router

Styling Best Practices

Use Tokens Consistently

// Wrong - hardcoded values
<View padding={16} backgroundColor="#fff" />

// Correct - token references
<View padding="$4" backgroundColor="$background" />

Responsive Design

<YStack
  padding="$4"
  $sm={{ padding: '$2' }}
  flexDirection="column"
  $md={{ flexDirection: 'row' }}
/>

Pseudo-States

<Button
  backgroundColor="$primary"
  hoverStyle={{ backgroundColor: '$primaryHover' }}
  pressStyle={{ backgroundColor: '$primaryPress', scale: 0.98 }}
  focusVisibleStyle={{ outlineWidth: 2, outlineColor: '$primaryFocus' }}
/>

Accessibility Patterns

Required Elements

// Dialog must have title and description
<Dialog>
  <Dialog.Title>Confirm Action</Dialog.Title>
  <Dialog.Description>Are you sure?</Dialog.Description>
</Dialog>

// Form inputs need labels
<Label htmlFor="email">Email</Label>
<Input id="email" />

// Icon-only buttons need accessible label
<Button icon={<MenuIcon />} aria-label="Open menu" />

Focus Management

// Use FocusScope for dialogs (web)
import { FocusScope } from 'tamagui'

<FocusScope trapped restoreFocus>
  <Dialog.Content>...</Dialog.Content>
</FocusScope>

References

For detailed patterns and checklists, see:

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です