React + Tailwind CSS環境で、所謂フラッシュメッセージの実装をしてみました。
ざっと以下のようなイメージです。
![]() |
こちら、GIF化するとわかりにくいですが、clickを連打してフラッシュメッセージを複数回呼び出しております。
呼び出しはキューで順次処理されています。
※ Tailwind CSSの3系での作成となります。4系でも動くとは思われますが、責任は負いません!
FlashMessage.tsx
まずはコンポーネント部分になります。
importで読み込んでいるflashMessage.types.tsは後続の項で記載していきます。
FLASH_MESSAGE_TYPEの指定で通知の背景色が、isShownの状態によってアニメーションが切り替わります。
import { FLASH_MESSAGE_TYPE } from '@/types/flashMessage.types'
import { FlashMessageType } from '@/types/flashMessage.types'
import { ReactElement } from 'react'
type FlashMessageProps = {
isShown: boolean
title?: string
message: string
type: FlashMessageType
onAnimationEnd?: (e: React.AnimationEvent<HTMLElement>) => void
}
export default function FlashMessage({
isShown,
title = 'システムメッセージ',
message,
type = FLASH_MESSAGE_TYPE.DEFAULT,
onAnimationEnd = () => { },
}: FlashMessageProps): ReactElement | null {
const getBackgroundColor = (type: FlashMessageType) => {
switch (type) {
case FLASH_MESSAGE_TYPE.SUCCESS:
return 'bg-green-500/45'
case FLASH_MESSAGE_TYPE.ERROR:
return 'bg-red-500/45'
case FLASH_MESSAGE_TYPE.WARNING:
return 'bg-orange-500/45'
case FLASH_MESSAGE_TYPE.DEFAULT:
return 'bg-neutral-950/15'
default:
break;
}
}
return message ? (
<div className='fixed justify-center items-center top-2 z-[2147483647] flex w-full pointer-events-none'>
<section
className={`px-4 py-2 w-3/4 min-h-16 text-white rounded-3xl border border-white/10 backdrop-blur-md pointer-events-auto ${getBackgroundColor(type)} ${isShown ? 'animate-slidein-down' : 'animate-slideout-top'}`}
onAnimationEnd={(e) => onAnimationEnd(e)}
>
<h1 className='font-extrabold'>{title}</h1>
<pre className='whitespace-pre-wrap'>{message}</pre>
</section>
</div>
) : null
}
flashMessage.types.ts
先程importを行っていたFlashMessage.type.tsになります。
as constを使ったオブジェクトリテラルは、実質的な enum(列挙型)として機能します。
export const FLASH_MESSAGE_TYPE = {
SUCCESS: 'success',
ERROR: 'error',
WARNING: 'warning',
DEFAULT: 'default',
} as const
export type FlashMessageType = (typeof FLASH_MESSAGE_TYPE)[keyof typeof FLASH_MESSAGE_TYPE]
export type FlashMessageData = {
message: string
type: FlashMessageType
}
FlashMessageProvider.tsx
こちらがFlashMessageの状態管理を担うProviderコンポーネントになります。
1メッセージ辺りの表示時間は3秒にしてあるので、適宜調整してください。
'use client'
import FlashMessage from '@/components/FlashMessage'
import { createContext, useState, useContext, useRef, useEffect, useMemo, useCallback } from 'react'
import { FlashMessageData } from '@/types/flashMessage.types'
type FlashMessageProviderType = {
setFlashMessage: (message: FlashMessageData) => void
}
const defaultProvider: FlashMessageProviderType = {
setFlashMessage: () => { },
}
const FlashMessageContext = createContext(defaultProvider)
export function useFlashMessageContext() {
return useContext(FlashMessageContext)
}
export default function FlashMessageProvider({ children }: { children: React.ReactNode }) {
const [messages, setMessages] = useState<FlashMessageData[]>([])
const [isShown, setIsShown] = useState(false)
// 現在表示中のメッセージ
const displayMessage = messages[0] || { message: '', type: 'default' }
// タイマーのIDを管理
const showTimerRef = useRef<NodeJS.Timeout | null>(null)
const hideTimerRef = useRef<NodeJS.Timeout | null>(null)
const setFlashMessage = useCallback((data: FlashMessageData) => {
if (!data || !data.message) return
setMessages((prev) => [...prev, data])
}, [])
const api = useMemo(
() => ({
setFlashMessage,
}),
[setFlashMessage]
)
// onAnimationEnd を useCallback でメモ化
// isShown の状態によって処理を分岐させるため、依存配列に [isShown] を指定
const onAnimationEnd = useCallback(() => {
// 既存のタイマーをクリア
if (showTimerRef.current) clearTimeout(showTimerRef.current)
if (hideTimerRef.current) clearTimeout(hideTimerRef.current)
if (isShown) {
// (A) 表示アニメーション終了後、3秒待って非表示アニメーションを開始
showTimerRef.current = setTimeout(() => {
setIsShown(false)
}, 3000)
} else {
// (B) 非表示アニメーション終了後、500ms待ってからキューの先頭を削除
hideTimerRef.current = setTimeout(() => {
setMessages((prev) => prev.slice(1))
// (stateが更新されると、下のuseEffectがトリガーされる)
}, 500)
}
}, [isShown])
// メッセージキューを監視するuseEffect (メインループ)
useEffect(() => {
// (C) キューに表示すべきメッセージがあり、かつ現在非表示の場合のみ実行
if (messages.length > 0 && !isShown) {
// (B)のタイマーが実行中ならキャンセル (すぐに次を表示するため)
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current)
hideTimerRef.current = null
}
// 表示アニメーションを開始
setIsShown(true)
}
}, [messages])
// アンマウント時のクリーンアップ
useEffect(() => {
return () => {
if (showTimerRef.current) clearTimeout(showTimerRef.current)
if (hideTimerRef.current) clearTimeout(hideTimerRef.current)
}
}, [])
return (
<FlashMessageContext.Provider value={api}>
<FlashMessage
isShown={isShown}
message={displayMessage.message}
type={displayMessage.type}
onAnimationEnd={onAnimationEnd}
/>
{children}
</FlashMessageContext.Provider>
)
}
tailwind.config.ts
themeの拡張設定(animation, keyframes)をtailwind.config.tsに取り込むことでアニメーションが可能になります。
使うときにはclassNameにanimate-slidein-downといった感じで挿入するだけでOKです。
今回使用するのはanimate-slidein-downとanimate-slideout-topだけではありますが、汎用的に使えるその他の設定も記載してあります。
import type { Config } from 'tailwindcss'
const config: Config = {
...
theme: {
extend: {
animation: {
fadein: 'fadein 0.2s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
fadeout: 'fadeout 0.2s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
'slidein-down': 'slidein-down 0.2s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
'slidein-right': 'slidein-right 0.2s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
'slidein-left': 'slidein-left 0.2s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
'slideout-top': 'slideout-top 0.4s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
'slideout-right': 'slideout-right 0.4s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
'slideout-left': 'slideout-left 0.4s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',
},
keyframes: {
fadein: {
'0%': {
opacity: '0',
},
to: {
opacity: '1',
},
},
fadeout: {
'0%': {
opacity: '.4',
},
to: {
opacity: '0',
},
},
'slidein-down': {
'0%': {
transform: 'translateY(-80%)',
opacity: '0',
},
to: {
transform: 'translateY(0)',
opacity: '1',
},
},
'slidein-right': {
'0%': {
transform: 'translateX(-80%)',
opacity: '0',
},
to: {
transform: 'translateX(0)',
opacity: '1',
},
},
'slidein-left': {
'0%': {
transform: 'translateX(80%)',
opacity: '0',
},
to: {
transform: 'translateX(0)',
opacity: '1',
},
},
'slideout-top': {
'0%': {
transform: 'translateY(0)',
opacity: '.4',
},
to: {
transform: 'translateY(-80%)',
opacity: '0',
},
},
'slideout-right': {
'0%': {
transform: 'translateX(0)',
opacity: '.4',
},
to: {
transform: 'translateX(80%)',
opacity: '0',
},
},
'slideout-left': {
'0%': {
transform: 'translateX(0)',
opacity: '.4',
},
to: {
transform: 'translateX(-80%)',
opacity: '0',
},
},
},
},
},
...
}
export default config
使い方
まず、layout.tsxでFlashMessageProviderを登録します。
ルートレイアウトに配置することで、どのページ・コンポーネントからでもsetFlashMessageを呼び出せるようになります。
layout.tsxはページ遷移(ルート変更)が起きてもアンマウント(再描画)されません。ここに <FlashMessage />本体(Providerの内部でレンダリングされる)を配置することで、ページを移動してもフラッシュメッセージが表示され続けるという動きになります。
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import FlashMessageProvider from '@/providers/FlashMessageProvider'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'flashメッセージのテスト',
description: 'flashメッセージのテスト',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='ja' className='bg-gray-100' suppressHydrationWarning>
<body className={inter.className}>
<FlashMessageProvider>
<main className='my-0 mx-auto max-w-[1080px]'>
{children}
</main>
</FlashMessageProvider>
</body>
</html>
)
}
あとはフラッシュメッセージを使いたいページ・コンポーネントから、任意のタイミングでsetFlashMessageを呼び出すだけです。
'use client'
import Button from '@/components/elements/Button'
import { useFlashMessageContext } from '@/providers/FlashMessageProvider'
import { FLASH_MESSAGE_TYPE } from '@/types/flashMessage.types'
export default function Home() {
const { setFlashMessage } = useFlashMessageContext()
return (
<>
<h1>HOME</h1>
{/* クリックされたときにフラッシュメッセージを呼び出す */}
<Button onClick={() => setFlashMessage({ message: 'click', type: FLASH_MESSAGE_TYPE.SUCCESS })}>Flashメッセージ</Button>
</>
)
}
おわり
こういうのって意外と自前で作ると面倒なんですよね。
一回作れば使い回せて、他のコンポーネントを作るときの参考にもなります。
特にアニメーション付きのコンポーネントの状態管理はひと工夫が必要だったので、結構勉強になりました。
