아이콘 사용 및 관리

아이콘 기법 선택

아이콘 저장과 렌더링은 별개의 선택사항입니다. 예를 들어 SVG 스프라이트는 CSS 아이콘 시스템에 아이콘을 제공하거나 <use>로 참조될 수 있습니다.

  • CSS 아이콘 시스템: CSS가 아이콘을 표시할 시기와 아이콘을 선택해야 하는 경우에 사용합니다. 단색 아이콘에는 마스크를 사용하고 전체 색상 아이콘에는 배경 이미지를 사용합니다.
  • <use>가 있는 SVG: 기본 제공 스타일 옵션으로 충분한 경우 기호를 재사용하는 데 사용합니다. CSS는 참조된 SVG의 콘텐츠를 임의로 스타일 지정하거나 애니메이션화할 수 없지만, 상속된 속성과 맞춤 속성은 기호가 이를 사용하도록 설계된 경우 전달될 수 있습니다. 예를 들어 경로에서 fill="var(--icon-accent, currentColor)"를 사용하여 CSS가 아이콘의 해당 부분에 색상을 지정하도록 할 수 있습니다.
  • 인라인 SVG: SVG 콘텐츠의 임의 스타일 지정 또는 애니메이션이 필요한 경우에 사용합니다.
  • CSS 필터가 있는 <img>: 아이콘 색상을 변경할 때는 이 방법을 사용하지 마세요. 필터는 이미지에 임의의 CSS 색상 값을 안정적으로 적용할 수 없으므로 색상 변경이 번거롭고 유연하지 않습니다.

결정하려면 두 가지 질문을 해보세요. CSS에서 아이콘을 선택하거나 교체해야 하나요 (구성요소 기본값, 상태 변경, 테마 설정)? 답이 '예'라면 CSS 아이콘 시스템을 사용하세요. 색상 외에 아이콘 내부까지 도달해야 하나요? 그렇고 아이콘 소스를 제어하는 경우 맞춤 속성으로 기호를 디자인하고 <use>를 사용합니다. 소스를 제어하지 않는 경우 SVG를 인라인합니다. 서드 파티 아이콘 세트는 맞춤 속성을 노출하는 경우가 거의 없으므로 <use>를 사용하면 색상 외에는 거의 제공되지 않습니다.

삽입된 아이콘 또는 원격 아이콘

CSS 아이콘 시스템과 <use> 모두 데이터 URI, 개별 파일 또는 스프라이트에서 아이콘을 가져올 수 있습니다. 삽입 (데이터 URI 또는 인라인 SVG)은 요청을 방지하고 첫 번째 페인트로 렌더링하지만 CSS 또는 HTML을 확장하고 별도로 캐시할 수 없으므로 모든 페이지에 표시되는 작은 아이콘 집합에 적합합니다. 개별 파일은 독립적으로 캐시되고 요청 시 로드되며, 각 요청에 대한 비용이 발생합니다. 스프라이트는 전체 집합에 대해 캐시 가능한 하나의 요청이지만 사용 여부와 관계없이 모든 아이콘을 다운로드하며 <use>는 동일한 출처의 스프라이트만 참조할 수 있습니다. 소수의 핵심 아이콘에는 삽입을 사용하고, 더 큰 세트에는 스프라이트 또는 개별 파일을 사용하는 것이 좋습니다.

페이지에서 한 번에 수천 개의 아이콘을 렌더링하는 경우 실제 페이지와 애셋을 벤치마킹합니다. 스트레스 테스트에서 인라인 SVG는 이 규모에서 약간의 성능 이점이 있었지만 결과는 브라우저와 구현에 따라 다릅니다. Cloud Four의 SVG 아이콘 스트레스 테스트

기본 접근 방식: CSS 기반 아이콘 엔진

아이콘은 디자인 토큰입니다. 각 아이콘은 이미지를 보유하는 맞춤 속성이며, 모든 요소는 --icon-start 또는 --icon-end를 설정하여 아이콘을 표시할 수 있습니다. 엔진은 세 가지 CSS 기능을 결합합니다.

  • 등록된 맞춤 속성 (@property)은 --icon-start 및 --icon-end가 상속되지 않도록 하므로 구성요소에 설정된 아이콘이 하위 요소로 연쇄 적용되지 않습니다.
  • 컨테이너 스타일 쿼리 (@container style(...))는 요소에 아이콘이 할당된 시점을 감지하고 클래스나 마크업 변경 없이 렌더링합니다.
  • CSS 마스크 및 currentColor는 요소의 텍스트 색상을 따르는 가상 요소에서 아이콘을 렌더링합니다. 크기, 색상, 간격, 세로 오프셋은 적절한 대체가 있는 상속 가능한 --icon-style-* 속성에서 읽으므로 상위 요소에서 전체 하위 트리의 스타일을 다시 지정할 수 있습니다. --icon-style- 접두사는 이러한 설정이 --icon-*에도 있는 아이콘 토큰 자체와 충돌하지 않도록 합니다.

아이콘 디자인 토큰 정의

인라인 SVG 데이터 URI 또는 루트 상대 URL (예: url("/icons/trash.svg"))을 사용하여 SVG 아이콘과 --icon-style-* 재정의를 재사용 가능한 맞춤 속성으로 정의합니다. 외부 SVG 파일에서 아이콘을 로드할 때는 루트 상대 URL 또는 삽입된 데이터 URI를 사용합니다. 맞춤 속성을 통해 사용되는 상대 URL은 맞춤 속성이 정의된 스타일시트가 아닌 아이콘이 사용되는 스타일시트를 기준으로 결정될 수 있습니다. 이러한 스타일시트가 서로 다른 디렉터리에 있는 경우 URL이 잘못된 파일로 연결될 수 있습니다.

아이콘 토큰을 정의하는 스타일시트와 관련된 URL을 유지하기 위해 등록된 속성이 도움이 됩니다. 등록된 <url> 또는 <image>의 계산된 값은 이를 선언한 스타일시트에 따라 확인됩니다. initial-value의 URL을 사용하여 토큰을 <image>로 등록하는 데 의존하지 마세요. Chrome과 Safari는 문서에 대해 initial-value URL을 확인하므로 Firefox에서만 작동합니다. 또한 Chrome은 문법이 <url>로 시작하지 않는 한 문서에 대해 :root에 선언된 값을 확인합니다.

다른 디렉터리의 스타일시트와 같이 상대 URL을 피할 수 없는 경우 <url>로 시작하는 문법으로 토큰을 등록하고 :root에 실제 URL을 선언합니다. 이는 모든 브라우저에서 선언 스타일시트에 대해 확인됩니다.

@property --icon-caret {
  /* The syntax must start with <url>. A syntax that starts with <image> resolves against the document in Chrome. */
  syntax: "<url> | <image> | none";
  inherits: true;
  initial-value: none;
}

:root {
  /* Declared here, the URL resolves against this stylesheet */
  --icon-caret: url("icons/caret.svg");
}

이제 각 아이콘이 두 위치에 있고 상용구가 훨씬 더 많으므로 프로젝트에 서로 다른 디렉터리에 스타일시트가 있고 관리 가능한 수의 아이콘이 있는 경우에만 이 방법이 유용합니다. 아이콘이 12개 이상인 경우 각 @property를 직접 작성하는 대신 동일한 문법을 사용하여 CSS.registerProperty가 있는 스크립트에서 --icon-* 토큰(--icon-style-* 제외)을 등록합니다. 등록된 속성은 전역적이며 shadow root에서 작동하지 않으므로 모든 프로젝트에 적합하지 않을 수 있습니다. 루트 상대 URL 또는 데이터 URI를 사용할 수 있는 경우 더 간단하고 어디서나 작동합니다.

:root {
  /* Optional: override the built-in fallbacks (1em, currentColor, 0.3em, -0.02em) */
  --icon-style-size: 1em;
  --icon-style-spacing: 0.4em;
  --icon-trash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6h16Z'/%3E%3C/svg%3E");
  --icon-favorite: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
}

토큰에서 아이콘 렌더링

요소는 --icon-start (선행) 또는 --icon-end (후행)를 위의 토큰 중 하나로 설정하여 아이콘을 표시합니다. 두 개의 CSS가 이 할당을 렌더링된 아이콘으로 변환합니다.

먼저 @property를 사용하여 --icon-start 및 --icon-end를 등록합니다. "*" 문법은 모든 값을 허용하며 inherits: false은 중요한 부분입니다. 이 부분이 없으면 버튼에 설정된 아이콘이 모든 하위 요소에 상속되고 각 하위 요소도 아이콘을 렌더링합니다.

@property --icon-start {
  syntax: "*";
  inherits: false;
}

@property --icon-end {
  syntax: "*";
  inherits: false;
}

둘째, 컨테이너 스타일 쿼리는 요소에 속성이 설정된 시점을 감지하고 가상 요소에서 아이콘을 렌더링합니다. 의사 요소는 --icon-start를 상속하지 않으므로(상속되지 않는 것으로 등록됨) 첫 번째 선언은 inherit를 사용하여 값을 명시적으로 전달합니다. 나머지 선언은 크기 조절, 마스킹, 정렬, 간격을 처리합니다. 채우기는 기본적으로 currentColor로 설정되므로 아이콘은 요소의 텍스트 색상을 채택하고 전환을 따릅니다.

/* Style query for elements configuring a start icon */
@container style(--icon-start) {
  ::before {
    --icon-start: inherit; /* Forward the value inside the query block */
    content: "" / var(--icon-style-alt, "");
    display: inline-block;
    width: var(--icon-style-size, 1em);
    height: var(--icon-style-size, 1em);
    background-color: var(--icon-style-color, currentColor);
    mask: var(--icon-start) no-repeat center / contain;
    /* Center on the cap height, nudged slightly up for optical balance */
    vertical-align: calc(0.5cap - var(--icon-style-size, 1em) / 2 + var(--icon-style-voffset, -0.02em));
    flex: none;
    pointer-events: none;
    forced-color-adjust: none;
  }

  /* Automatically add spacing ONLY if the element has other text or sibling content */
  :where(:not(.icon, :empty))::before {
    margin-inline-end: var(--icon-style-spacing, 0.3em);
  }
}

각 선언은 다음과 같은 이유로 필요합니다.

  • vertical-align: middle는 x 높이의 절반에 맞춰 정렬되며, 대부분의 아이콘 세트에서 눈에 띄게 낮게 배치됩니다. 0.5cap를 중심으로 하면 --icon-style-size가 커짐에 따라 아이콘의 중심이 고정되고, 작은 음수 --icon-style-voffset는 아이콘 아래 공간보다 아이콘 위 공간에 더 많은 가중치를 두는 눈을 보정합니다.
  • flex: none: 빈 가상 요소의 최소 크기는 0이므로 플렉스 버튼 내부에서 라벨이 길면 가장 먼저 축소되며, 아무것도 남지 않을 수도 있습니다. 아이콘 크기는 디자인 결정이며 레이아웃 압박으로 협상할 수 있는 것이 아닙니다.
  • pointer-events: none: 아이콘이 클릭을 가로채거나 event.target가 되지 않도록 합니다.
  • forced-color-adjust: none: 강제 색상 모드에서는 브라우저가 background-color를 대체하여 마스크 아이콘이 사라집니다. 마스크 채우기는 유지되고 currentColor는 여전히 강제 텍스트 색상을 따릅니다.
  • content: "" / var(--icon-style-alt, ""): 생성된 콘텐츠의 대체 텍스트를 사용하면 아이콘 전용 컨트롤이 --icon-style-alt를 통해 아이콘에서 액세스 가능한 이름을 가져올 수 있습니다(아래의 아이콘 전용 컨트롤 참고). 기본 빈 alt는 보조 기술에서 장식 아이콘을 숨깁니다.

후행 아이콘의 경우 --icon-start 및 ::before 대신 --icon-end 및 ::after를 사용하여 동일한 패턴을 따릅니다. mask: var(--icon-end)를 사용하고 margin-inline-end 대신 margin-inline-start로 간격을 적용합니다.

독립형 및 아이콘 전용 요소

독립형 아이콘 또는 아이콘 전용 버튼의 경우 빈 요소 (아이콘 전용 컨트롤의 버튼 자체)에 .icon를 직접 적용하고 --icon로 아이콘을 설정합니다. 여기서 --icon는 상위 수준 속성입니다. .icon:empty 규칙은 이를 선행 아이콘의 --icon-start에 매핑합니다.

.icon:empty {
  --icon-start: var(--icon);
}

사용 예

스타일시트의 구성요소 기본값

아이콘을 맞춤 속성으로 사용할 때의 주요 이점은 아이콘이 계단식에 참여한다는 것입니다. 선택기에 아이콘을 설정하면 마크업을 변경하지 않고도 일치하는 모든 요소에 아이콘이 적용됩니다.

.button-delete {
  --icon-start: var(--icon-trash);
}

a[href^="http"]:not([href*="example.com"]) {
  --icon-end: var(--icon-external);
}
<button class="button-delete">Delete item</button>

상태에 따라 아이콘 변경

아이콘은 속성일 뿐이므로 상태 변경은 일반적인 CSS 재정의입니다. 표시하거나 숨길 두 번째 요소가 없고 전환할 클래스가 없습니다.

.button-expand {
  --icon-start: var(--icon-caret-right);
}

.button-expand[aria-expanded="true"] {
  --icon-start: var(--icon-caret-down);
}

표시에도 동일하게 적용됩니다. 툴바와 같은 컨테이너에 --icon-style-size, --icon-style-color 또는 --icon-style-spacing를 설정하면 컨테이너 내부의 모든 아이콘이 업데이트됩니다.

인라인 스타일이 있는 일회성 아이콘

구성요소 규칙이 적용되지 않는 요소는 동일한 속성을 인라인으로 설정할 수 있습니다(예: <button style="--icon-start: var(--icon-trash)">). 이를 기본값이 아닌 이스케이프 해치로 취급합니다. 동일한 아이콘이 동일한 종류의 요소에 두 번 이상 표시되면 스타일시트 규칙으로 이동합니다.

아이콘 전용 컨트롤

아이콘 전용 버튼은 위에서 설명한 .icon 클래스와 --icon를 사용합니다. aria-label를 사용하여 버튼에 액세스 가능한 이름을 지정하거나 --icon-style-alt를 통해 아이콘이 이름을 제공하도록 합니다. 둘 다 없으면 CSS 생성 아이콘은 장식용입니다. 독립형 정보 아이콘의 경우 role="img" 및 aria-label를 사용하여 .icon 요소를 노출합니다.

.button-close {
  --icon: var(--icon-close);
  --icon-style-alt: "Close";
}
<!-- Icon-only interactive control, icon and name from the stylesheet -->
<button class="icon button-close"></button>

<!-- Icon-only interactive control, named via aria-label -->
<button class="icon" style="--icon: var(--icon-trash);" aria-label="Delete item"></button>

<!-- Standalone informative icon -->
<span class="icon" style="--icon: var(--icon-favorite);" role="img" aria-label="Favorite"></span>

SVG 직접 사용

CSS 아이콘 시스템이 적합하지 않은 경우 인라인 SVG와 <use>를 대안이 아닌 함께 사용하세요. 페이지에 아이콘이 처음 표시될 때 SVG를 인라인하고 반복할 때마다 <use href="#id">로 참조합니다. 로컬 참조는 요청 비용이 들지 않으며 교차 출처 제한이 없습니다. 여러 페이지에서 공유되는 아이콘을 위해 외부 스프라이트 (<use href="/icons.svg#id">)를 예약합니다.

<use>를 사용하면 참조된 콘텐츠가 섀도 트리에 클론되므로 페이지 CSS가 요소를 선택할 수 없습니다. 상속되는 것은 color, currentColor, 모든 맞춤 속성입니다. 최신 CSS는 SVG 표시 속성 외에도 많은 도형 속성을 변경할 수 있으므로, 제어하는 아이콘이 필요한 후크를 정확하게 노출할 수 있습니다(예: 특정 경로의 stroke-width="var(--icon-stroke, 2)" 또는 d: var(--icon-path, path(…))).

CSS d는 Safari에서 지원되지 않으므로 이러한 지원 격차가 허용되는 경우에만 이 맞춤설정을 사용하세요. 아이콘을 제어하지 않는 경우 인라인으로 표시합니다.

알아두면 좋은 <use>가지 주의사항:

  • viewBox는 <symbol> 또는 참조된 <svg>에 있어야 합니다. <use>를 래핑하는 외부 <svg>는 참조에서 이를 선택하지 않으므로 래퍼에서 width 및 height를 설정하고 viewBox를 기호와 함께 유지합니다.
  • 스타일은 어느 방향으로도 경계를 넘지 않습니다. 외부 스프라이트 내부의 <style>는 페이지에 적용되지 않으며 페이지 스타일시트는 참조된 트리 내부에 적용되지 않습니다. 상속된 속성과 맞춤 속성만 전달됩니다.
  • 외부 참조는 동일 출처만 가능하며 교차 출처 스프라이트는 자동으로 실패합니다.
  • 문서에 인라인된 스프라이트는 페이지의 id 네임스페이스를 공유하므로 충돌을 방지하기 위해 네임스페이스 기호 ID를 사용합니다.

CSS 아이콘 시스템과 마찬가지로 장식용 SVG를 aria-hidden="true"로 표시하고 포함된 컨트롤에 자체 액세스 가능한 이름을 지정합니다.

대체 전략

등록된 맞춤 속성

@property가 없으면 --icon-start 및 --icon-end는 일반적인 상속 맞춤 속성처럼 동작하므로 요소에 설정된 아이콘이 하위 요소에도 렌더링됩니다. 스타일 쿼리 (@container style(--icon-start) { * { --icon-start: initial; } }) 내에서 하위 요소의 속성을 재설정하여 이를 방지하세요. 아래의 Safari 해결 방법과 동일한 접근 방식입니다.

컨테이너 스타일 쿼리

컨테이너 스타일 쿼리가 없는 브라우저를 지원하려면 클래스 또는 구성요소 선택기를 대체 후크로 사용하세요. 맞춤 속성에서 아이콘 선택을 유지합니다. 인라인 스타일 선택자만으로는 CSS 규칙을 통해 할당된 아이콘을 놓칠 수 있습니다.

:where(.icon-start, .icon:empty)::before {
  content: image-set(linear-gradient(transparent 0 100%), var(--icon-start)) / var(--icon-style-alt, "");
  /* icon styles */
}

:where(.icon-start)::before {
  --icon-start: inherit;
}

:where(.icon:empty)::before {
  --icon-start: var(--icon);
}

:where(.icon-start:not(.icon, :empty))::before {
  margin-inline-end: var(--icon-style-spacing, 0.3em);
}

후행 아이콘의 경우 .icon-start, --icon-start, ::before 대신 .icon-end, --icon-end, ::after와 동일한 클래스 후크 대체 기능을 사용합니다. margin-inline-end 대신 margin-inline-start를 사용하여 비어 있지 않은 콘텐츠에 간격을 적용합니다.

이 image-set() 패턴은 맞춤 속성이 설정되지 않은 경우 content를 무효화하므로 none로 대체되고 클래스를 추가하거나 삭제하지 않고도 가상 요소가 사라집니다. 의사 요소의 다른 스타일을 삭제하거나 다른 ::before 또는 ::after 규칙과의 충돌을 방지하지 않으므로 이러한 후크는 의사 요소를 아이콘에 사용할 수 있는 경우에만 사용하세요.

Safari 브라우저 해결 방법

Safari에는 아이콘에 영향을 미치는 두 가지 렌더링 버그가 있습니다. WebKit #301609는 사용자 상호작용이 있을 때까지 아이콘이 표시되지 않도록 할 수 있으며, #320220도 아이콘 렌더링에 영향을 미칩니다. 아래 해결 방법은 두 가지 문제를 모두 해결합니다.

@keyframes webkit-301609 {}

@supports ((-webkit-nbsp-mode: normal) or (-webkit-touch-callout: none) or (font: -apple-system-body)) and (content-visibility: auto) {
  @layer webkit-301609-fix {
    ::before, ::after {
      animation: webkit-301609 0s;
    }
  }
}

@supports 쿼리는 Safari만 타겟팅하고 @layer는 애니메이션 충돌을 줄입니다.

WebKit 버그 #325900으로 인해 마우스 오버 시 아이콘 변경과 같은 경우에 --icon-start의 직접 재정의가 적용되지 않을 수 있습니다. 이는 상속되지 않는 등록된 속성의 해결 방법에도 영향을 미칩니다. Safari의 경우 --icon-start를 상속으로 등록하고 스타일 쿼리 내의 하위 요소에서 재설정합니다.

@supports ((-webkit-nbsp-mode: normal) or
           (-webkit-touch-callout: none) or
           (font: -apple-system-body)) and (content-visibility: auto) {
  /* Override previous declaration */
  @property --icon-start {
    syntax: "*";
    inherits: true;
  }

  @layer webkit-325900-fix {
    @container style(--icon-start) {
      * {
        --icon-start: initial;
      }
    }
  }
}

재설정하면 아이콘 값이 하위 요소로 전파되지 않습니다. 레이어에 유지하면 레이어되지 않은 아이콘 선언이 재설정보다 우선 적용됩니다.