とにかく画像を楽に扱う
まとめ
- SVGは
unplugin-icons
とiconify
を活用しましょう。 - SVGアイコンにはTailwindCSSやUnoCSSを利用する方法もあります。
- SVG以外の画像には
Unpic
を使うと便利です。
SVG
SVGを表示する方法はいくつかありますが、柔軟性を考慮しなければimg
タグでも表示可能です。
しかし、一般的にSVGアイコンはサイズだけでなく、色もコントロールしたいことが多いでしょう。
ex. 選択状態だと赤色になるなど
img
タグで表示するためだけに色が異なる画像を複数用意するのは、管理が大変ですし避けたいところです。
また、Hoverなどのイベントで色を変える場合、CSSでカラーをコントロールする必要があります。 しかし、アイコン一つ一つをコンポーネントにするのは手間がかかります…
そんな時は、unplugin-icons
とiconify
が最適です。
unplugin-iconsとiconify
iconify
はアイコンセットのフレームワークで、有名なアイコンセットやアイコンセットを自作するための各種ツールを提供しています。
unplugin-icons
はiconify
のアイコンパック形式をサポートしているため、有名なOSSアイコンパックを簡単に組み込めます。
さらに、unplugin-icons
はunplugin
をベースに実装されているため、非常に多くの環境で動作します。
UIフレームワークやバンドラーに依存せず、アイコンの管理を統一することが可能です。
unplugin-iconsのセットアップ
iconify
を利用する場合、セットアップは非常に簡単です。
以下にVite
+ Svelte
のセットアップ例を示します。
npm i -D unplugin-icons @iconify/json
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
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
TailwindCSS
やUnoCSS
と併用すると、セットアップが簡単になります。
UnoCSS
UnoCSS
はiconify
をサポートするプラグインを公式が提供しています。
インターフェースもunplugin-icons
と共通なため、unplugin-icons
の設定を流用することも可能です。
# unplugin-iconsと併用する場合は`@iconify/utils`は不要ですnpm i -D @iconify/json @iconify/utils
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
を使用します。
npm i -D @egoist/tailwindcss-icons @iconify/tools @iconify/types @iconify/utils
以下のコードはIssueのコードを参考にしています。
Issue:https://github.com/egoist/tailwindcss-icons/issues/37#issuecomment-2026939421
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', }), },})
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以外の画像
png
やjpg
などの画像はそのまま配信することも可能ですが、注意しないと想定以上に大きな画像のまま公開されることがあります。
可能であれば、webp
やavif
に変換し、画像サイズをできるだけ小さくすることが望ましいです。
最近のUIフレームワークやメタフレームワークでは、画像の最適化をサポートしているものが増えています。
- Next.js:
next/image
- Nuxt.js:
NuxtImg
- SvelteKit:
@sveltejs/enhanced-img
- Astro:
astro:assets
orastro-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/
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フレームワークを利用できる状況でない限り、ここまで意識する必要はないかもしれません。
それでも一度覚えておけばフレームワーク問わず知見を流用できるため、ぜひ一度試してみてください。