アイコンの実装方法

ウェブページにおけるアイコンの実装方法はさまざまです。マークアップに img 要素を配置する方法もあるし、CSS から背景画像やアイコン・フォントを使う方法もあります。そういった中からどの方法を採用すべきかを判断するには、HTML Standard の Requirements for providing text to act as an alternative for images にあるとおり、「そのアイコンの意味を伝えるテキストが付随するかどうか」を考える必要があります。

テキストが付随しないアイコン

家のアイコンだけでホームページへのリンクを表す場合など、ラベルとしてのテキストが存在しないアイコンは、自分自身でその意味を伝える必要があります。こういったアイコンの実装方法は限られていて、ほぼ 1 つしかありません。alt 属性に代替テキストとしてラベルを指定した img 要素をマークアップに配置する方法です。

<a href="/">
    <img alt="Home" src="icon-home.png">
</a>

CSS による背景画像やアイコン・フォントで表現する方法もありますが、その画像やフォントが意図どおりに読み込まれなかった場合に意味がまったく伝わらなくなってしまいます。必ず、適切な alt 属性をともなった img 要素をマークアップに配置すべきです。

なお、HTML 文書にアイコン画像を埋め込む方法としては、img 要素のほかにインラインの svg 要素も考えられます。ここでは詳しく触れませんが、その場合、titledesc といった要素による代替テキストのほか、foreignObject 要素などによるフォールバックも考慮する必要があるでしょう。

テキストが付随するアイコン

家のアイコンのすぐ下に Home という文字列があるような、その周囲にラベルとしてのテキストがあるアイコンはどうでしょうか。この場合、ユーザーに意味を伝える目的はテキストが果たしており、アイコンの役割はテキストを補助することであると言えます。そのため、アイコンの実装方法の自由度は高いです。

まず、マークアップに画像を埋め込む方法。この場合、代替テキストは空文字列である必要があります。

<a href="/">
    <img alt="" src="icon-home.png">
    Home
</a>

CSS を利用するなら、擬似要素とスプライト画像による方法が手軽です。マークアップにはテキスト以外に追加の要素や属性は必要ありません。

<style>
a[href="/"]:before {
    background: url(sprites.png) no-repeat 0 -160px;
    content: "";
    display: inline-block;
    height: 32px;
    width: 32px;
}
</style>

<a href="/">
    Home
</a>

アイコン・フォントも利用できますが、スクリーン・リーダーを考慮した追加のマークアップなど、安全でアクセシブルな実装を実現するにはそれなりの手間がかかります。

<style>
.icon-home:before {
    font-family: "icomoon";
    content: "\e608";
}
</style>

<a href="/">
    <span class="icon-home" aria-hidden="true"></span>
    Home
</a>

これら CSS を利用した実装は、マークアップに画像を埋め込む手法に比べ、パフォーマンスやメンテナビリティに優れています。しかし、画像やアイコン・フォントが利用できない環境ではアイコンは表示されないため、あくまでラベルとしてのテキストが付随する場合にのみ採用すべき方法です。

レスポンシブなラベル

いわゆるレスポンシブ・デザインでは、こういったアイコンに付随するテキストの有無を、画面のサイズなどによって切り替えたい場合があります。画面が狭いときはアイコンのみで、十分に広ければテキストも表示、というように。

結論から言うと、これを HTML と CSS だけで実装するとどうしても問題の生じる可能性があるため、JavaScript によって DOM を操作するのがより良い実装と考えます。以下、それぞれについて検証してみます。

CSS でテキストの表示を切り替える

テキストが表示されていない状態を考慮し、マークアップには alt 属性にラベルを指定した img 要素を配置します。ラベルとなるテキストは data-* 属性などに保存しておき、メディア・クエリに応じて擬似要素と content プロパティで表示を切り替える、というのが CSS による実装パターンです。

<style>
@media (min-width: 481px) {
    [data-label-text]:after {
        content: attr(data-label-text);
    }
}
</style>

<a href="/">
    <img alt="Home" src="icon-home.png">
    <span aria-hidden="true" data-label-text="Home"></span>
</a>

マークアップの alt 属性と CSS の生成内容で同じテキストが重複することになり、スクリーン・リーダーのユーザーを混乱させる可能性がありますが、この問題は aria-hidden 属性を持った要素を利用することで回避できます。

しかし、視覚系ブラウザーで画像が読み込まれなかった場合、同じテキストが繰り返し表示されてしまいます (この例では Home Home という表示になる)。

逆に、img 要素の alt 属性を空文字列とし、マークアップにあらかじめ含めたテキストの表示を CSS で切り替えるという実装でも、視覚系ブラウザーで画像が読み込まれなかった場合になにも表示されなくなってしまい、やはり問題があります。

このように、HTML と CSS だけによるアプローチでは、画像が利用できない環境に対応しきれません。

JavaScript で DOM を書き換える

理想的な実装は、画面が狭いときは alt 属性の指定された img 要素のみ、広いときは alt が空文字列の img とテキスト、というふうにマークアップを切り替える、ということになります。

<!-- 画面が狭いときは alt ありの img のみ -->
<a href="/">
    <img alt="Home" src="icon-home.png">
    <span class="text"></span>
</a>

<!-- 画面が広いときは alt が空の img とテキスト -->
<a href="/">
    <img alt="" src="icon-home.png">
    <span class="text">Home</span>
</a>

そこで以下のような JavaScript を考えてみました。

<!-- matchMedia() polyfill - https://github.com/paulirish/matchMedia.js -->
<script src="matchMedia.js"></script>
<script src="matchMedia.addListener.js"></script>

<script>
/**
 * Show/hide icon labels using media queries.
 *
 * @param {Object}  images     NodeList of icon images
 * @param {String}  mqString   Media query string
 * @param {Boolean} [fallback] Show label text for no-media-queries browsers
 * @example
 *     var navIcons = document.querySelectorAll('nav img');
 *     responsiveIconLabel(navIcons, '(min-width: 801px)', true);
 */
function responsiveIconLabel (images, mqString, fallback) {

    var mqSupported = window.matchMedia && window.matchMedia('only all').matches,
        textTemplate = document.createElement('span'),
        mql,
        mqListeners = [],
        i,
        len;

    textTemplate.className = 'text';

    for (i = 0, len = images.length; i < len; i++) {
        (function (img) {
            var alt = img.alt,
                txt = textTemplate.cloneNode(false);
            img.parentNode.insertBefore(txt, img.nextSibling);
            if (mqSupported) {
                mqListeners.push(swapAltText);
            } else if (fallback) {
                swapAltText(true);
            }
            function swapAltText (match) {
                if (match) {
                    img.alt = '';
                    txt.innerHTML = alt;
                } else {
                    img.alt = alt;
                    txt.innerHTML = '';
                }
            }
        })(images[i]);
    }

    if (mqSupported) {
        mql = window.matchMedia(mqString);
        mql.addListener(updateIconLabel);
        updateIconLabel(mql);
    }

    function updateIconLabel (mql) {
        var i,
            len,
            match = mql.matches;
        for (i = 0, len = images.length; i < len; i++) {
            mqListeners[i](match);
        }
    }
}
</script>

使い方は、まずマークアップとして alt 属性にラベルのテキストを指定した img 要素を用意します。

<a href="/">
    <img alt="Home" src="icon-home.png" class="icon-home">
</a>

アイコンの画像と、テキストを表示するメディア・クエリ文字列を引数にして、responsiveIconLabel 関数を呼び出します。3 つめの引数ではメディア・クエリをサポートしない環境でテキストを表示するかどうかを指定しています。

var homeIcon = document.querySelectorAll('.icon-home');
responsiveIconLabel(homeIcon, '(min-width: 801px)', true);

MediaQueryList オブジェクトの matches プロパティにより、指定したメディア・クエリにマッチしていれば imgalt を空にして、代わりにテキストを表示します。これは addListener メソッドにより、メディア・クエリの結果が変更されるたびに更新されます。

<a href="/">
    <img alt="" src="icon-home.png" class="icon-home">
    <span class="text">Home</span>
</a>

なお window.matchMedia をサポートしない環境のために matchMedia() polyfill を読み込んでいます。