コンテンツにスキップ

とにかく画像を楽に扱う

まとめ

  • SVGはunplugin-iconsiconifyを活用しましょう。
  • SVGアイコンにはTailwindCSSやUnoCSSを利用する方法もあります。
  • SVG以外の画像にはUnpicを使うと便利です。

SVG

SVGを表示する方法はいくつかありますが、柔軟性を考慮しなければimgタグでも表示可能です。 しかし、一般的にSVGアイコンはサイズだけでなく、色もコントロールしたいことが多いでしょう。

ex. 選択状態だと赤色になるなど

imgタグで表示するためだけに色が異なる画像を複数用意するのは、管理が大変ですし避けたいところです。

また、Hoverなどのイベントで色を変える場合、CSSでカラーをコントロールする必要があります。 しかし、アイコン一つ一つをコンポーネントにするのは手間がかかります…

そんな時は、unplugin-iconsiconifyが最適です。

unplugin-iconsとiconify

iconifyはアイコンセットのフレームワークで、有名なアイコンセットやアイコンセットを自作するための各種ツールを提供しています。 unplugin-iconsiconifyのアイコンパック形式をサポートしているため、有名なOSSアイコンパックを簡単に組み込めます。

さらに、unplugin-iconsunpluginをベースに実装されているため、非常に多くの環境で動作します。 UIフレームワークやバンドラーに依存せず、アイコンの管理を統一することが可能です。

unplugin-iconsのセットアップ

iconifyを利用する場合、セットアップは非常に簡単です。 以下にVite + Svelteのセットアップ例を示します。

Terminal window
npm i -D unplugin-icons @iconify/json
vite.config.ts
import { svelte } from '@sveltejs/vite-plugin-svelte'
import Icons from 'unplugin-icons/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
svelte(),
Icons({
compiler: 'svelte',
}),
],
})
<script>
import IconAccessibility from '~icons/carbon/accessibility'
import IconAccountBox from '~icons/mdi/account-box'
</script>
<IconAccessibility />
<!-- 通常のSVGタグのように扱えます -->
<IconAccountBox style='font-size: 2em; color: red' />
<!-- クラス名を指定してスタイルを適用できます -->
<IconAccountBox class='text-red-500' />

カスタムアイコンのセットアップ

上記ではiconifyのアイコンパックを利用しましたが、独自のSVGアイコンが必要な場合も多いでしょう。 unplugin-iconsでは、独自のアイコンパックをサポートしています。

詳細については、以下のリンクを参照してください: https://github.com/unplugin/unplugin-icons?tab=readme-ov-file#use-custom-external-collection-packages

vite.config.ts
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import Icons from 'unplugin-icons/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
// ...
Icons({
customCollections: {
// アイコンパックのパスを指定します
my: FileSystemIconLoader(
'./public/icon',
// SVGに設定されている色をcurrentColorに変更します
// これによりCSSで色をコントロールできるようになります
svg => svg.replace(/^<svg /, '<svg fill="currentColor" '),
),
},
}),
// ...
],
})

CSSでSVGアイコンを表示したい

ここまでは何かしらのUIフレームワークを前提としたセットアップでしたが、汎用的な手段としてCSSを利用する方法もあります。

参考資料:Icons in Pure CSS

TailwindCSSUnoCSSと併用すると、セットアップが簡単になります。

UnoCSS

UnoCSSiconifyをサポートするプラグインを公式が提供しています。 インターフェースもunplugin-iconsと共通なため、unplugin-iconsの設定を流用することも可能です。

Terminal window
# unplugin-iconsと併用する場合は`@iconify/utils`は不要です
npm i -D @iconify/json @iconify/utils
uno.config.ts
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
import { defineConfig, presetIcons } from 'unocss'
// unplugin-iconsと併用する場合はこれでもOK
// import { FileSystemIconLoader } from "unplugin-icons/loaders"
export default defineConfig({
presets: [
// ...o
presetIcons({
collections: {
my: FileSystemIconLoader(
'./public/icon',
svg => svg.replace(/^<svg /, '<svg fill="currentColor" '),
),
},
}),
// ...o
],
})
<!-- pattern: i-{collection_name}-{icon_name} -->
<!-- A basic anchor icon from Phosphor icons -->
<div class="i-ph-anchor-simple-thin" />
<!-- An orange alarm from Material Design Icons -->
<div class="i-mdi-alarm text-orange-400" />
<!-- A large Vue logo -->
<div class="i-logos-vue text-3xl" />
<!-- Sun in light mode, Moon in dark mode, from Carbon -->
<button class="i-carbon-sun dark:i-carbon-moon" />
<!-- Twemoji of laugh, turns to tear on hovering -->
<div class="i-twemoji-grinning-face-with-smiling-eyes hover:i-twemoji-face-with-tears-of-joy" />

TailwindCSS

TailwindCSSには公式のプラグインが提供されていないため、サードパーティ製のプラグインを使用する必要があります。 また、インターフェースが異なるため、unplugin-iconsとは異なるセットアップが必要です。

ここでは@egoist/tailwindcss-iconsを使用します。

Terminal window
npm i -D @egoist/tailwindcss-icons @iconify/tools @iconify/types @iconify/utils

以下のコードはIssueのコードを参考にしています。

Issue:https://github.com/egoist/tailwindcss-icons/issues/37#issuecomment-2026939421

icon.cjs
const { getIconCollections, iconsPlugin } = require('@egoist/tailwindcss-icons')
const {
cleanupSVG,
importDirectorySync,
isEmptyColor,
parseColors,
runSVGO,
} = require('@iconify/tools')
const { compareColors, stringToColor } = require('@iconify/utils/lib/colors')
const { myIconPath } = require('../../../../const.cjs')
/**
* @param {Record<string, string>} targets
* @returns {Record<string, import("@iconify/types").IconifyJSON>} icon set
*/
function getCollections(targets) {
/** @type {Record<string, import("@iconify/types").IconifyJSON>} */
const collections = {}
for (const [name, dir] of Object.entries(targets)) {
// Import icons
const iconSet = importDirectorySync(dir, {
includeSubDirs: false,
})
// Validate, clean up, fix palette and optimize
iconSet.forEachSync((name, type) => {
if (type !== 'icon')
return
const svg = iconSet.toSVG(name)
if (!svg) {
// Invalid icon
iconSet.remove(name)
return
}
// Clean up and optimize icons
try {
// Clean up icon code
cleanupSVG(svg)
// Change color to `currentColor`
// Skip this step if icon has hardcoded palette
const blackColor = stringToColor('black')
const whiteColor = stringToColor('white')
parseColors(svg, {
callback: (attr, colorStr, color) => {
if (!color) {
// Color cannot be parsed!
throw new Error(
`Invalid color: "${colorStr}" in attribute ${attr}`,
)
}
if (isEmptyColor(color)) {
// Color is empty: 'none' or 'transparent'. Return as is
return color
}
// Change black to 'currentColor'
if (
compareColors(
color,
// @ts-expect-error 型エラーを無視
blackColor,
)
) {
return 'currentColor'
}
// Remove shapes with white color
// @ts-expect-error 型エラーを無視
if (compareColors(color, whiteColor))
return 'remove'
// Icon is not monotone
return color
},
defaultColor: 'currentColor',
})
// Optimize
runSVGO(svg)
}
catch (err) {
// Invalid icon
console.error(`Error parsing ${name}:`, err)
iconSet.remove(name)
return
}
// Update icon
iconSet.fromSVG(name, svg)
})
collections[name] = iconSet.export()
}
return collections
}
module.exports = iconsPlugin({
collections: {
...getCollections({
my: './public/icon',
}),
},
})
tailwind.config.cjs
const { getCollections } = require('#@/feature/primitive/icon-tailwind.cjs')
const { getIconCollections, iconsPlugin } = require('@egoist/tailwindcss-icons')
const path = require('node:path')
module.exports = {
plugins: [
iconsPlugin({
collections: {
...getCollections({
my: path.join(__dirname, '/public/icon'),
}),
},
}),
],
}
<!-- pattern: i-{collection_name}-{icon_name} -->
<span class="i-mdi-home"></span>

SVG以外の画像

pngjpgなどの画像はそのまま配信することも可能ですが、注意しないと想定以上に大きな画像のまま公開されることがあります。 可能であれば、webpavifに変換し、画像サイズをできるだけ小さくすることが望ましいです。

最近のUIフレームワークやメタフレームワークでは、画像の最適化をサポートしているものが増えています。

  • Next.js: next/image
  • Nuxt.js: NuxtImg
  • SvelteKit: @sveltejs/enhanced-img
  • Astro: astro:assets or astro-imagetools

ただし、これらはそれぞれ微妙に使用感が異なり、リモート画像の最適化には手間がかかることがあります。

また、microCMSのようなCMSでは、imagixなどの画像最適化CDNを経由している場合があります。 この場合、フレームワークによる最適化ではなく、画像URLに特定のクエリパラメータを付与することで画像を最適化できます。 うまく活用すれば、ビルド時間や通信量の削減にもつながります。

しかし、ローカル画像とリモート画像でコンポーネントを切り替えたり、サービスごとに最適化コンポーネントを実装するのは手間がかかります。

そこで、Unpicがおすすめです。

Unpic

UnpicはCDN経由で画像を最適化するコンポーネントを複数のフレームワーク向けに提供しています。 URLからサービスを識別し、適切なクエリパラメータを付与してくれます。

加えて、Next.jsやAstroなど独自の最適化コンポーネントを提供しているフレームワークの場合、Unpicがそれらのラッパーを提供しています。 ローカルとリモートを自動で識別し、ローカル画像であればフレームワーク側の最適化コンポーネントで処理を行います。

現状ではNext.jsとAstroのみの対応ですが、今後SvelteKitやNuxt.jsなどのフレームワークもサポートされることを期待しています。(コントリビュートチャンスかも?)

Unpicのセットアップ

今回はローカル画像も対応しているNext.jsのセットアップを紹介します。

https://unpic.pics/img/svelte/

Terminal window
npm i -D @unpic/react
import { Image } from '@unpic/react/nextjs'
import logo from '../public/logo.png'
function LocalImage() {
// ビルド時、もしくはサーバサイドで最適化されます
return <Image alt="Logo" layout="constrained" src={logo} />
}
function RemoteImage() {
// リモート画像はCDN経由で最適化されます
return (
<Image
alt="Shopify product"
height={600}
layout="constrained"
src="https://cdn.shopify.com/static/sample-images/garnished.jpeg"
width={800}
/>
)
}

最後に

筆者は過去にAstro + Svelteの構成でプロジェクトに参加した際、あまり調査せずAstro Iconを採用した結果、SvelteコンポーネントでAstro Iconが使用できず、非常に面倒なことになりました(CSSのmask-imageで乗り切った)。 AstroやQwikのような複数のUIフレームワークを利用できる状況でない限り、ここまで意識する必要はないかもしれません。 それでも一度覚えておけばフレームワーク問わず知見を流用できるため、ぜひ一度試してみてください。