Cloudscape + Next.jsでダークモード切り替え機能を実装する

個人的にはダークモードの方がおめめに優しくて好きです。
2023.12.22

最近では、WebサイトやWebアプリケーション、あるいはOSにおいてダークモードがサポートされているケースが増えてきています。
(PC向けのWebアプリケーションとして)要件として必須であるケースはまだそう多くないかもしれませんが、UX向上のためにできればダークモードも提供できるようにしておきたいところです。

最近、UIフレームワークとしてCloudscapeを利用する機会があるのですが、Cloudscapeではダークモードがサポートされており簡単に導入することができます。

本エントリでは、Cloudscape( +Next.js)を利用したアプリケーションにダークモードの切り替え機能を実装する方法を紹介します。
なお、今回作成したサンプルは以下のリポジトリで公開しています。
https://github.com/amotz/cloudscape-dark-mode-sample

検証環境

  • next 14.0.4
  • react 18.2.0
  • cloudscape-design/components 3.0.468
  • cloudscape-design/global-styles 1.0.20

Cloudscapeとは

Cloudscapeは、AWSが作成しOSSとして公開しているデザインシステムです。
主にAWSの製品やサービス(マネジメントコンソール)向けに構築され使用されています。

UIフレームワークとして様々なコンポーネントが用意されており、AWSマネジメントコンソールに近い雰囲気のUIを簡単に実現できます。
なお、2023/12/22時点では、CloudscapeはReactのみをサポートしています。

Cloudscapeの導入に関しては、以下が参考になります。

Cloudscapeでのダークモード対応

比較的リッチなUIフレームワークでは、ダークモードがサポートされているケースも多いと思いますが、Cloudscapeにおいてもダークモードがサポートされています。

Cloudscape - Visual modes

CloudscapeではGlobal StyleのJSヘルパーが用意されており、こちらを利用することでダークモード/ライトモードの切り替えを簡単に行うことができます。

Cloudscape - Manipulate global styles using JS helpers

import { applyMode, Mode } from '@cloudscape-design/global-styles';

// apply a color mode
applyMode(Mode.Dark);
applyMode(Mode.Light);

実装サンプル

それでは、実際にCloudscape(+ Next.js)を利用してダークモード実装を行ってみます。
今回は、Toggleコンポーネントを利用してダークモードの切り替えを行うサンプルを作成します。

適当にCloudscapeを利用したNext.jsアプリケーションの画面を作成してから、ダークモード切り替えを行う機能を仕込んでみます。

pages/index.tsx

import {
  AppLayout,
  BreadcrumbGroup,
  SideNavigation,
  Flashbar,
  ContentLayout,
  Header,
  Container,
  Link,
  TopNavigation,
  SpaceBetween,
  FormField,
  Form,
  Button,
  Input,
  Tiles,
  HelpPanel,
  Toggle,
  Alert,
} from "@cloudscape-design/components";
import { applyMode, Mode } from "@cloudscape-design/global-styles";
import { useState, useEffect } from "react";

export default function Home() {
  const [tileValue, setTileValue] = useState("");
  const [useDarkMode, setUseDarkMode] = useState(false);

  useEffect(() => {
    applyMode(useDarkMode ? Mode.Dark : Mode.Light);
  }, [useDarkMode]);

  return (
    <>
      <TopNavigation
        identity={{
          href: "#",
          title: "Sample Service with Cloudscape",
        }}
        utilities={[
          {
            type: "button",
            iconName: "notification",
            title: "Notifications",
            ariaLabel: "Notifications (unread)",
            badge: true,
            disableUtilityCollapse: false,
          },
          {
            type: "menu-dropdown",
            iconName: "settings",
            ariaLabel: "Settings",
            title: "Settings",
            items: [
              {
                id: "settings-org",
                text: "Organizational settings",
              },
              {
                id: "settings-project",
                text: "Project settings",
              },
            ],
          },
          {
            type: "menu-dropdown",
            text: "Customer Name",
            description: "email@example.com",
            iconName: "user-profile",
            items: [
              { id: "profile", text: "Profile" },
              { id: "preferences", text: "Preferences" },
              { id: "security", text: "Security" },
              {
                id: "support-group",
                text: "Support",
                items: [
                  {
                    id: "documentation",
                    text: "Documentation",
                    href: "#",
                    external: true,
                    externalIconAriaLabel: " (opens in new tab)",
                  },
                  { id: "support", text: "Support" },
                  {
                    id: "feedback",
                    text: "Feedback",
                    href: "#",
                    external: true,
                    externalIconAriaLabel: " (opens in new tab)",
                  },
                ],
              },
              { id: "signout", text: "Sign out" },
            ],
          },
        ]}
      />
      <AppLayout
        breadcrumbs={
          <BreadcrumbGroup
            items={[
              { text: "Home", href: "#" },
              { text: "Service", href: "#" },
            ]}
          />
        }
        navigationOpen={true}
        navigation={
          <SideNavigation
            header={{
              href: "#",
              text: "Sample Service",
            }}
            items={[{ type: "link", text: `Page #1`, href: `#` }]}
          />
        }
        notifications={
          <Flashbar
            items={[
              {
                type: "info",
                dismissible: true,
                content: "This is an info message.",
                id: "message_1",
              },
            ]}
          />
        }
        toolsOpen={true}
        tools={<HelpPanel header={<h2>Overview</h2>}>Help content</HelpPanel>}
        content={
          <ContentLayout
            header={
              <Header
                variant="h1"
                info={<Link variant="info">Info</Link>}
                actions={
                  <Toggle
                    onChange={({ detail }) => setUseDarkMode(detail.checked)}
                    checked={useDarkMode}
                  >
                    Dark Mode
                  </Toggle>
                }
              >
                Page header
              </Header>
            }
          >
            <Container
              header={<Header variant="h2">Form container header</Header>}
            >
              <Form
                actions={
                  <SpaceBetween direction="horizontal" size="xs">
                    <Button formAction="none" variant="link">
                      Cancel
                    </Button>
                    <Button variant="primary">Submit</Button>
                  </SpaceBetween>
                }
              >
                <SpaceBetween direction="vertical" size="l">
                  <Alert statusIconAriaLabel="Warning" type="warning">
                    This is a warning message.
                  </Alert>
                  <FormField label="First field">
                    <Input value="" />
                  </FormField>
                  <FormField label="Second field">
                    <Input value="" />
                  </FormField>
                  <FormField label="Third field">
                    <Tiles
                      onChange={({ detail }) => setTileValue(detail.value)}
                      value={tileValue}
                      items={[
                        {
                          label: "Item 1 label",
                          description: "This is a description for item 1",
                          value: "item1",
                        },
                        {
                          label: "Item 2 label",
                          description: "This is a description for item 2",
                          value: "item2",
                        },
                      ]}
                    />{" "}
                  </FormField>
                </SpaceBetween>
              </Form>
            </Container>
          </ContentLayout>
        }
      />
    </>
  );
}

これでダークモード切り替えの機能が実装できたので、ビルドしてみます。
アプリを起動してみると、ダークモードの切り替えができるようになっています!

おわりに

Cloudscapeを利用したNext.jsアプリケーションにダークモードの切り替え機能を実装してみました。
リッチなUIフレームワークはダークモードがサポートされているケースも多いと思うので、要件として必須でなくとも実装コストが低そうであれば積極的に導入していきたいですね。

どなたかの参考になれば幸いです。

参考