EY-Office ブログ

Tailwind CSSでレスポンシブ・デザインやダーク・モードに対応してみたが、簡単だった!

Tailwind CSSの良さが少しわかった気がしたの続きです。前回は対応できなかったレスポンシブ・デザインやダークモードに対応してみました。

Tailwind CSS

レスポンシブ・デザイン

先週時点でのアプリをiPhoneで表示すると以下のように、タブの幅が小さかったり左右の余白が大きすぎて 対戦結果の表示が縦になってしまっています。😅

Tailwind CSSはレスポンシブ・デザインに対応する手段を提供しています。詳細はResponsive Designに書かれているように <img class="w-16 md:w-32 lg:w-48" src="...">と定義すると、スマフォ(幅768px以下)なら画像imgの幅は4rem(64px)、タブレット(幅1024px以上)なら画像imgの幅は8rem(128px)、PC(幅1280px以上)なら画像imgの幅は12rem(192px)になります。

今回は、スマフォ(幅768px以下)をデフォルトにし、PC固有のクラス指定に md:を付けて対応することにしました。変更点のdiffは以下のようになりました。

@@ -23,9 +23,9 @@ const App: React.FC = () => {
   };

   return (
-    <div className="ml-8">
+    <div className="md:ml-8">
       <Header>じゃんけん ポン!</Header>
-      <Paper className="w-3/5">
+      <Paper className="md:w-3/5">
         <JyankenBox actionPon={(te) => pon(te)} />
         <Tabs titles={["対戦結果", "対戦成績"]} tabIndex={tabIndex} setTabIndex={tabChange} />
         {tabIndex === 0 ? <ScoreList scores={scores} /> : null}
@@ -78,7 +78,7 @@ const ScoreListItem: React.FC<ScoreListItemProps> = ({ score }) => {
   const teString = ["グー", "チョキ", "パー"];
   const judgmentString = ["引き分け", "勝ち", "負け"];
   const dateHHMMSS = (d: Date) => d.toTimeString().substring(0, 8);
-  const tdClass = `px-6 py-4 ${JudgmentColor[score.judgment]}`;
+  const tdClass = `px-3 md:px-6 py-4 ${JudgmentColor[score.judgment]}`;

   return (
     <tr className="bg-white border-b">
@@ -118,7 +118,7 @@ type PaperProps = {
   className?: string;
 };
 const Paper: React.FC<PaperProps> = ({ children, className }) => {
-  return <div className={`p-6 bg-white ${className}`}>{children}</div>;
+  return <div className={`p-3 md:p-6 bg-white ${className}`}>{children}</div>;
 };

 type TableProps = {
@@ -132,7 +132,7 @@ const Table: React.FC<TableProps> = ({ header, body }) => {
         <thead className="bg-slate-50 border-r border-l border-b">
           <tr>
             {header.map((title, ix) => (
-              <th key={ix} scope="col" className="px-6 py-3">
+              <th key={ix} scope="col" className="px-3 md:px-6 py-3">
                 {title}
               </th>
             ))}

変更したのでは、5ヵ所でclassName="px-3 md:px-6 py-3"のようにpadding-left:,padding-right:をスマフォなら12px、PCなら24pxと切り替えています。簡単ですね!

以下の画面がレスポンシブ・デザインに対応できた結果です。😁

ダーク・モード

皆さんはダーク・モード使ってますか? 私はまったく使っていないので、初めてのダーク・モード対応画面を作りました。

以下の画面がダーク・モードに対応した結果です。

もちろん、Tailwind CSSはDark Modeに対応しています。レスポンシブ・デザイン同様に dark:という接頭辞を付ける事でダーク・モード固有の設定を指定します。

<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">

上の指定では通常(ライト・モード)ではボーダー/バックグラウンド共に白色、ダーク・モードではバックグラウンドはgray-800(濃いグレー)、ボーダーはgray-700(少し濃いグレー)になります。

こちらの変更ヵ所はかなりありましたね。😅 ただし単純な指定ですね。

 import React, { useMemo, useState } from "react";
 import Jyanken, { Statuses, Score, Te, Judgment } from "./Jyanken";

-const JudgmentColor = ["text-[#000000]", "text-[#2979ff]", "text-[#ff1744]"];
+const JudgmentColor = ["text-[#000] dark:text-[#ccc]", "text-[#2979ff]", "text-[#ff1744]"];

 const App: React.FC = () => {
   const [scores, setScores] = useState<Score[]>([]);
@@ -39,7 +39,7 @@ type HeaderProps = {
   children: React.ReactNode;
 };
 const Header: React.FC<HeaderProps> = ({ children }) => {
-  return <h1 className="my-4 text-3xl font-bold">{children}</h1>;
+  return <h1 className="my-4 text-3xl font-bold dark:text-gray-200">{children}</h1>;
 };

 type JyankenBoxProps = {
@@ -81,7 +81,7 @@ const ScoreListItem: React.FC<ScoreListItemProps> = ({ score }) => {
   const tdClass = `px-3 md:px-6 py-4 ${JudgmentColor[score.judgment]}`;

   return (
-    <tr className="bg-white border-b">
+    <tr className="bg-white border-b  dark:bg-gray-800 dark:border-gray-700">
       <td className={tdClass}>{dateHHMMSS(score.created_at)}</td>
       <td className={tdClass}>{teString[score.human]}</td>
       <td className={tdClass}>{teString[score.computer]}</td>
@@ -95,7 +95,7 @@ type StatusBoxProps = {
 };
 const StatusBox: React.FC<StatusBoxProps> = ({ status }) => {
   const statusRow = (title: string, judge: Judgment, count: number) => (
-    <tr key={title} className="bg-white border-b">
+    <tr key={title} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
       <th className="pl-16 py-4">{title}</th>
       <td className={`text-right pr-16 py-4 ${JudgmentColor[judge]}`}>{count}</td>
     </tr>
@@ -118,7 +118,7 @@ type PaperProps = {
   className?: string;
 };
 const Paper: React.FC<PaperProps> = ({ children, className }) => {
-  return <div className={`p-3 md:p-6 bg-white ${className}`}>{children}</div>;
+  return <div className={`p-3 md:p-6 bg-white dark:bg-gray-800 ${className}`}>{children}</div>;
 };

 type TableProps = {
@@ -127,9 +127,9 @@ type TableProps = {
 };
 const Table: React.FC<TableProps> = ({ header, body }) => {
   return (
-    <table className="w-full text-sm text-left text-gray-500">
+    <table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
       {header && (
-        <thead className="bg-slate-50 border-r border-l border-b">
+        <thead className="bg-slate-50 border-r border-l border-b dark:bg-gray-700 dark:text-gray-400 dark:border-gray-700">
           <tr>
             {header.map((title, ix) => (
               <th key={ix} scope="col" className="px-3 md:px-6 py-3">
@@ -139,7 +139,7 @@ const Table: React.FC<TableProps> = ({ header, body }) => {
           </tr>
         </thead>
       )}
-      <tbody className="bg-white border-b border-r border-l">{body}</tbody>
+      <tbody className="bg-white border-b border-r border-l dark:bg-gray-800 dark:border-gray-700">{body}</tbody>
     </table>
   );
 };
@@ -151,7 +151,7 @@ type ButtonProps = {
 };
 const Button: React.FC<ButtonProps> = ({ children, onClick, className }) => {
   const buttonClass =
-    "text-white text-center text-sm rounded w-16 px-2 py-2 bg-blue-600 hover:bg-blue-700 shadow shadow-gray-800/50";
+    "text-white text-center text-sm rounded w-16 px-2 py-2 bg-blue-600 hover:bg-blue-700 shadow shadow-gray-800/50 dark:bg-blue-600 dark:hover:bg-blue-700";

   return (
     <button type="button" onClick={onClick} className={`${buttonClass} ${className}`}>
@@ -167,10 +167,10 @@ type TabsProps = {
 };
 const Tabs: React.FC<TabsProps> = ({ titles, tabIndex, setTabIndex }) => {
   const titleClass =
-    "w-full inline-block text-white bg-cyan-500 p-4 border-b-2 hover:text-white";
-  const TitleSelectedClass = " border-blue-600 active";
+    "w-full inline-block text-white bg-cyan-500 p-4 border-b-2 dark:border-gray-700 dark:text-gray-200 hover:text-white";
+  const TitleSelectedClass = " border-blue-600 dark:border-gray-300 active";
   return (
-    <ul className="flex text-sm font-medium text-center border-b border-gray-200 mt-8">
+    <ul className="flex text-sm font-medium text-center border-gray-200 mt-8">
       {titles.map((title, ix) => {
         return (
           <li key={ix} className="w-full">

まとめ

そもそも、レスポンシブ・デザインに対応するにはFlexboxを使うなど表示幅が変わることを前提としたCSS指定も必要ですが、今回の例のように微調整レベルの指定は簡単にできる事がわかったと思います。
ダーク・モードもバックグラウンド色の指定のような部分を共通化すれば、もっとシンプルになるかなと思います。

Tailwind CSSは、レスポンシブ・デザインやダーク・モードの対応も簡単で、Tailwind CSSの良さが少しわかった気がしたを更に感じました。😁


サンプルコード

レスポンシブ・デザインとダーク・モードに対応したコード

  • App.tsx
import React, { useMemo, useState } from "react";
import Jyanken, { Statuses, Score, Te, Judgment } from "./Jyanken";

const JudgmentColor = ["text-[#000] dark:text-[#ccc]", "text-[#2979ff]", "text-[#ff1744]"];

const App: React.FC = () => {
  const [scores, setScores] = useState<Score[]>([]);
  const [status, setStatus] = useState<Statuses>({ draw: 0, win: 0, lose: 0 });
  const [tabIndex, setTabIndex] = useState(0);
  const jyanken = useMemo(() => new Jyanken(), []);

  const tabChange = (ix: number) => {
    setTabIndex(ix);
    getResult();
  };
  const getResult = () => {
    setScores(jyanken.getScores());
    setStatus(jyanken.getStatuses());
  };
  const pon = (te: number) => {
    jyanken.pon(te);
    getResult();
  };

  return (
    <div className="md:ml-8">
      <Header>じゃんけん ポン!</Header>
      <Paper className="md:w-3/5">
        <JyankenBox actionPon={(te) => pon(te)} />
        <Tabs titles={["対戦結果", "対戦成績"]} tabIndex={tabIndex} setTabIndex={tabChange} />
        {tabIndex === 0 ? <ScoreList scores={scores} /> : null}
        {tabIndex === 1 ? <StatusBox status={status} /> : null}
      </Paper>
    </div>
  );
};

type HeaderProps = {
  children: React.ReactNode;
};
const Header: React.FC<HeaderProps> = ({ children }) => {
  return <h1 className="my-4 text-3xl font-bold dark:text-gray-200">{children}</h1>;
};

type JyankenBoxProps = {
  actionPon: (te: number) => void;
};
const JyankenBox: React.FC<JyankenBoxProps> = ({ actionPon }) => {
  return (
    <div className="w-[230px] mx-auto flex">
      <Button onClick={() => actionPon(Te.Guu)}>グー</Button>
      <Button className="mx-5" onClick={() => actionPon(Te.Choki)}>
        チョキ
      </Button>
      <Button onClick={() => actionPon(Te.Paa)}>パー</Button>
    </div>
  );
};

type ScoreListProps = {
  scores: Score[];
};
const ScoreList: React.FC<ScoreListProps> = ({ scores }) => {
  return (
    <Table
      header={["時間", "人間", "コンピュータ", "結果"]}
      body={scores.map((score, ix) => (
        <ScoreListItem key={ix} score={score} />
      ))}
    />
  );
};

type ScoreListItemProps = {
  score: Score;
};
const ScoreListItem: React.FC<ScoreListItemProps> = ({ score }) => {
  const teString = ["グー", "チョキ", "パー"];
  const judgmentString = ["引き分け", "勝ち", "負け"];
  const dateHHMMSS = (d: Date) => d.toTimeString().substring(0, 8);
  const tdClass = `px-3 md:px-6 py-4 ${JudgmentColor[score.judgment]}`;

  return (
    <tr className="bg-white border-b  dark:bg-gray-800 dark:border-gray-700">
      <td className={tdClass}>{dateHHMMSS(score.created_at)}</td>
      <td className={tdClass}>{teString[score.human]}</td>
      <td className={tdClass}>{teString[score.computer]}</td>
      <td className={tdClass}>{judgmentString[score.judgment]}</td>
    </tr>
  );
};

type StatusBoxProps = {
  status: Statuses;
};
const StatusBox: React.FC<StatusBoxProps> = ({ status }) => {
  const statusRow = (title: string, judge: Judgment, count: number) => (
    <tr key={title} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
      <th className="pl-16 py-4">{title}</th>
      <td className={`text-right pr-16 py-4 ${JudgmentColor[judge]}`}>{count}</td>
    </tr>
  );
  return (
    <Table
      body={[
        statusRow("勝ち", Judgment.Win, status.win),
        statusRow("負け", Judgment.Lose, status.lose),
        statusRow("引き分け", Judgment.Draw, status.draw)
      ]}
    />
  );
};

// -------------------------------------------------------------------------

type PaperProps = {
  children: React.ReactNode;
  className?: string;
};
const Paper: React.FC<PaperProps> = ({ children, className }) => {
  return <div className={`p-3 md:p-6 bg-white dark:bg-gray-800 ${className}`}>{children}</div>;
};

type TableProps = {
  header?: string[];
  body: React.ReactElement<any, any>[];
};
const Table: React.FC<TableProps> = ({ header, body }) => {
  return (
    <table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
      {header && (
        <thead className="bg-slate-50 border-r border-l border-b dark:bg-gray-700 dark:text-gray-400 dark:border-gray-700">
          <tr>
            {header.map((title, ix) => (
              <th key={ix} scope="col" className="px-3 md:px-6 py-3">
                {title}
              </th>
            ))}
          </tr>
        </thead>
      )}
      <tbody className="bg-white border-b border-r border-l dark:bg-gray-800 dark:border-gray-700">{body}</tbody>
    </table>
  );
};

type ButtonProps = {
  children: React.ReactNode;
  onClick: () => void;
  className?: string;
};
const Button: React.FC<ButtonProps> = ({ children, onClick, className }) => {
  const buttonClass =
    "text-white text-center text-sm rounded w-16 px-2 py-2 bg-blue-600 hover:bg-blue-700 shadow shadow-gray-800/50 dark:bg-blue-600 dark:hover:bg-blue-700";

  return (
    <button type="button" onClick={onClick} className={`${buttonClass} ${className}`}>
      {children}
    </button>
  );
};

type TabsProps = {
  titles: string[];
  tabIndex: number;
  setTabIndex: (index: number) => void;
};
const Tabs: React.FC<TabsProps> = ({ titles, tabIndex, setTabIndex }) => {
  const titleClass =
    "w-full inline-block text-white bg-cyan-500 p-4 border-b-2 dark:border-gray-700 dark:text-gray-200 hover:text-white";
  const TitleSelectedClass = " border-blue-600 dark:border-gray-300 active";
  return (
    <ul className="flex text-sm font-medium text-center border-gray-200 mt-8">
      {titles.map((title, ix) => {
        return (
          <li key={ix} className="w-full">
            <a
              onClick={(_) => setTabIndex(ix)}
              className={titleClass + (ix === tabIndex ? TitleSelectedClass : "")}
            >
              {title}
            </a>
          </li>
        );
      })}
    </ul>
  );
};

export default App;
  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>React App</title>
  </head>
  <body class="dark:bg-gray-800">
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
   </body>
</html>

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ