ReactとStripe Elementで作成した決済フォームの、CSS・見た目をカスタマイズする方法


Stripe Elementsで描画される決済フォームは、セキュリティなどの目的で`iframe`の中に配置されます。

![スクリーンショット 2022-11-21 18.45.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/3c293c26-a855-ef74-8fd6-c24a5c65f737.png)

そのため、CSSでの内容・見た目のカスタマイズは**Element Appearance API**を利用して行う必要があります。

https://stripe.com/docs/elements/appearance-api

この記事では、Payment Elementsの見た目を調整する方法を紹介します。

## プリセットスタイル（テーマ）で設定する

StripeのPayment Elementsでは、いくつかのスタイルプリセットが用意されています。

![スクリーンショット 2022-11-21 17.58.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d38e6024-2640-e495-ebc5-abcdbccf25b0.png)
[ドキュメントサイトでデモを見る](https://stripe.com/docs/elements/appearance-api)

これらは、`<Elements/>`の`options.appearance`で設定します。

```js
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY)}
      options={{
        appearance: {
          theme: 'stripe'
        },
        clientSecret: piClientSecret,
      }}
    >
```

Darkモード用のテーマも用意されています。

![スクリーンショット 2022-11-21 18.24.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/1a267040-5765-5280-362b-74c949293df8.png)

以下のサンプルでは、`prefers-color-scheme`を利用して、動的にテーマを切り替えます。

```jsx
export default function Root() {
  
  /**
   * Payment Intentを作成する処理
   * React v18以降でのuseEffectが2回実行される挙動を、useRefを利用して抑止するワークアラウンドを追加
   **/
  const hasAPICalledRef = useRef(false)
  const [piClientSecret, setPiClientSecret] = useState('')
  useEffect(() => {
    if (hasAPICalledRef.current) return
    hasAPICalledRef.current = true
    fetch('http://localhost:3000/create_payment_intent', {
      method: 'post'
    }).then(data => data.json())
      .then(response => setPiClientSecret(response.clientSecret))
  }, [])

  /**
   * ブラウザの設定がダークモードか否かを判定する処理
   **/
  const [isDarkMode, setIsDarkMode] = useState(false)
  useEffect(() => {
    // Next.jsを想定したSSR対策
    if (!window) return

    // Stateを更新する処理
    const onSelectColorScheme = (e: {matches:boolean}) => {
      setIsDarkMode(e.matches)
    }

    // ブラウザの設定が変わったことを検知するイベントリスナーを追加
    window.matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', onSelectColorScheme)
    
    // 初回読み込み時の判定処理
    const isDarkMode = window?.matchMedia('(prefers-color-scheme: dark)').matches
    onSelectColorScheme({
      matches: isDarkMode
    })
    
    return () => {
      // unmount時にイベントリスナーを解除
      window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', onSelectColorScheme)
    }
  },[])


  if (!piClientSecret) return null;
  return (
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY as string)}
      options={{
        appearance: {
          // 通常とダークモードでテーマが変わるように設定
          theme: isDarkMode ? 'night' : 'stripe',
        },
        clientSecret: piClientSecret, 
      }}
    >
      <Home />
    </Elements>
  )
}
```


## CSS変数をカスタマイズする

ElementsのCSSには、CSS Variablesが設定されています。

この値をAppearance APIから変更することで、フォントや色・マージンなどを調整できます。

```jsx
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY)}
      options={{
        appearance: {
          theme: 'stripe',
          variables: {
            colorPrimary: '#2b8501',
            colorBackground: '#eeeeee',
            colorText: '#54883c',
            colorDanger: '#654321',
            fontFamily: 'Osaka',
            spacingUnit: '5px',
            borderRadius: '4px',
            spacingTab: '10px',
            spacingGridRow: '20px',
            spacingGridColumn: '50px'
          },
        },
        clientSecret: piClientSecret,
      }}
    >
```

テキストやフォームの背景色を変更できました。

![スクリーンショット 2022-11-21 18.32.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/23d27891-9720-0564-740b-077ec7b731b5.png)

利用できる変数名は、ドキュメントの`Commonly used bariables`と`Less commonly used variables`をご確認ください。

https://stripe.com/docs/elements/appearance-api?platform=web#commonly-used-variables

## RulesでCSSを上書きする

CSS変数だけではカスタマイズできない箇所がある場合、CSSをJSオブジェクトとして上書きできます。

`options.appearance.rules`に、「クラス名 - ルール」で定義しましょう。


```jsx
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY)}
      options={{
        appearance: {
          theme:  'stripe',
          rules: {
            '.Tab': {
              borderRadius: '0',
              border: '1px solid #E0E6EB',
              boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 6px rgba(18, 42, 66, 0.02)',
            },      
            '.Tab:hover': {
              color: 'var(--colorText)',
            },      
            '.Tab--selected': {
              borderColor: '#E0E6EB',
              boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 6px rgba(18, 42, 66, 0.02), 0 0 0 2px var(--colorPrimary)',
            },          
            '.Input': {
              borderRadius: '0',
            },  
            '.Input--invalid': {
              boxShadow: '0 1px 1px 0 rgba(0, 0, 0, 0.07), 0 0 0 2px var(--colorDanger)',
            },
          }
        },
        clientSecret: piClientSecret,
      }}
    >

```

このサンプルでは、borderの色や曲がり具合などをカスタマイズしています。

![スクリーンショット 2022-11-21 18.39.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/8d7b55e9-4731-caff-c03a-f9681791ee72.png)


利用できるルール（クラス名やステートなど）は、ドキュメントにて確認できます。

https://stripe.com/docs/elements/appearance-api?platform=web#detail-rules

## プリセット（テーマ）を使わずに見た目を調整する

VariablesとRulesを利用して、独自のデザインを設定することもできます。

その場合、`theme: 'none'`設定しましょう。
```diff_javascript

    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY)}
      options={{
        appearance: {
-          theme: 'stripe'
+          theme: 'none'
        },
        clientSecret: piClientSecret,
      }}
    >
```

Stripe側による見た目の装飾が最小限になります。

![スクリーンショット 2022-11-21 18.01.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/78501cf5-444f-b356-734c-a22969a2e194.png)

あとは`options.appearance.variables`と`options.appearance.rules`を利用してカスタマイズしましょう。

```jsx

    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY)}
      options={{
        appearance: {
          theme:  'none',
          variables: {
            colorPrimary: '#2b8501',
            colorDanger: '#654321',
            fontFamily: 'Osaka',
            spacingUnit: '5px',
            borderRadius: '4px',
            spacingGridColumn: '50px'
          },
          rules: {
            '.Tab': {
              border: '1px solid #E0E6EB',
              boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 6px rgba(18, 42, 66, 0.02)',
            },      
            '.Tab:hover': {
              color: 'var(--colorText)',
            },      
            '.Tab--selected': {
              borderColor: '#E0E6EB',
              boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 6px rgba(18, 42, 66, 0.02), 0 0 0 2px var(--colorPrimary)',
            },          
            '.Input': {
              border: '1px solid #E0E6EB',
              boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 6px rgba(18, 42, 66, 0.02, 0 0 0 2px var(--colorPrimary))',
            },  
            '.Input--invalid': {
              boxShadow: '0 1px 1px 0 rgba(0, 0, 0, 0.07), 0 0 0 2px var(--colorDanger)',
            },
          }
        },
        clientSecret: piClientSecret,
      }}
    >
```

![スクリーンショット 2022-11-21 18.44.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/bdd19205-1e2d-73cb-446d-46edf080340b.png)


## [PR] Stripe開発者向け情報をQiitaにて配信中！

- [Stripe Updates]：開発者向けStripeアップデート紹介・解説
- ユースケース別のStripe製品や実装サンプルの紹介
- Stripeと外部サービス・OSSとの連携方法やTipsの紹介
- 初心者向けのチュートリアル(予定)

など、Stripeを利用してオンラインビジネスを始める方法について週に２〜３本ペースで更新中です。

-> [Stripe Organizationsをフォローして最新情報をQiitaで受け取る](https://qiita.com/organizations/stripe)