零弐壱蜂

CSS in JSでcontentプロパティにSVGコンポーネントを挿入する方法

背景

ReactでCSS in JSを利用している際、SVGアイコンのようなReactコンポーネントをCSSのcontentプロパティやbackground-imageプロパティに埋め込みたい場合がある。

環境

  • React
  • CSS in JS
    • Emotionを利用しているが、他のCSS in JSでも同様の方法で実現できるはず。
    • content: url()を想定しているが、background-image: url()でも同様の方法で実現できる。
  • SVGアイコンコンポーネント
    • @radix-ui/react-icons
      Reactコンポーネントとして提供されているSVGアイコンであれば良い。

方法

CSSの疑似要素(::before::after)のcontentプロパティや、要素のbackground-imageプロパティにSVGを表示する方法のひとつとして、data:image/svg+xmlスキームを利用する方法がある。

以下のような形式の文字列が必要になる。

data:image/svg+xml, + SVGのXMLコードをURLエンコードしたもの

SVGのXMLコードは、そのままではURLの一部として安全に使えない文字(例: <>#など)を含む可能性があるため、さらにencodeURIComponent関数を使ってURLエンコードする必要がある。

実装手順

Reactコンポーネントとして定義されたSVGアイコンを、上記のデータURI形式の文字列に変換する関数を作成する。この変換には、ReactコンポーネントをHTML文字列にレンダリングするrenderToStaticMarkup関数(react-dom/serverパッケージに含まれる)を利用する。

// SvgComponentToDataUrlScheme.ts
import type { ReactElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

export const SvgComponentToDataUrlScheme = (Component: ReactElement) => {
  // 1. Reactコンポーネントを純粋なSVGマークアップ文字列に変換
  const SVG = renderToStaticMarkup(Component);
  // 2. SVGマークアップをURLエンコード
  const encodedSvg = encodeURIComponent(svgMarkup);
  // 3. データURIスキームの接頭辞と結合して返す
  return `data:image/svg+xml,${encodedSvg}`;
};

上記のSvgComponentToDataUrlScheme関数を使って、実際にCSS in JSでSVGアイコンを表示する例は以下の通り。

import { SvgComponentToDataUrlScheme } from '@/lib/SvgComponentToDataUrlScheme';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { css } from '@emotion/react';

const IconExternalLink = SvgComponentToDataUrlScheme(<ExternalLinkIcon />);

export const IconExternalLinkStyle = css`
  &::before {
    display: inline-block;
    margin-left: 0.15em;
    vertical-align: middle;
    content: url(${IconExternalLink});
  }
`;

このように、SvgComponentToDataUrlScheme関数にSVGコンポーネントを渡すことで、CSSのurl()内で直接使用できるデータURI文字列を取得できる。

renderToStaticMarkuprenderToStringの違い

ReactコンポーネントをHTMLの文字列に変換する関数として、renderToStaticMarkuprenderToStringがある。今回のSVG変換ではrenderToStaticMarkupを使用したが、なぜこちらが適しているのか、両者の違いを明確にしておく。

renderToString

renderToStringは、主にサーバーサイドレンダリング(SSR)で利用される。Reactアプリケーションをサーバー側でHTML文字列に変換したものをブラウザに送信することで、初期表示を高速化したりSEOを向上させたりする目的で使われる。

import { renderToString } from 'react-dom/server';
import App from './App';

export function render(url, context) {
  return renderToString(<App />);
}

しかし、renderToStringは、data-reactrootdata-reactidなどのReactが内部で利用する属性を出力する。これは、ハイドレーションが必要な場合に利用される。
ハイドレーションが必要ない静的ページの場合、renderToStaticMarkupを使用すれば、Reactの特定のデータ属性やイベントハンドラは含まれず、純粋なHTMLが出力される。

renderToStaticMarkup

一方、renderToStaticMarkupは、Reactコンポーネントを純粋なHTML文字列に変換する。renderToStringとは異なり、Reactが内部的に使用する特別な属性(data-reactidなど)を出力しない。

この関数は、ハイドレーションが不要な静的なHTMLコンテンツを生成する場合に適している。今回のSVGアイコンの埋め込みでは、生成されたSVG文字列に対して後からReactがインタラクティブ性を付与する必要はない(ハイドレーションは不要)ため、余計な属性を含まず、ファイルサイズも小さくなるrenderToStaticMarkupがより適切な選択となる。

@types/react-dom/server.d.ts

Reactの型定義ファイル(@types/react-dom/server.d.ts)にも、これらの関数の違いが説明されている。

/**
 * Render a React element to its initial HTML. This should only be used on the server.
 * React will return an HTML string. You can use this method to generate HTML on the server
 * and send the markup down on the initial request for faster page loads and to allow search
 * engines to crawl your pages for SEO purposes.
 *
 * If you call `ReactDOMClient.hydrateRoot()` on a node that already has this server-rendered markup,
 * React will preserve it and only attach event handlers, allowing you
 * to have a very performant first-load experience.
 */

/**
 * React 要素を初期 HTML にレンダーします。これはサーバーでのみ使用する必要があります。
 * React は HTML 文字列を返します。このメソッドを使用して、HTML をサーバー上で生成し、
 * 初期リクエストでマークアップをダウンロードしてページの読み込みを高速化し、
 * 検索エンジンが SEO の目的でページをクロールできるようにすることができます。
 *
 * `ReactDOMClient.hydrateRoot()` をすでにこのサーバー側レンダリングされたマークアップを持つノードで呼び出すと、
 * React はそれを保持し、イベントハンドラーのみをアタッチして、
 * パフォーマンスの高い初回読み込み体験を提供できます。
 */
export function renderToString(element: ReactElement, options?: ServerOptions): string;
/**
 * Similar to `renderToString`, except this doesn't create extra DOM attributes
 * such as `data-reactid`, that React uses internally. This is useful if you want
 * to use React as a simple static page generator, as stripping away the extra
 * attributes can save lots of bytes.
 */

/**
 * `renderToString` と似ていますが、内部で React が使用する `data-reactid` などの
 * 追加の DOM 属性を作成しません。これは、React を単純な静的ページジェネレータとして
 * 使用する場合に便利で、余分な属性を削除することで多くのバイトを節約できます。
 */
export function renderToStaticMarkup(element: ReactElement, options?: ServerOptions): string;