שימוש בסמלים וניהול שלהם

בחירת טכניקה לסמל

אחסון הסמל ועיבוד הסמל הן שתי אפשרויות נפרדות. לדוגמה, ספרייט SVG יכול לספק סמל למערכת הסמלים של CSS או שאפשר להפנות אליו באמצעות <use>.

  • מערכת הסמלים של שירות ה-CSS: השתמשו באפשרות הזו אם אתם רוצים ששירות ה-CSS יבחר מתי להציג סמל ואיזה סמל להציג. השתמשו במסכה לסמל בצבע אחיד או בתמונת רקע לסמל בצבע מלא.
  • ‫SVG עם <use>: משתמשים באפשרות הזו כדי להשתמש שוב בסמל כשאפשרויות העיצוב המובנות שלו מספיקות. אי אפשר להשתמש ב-CSS כדי לעצב או להוסיף אנימציה לתוכן של קובץ ה-SVG שאליו מפנים, אבל אפשר להעביר מאפיינים שעוברים בירושה ומאפיינים מותאמים אישית כשהסמל מיועד להשתמש בהם. לדוגמה, נתיב יכול להשתמש ב-fill="var(--icon-accent, currentColor)" כדי לאפשר ל-CSS לצבוע את החלק הזה של הסמל.
  • ‫SVG מוטבע: משתמשים בו כשצריך עיצוב או אנימציה שרירותיים של תוכן ה-SVG.
  • <img> עם מסנני CSS: אל תשתמשו בשיטה הזו כדי לשנות את הצבע של סמלים. מסננים לא יכולים להחיל באופן מהימן ערך צבע שרירותי של CSS על התמונה, ולכן שינוי הצבע הוא מסורבל ולא גמיש.

כדי להחליט, צריך לשאול שתי שאלות. האם צריך לבחור או להחליף את הסמל ב-CSS (ברירות מחדל של רכיבים, שינויי מצב, עיצוב)? אם כן, צריך להשתמש במערכת הסמלים של שירות ה-CSS. האם צריך להגיע לתוך הסמל מעבר לצבע? אם כן, ואתם שולטים במקור הסמל, עצבו את הסמל עם מאפיינים מותאמים אישית והשתמשו ב-<use>. אם אתם לא שולטים במקור, השתמשו ב-SVG מוטבע. ערכות סמלים של צד שלישי חושפות לעיתים רחוקות מאפיינים מותאמים אישית, כך ש-<use> נותן לך צבע וקצת יותר מזה.

סמלים מוטמעים או מרוחקים

גם מערכת הסמלים של CSS וגם <use> יכולות לקבל סמלים מ-URI של נתונים, מקובץ בודד או מ-sprite. הטמעה (מזהי URI של נתונים או SVG מוטבע) מונעת בקשה ומעבדת את התמונה הראשונית (FP), אבל מנפחת את ה-CSS או ה-HTML ואי אפשר לשמור אותה במטמון בנפרד, כך שהיא מתאימה לקבוצה קטנה של סמלים שמופיעים בכל דף. קבצים בודדים נשמרים במטמון בנפרד ונטענים על פי דרישה, בעלות של בקשה אחת לכל קובץ. ספרייט הוא בקשה אחת שניתן לשמור במטמון עבור כל הקבוצה, אבל הוא מוריד כל סמל, בין אם הוא בשימוש ובין אם לא, ו-<use> יכול להפנות רק לספרייט מאותו מקור. עדיף להטמיע כמה סמלים עיקריים, ולהשתמש ב-sprite או בקבצים נפרדים לקבוצות גדולות יותר.

אם בדף מוצגים אלפי סמלים בבת אחת, כדאי לבדוק את הדף והנכסים בפועל. בבדיקת עומסים, ל-SVG מוטבע היה יתרון קטן בביצועים בקנה מידה כזה, אבל התוצאות תלויות בדפדפן ובאופן ההטמעה. בדיקת עומס של סמל SVG של Cloud Four

הגישה העיקרית: מנוע סמלים מבוסס-CSS

סמלים הם אסימוני עיצוב: כל סמל הוא מאפיין מותאם אישית שמכיל תמונה, וכל רכיב יכול להציג סמל על ידי הגדרת --icon-start או --icon-end. המנוע משלב שלוש יכולות של CSS:

  • מאפיינים מותאמים אישית רשומים (@property) מונעים מ---icon-start ומ---icon-end לעבור בירושה, כך שסמל שמוגדר ברכיב לא עובר בירושה לרכיבי הצאצא שלו.
  • שאילתות סגנון של קונטיינרים (@container style(...)) מזהות מתי מוקצה סמל לרכיב ומציגות אותו, בלי שינויים במחלקה או בתגי העיצוב.
  • מסכות CSS ו-currentColor מעבדות את הסמל ברכיב פסאודו שתואם לצבע הטקסט של הרכיב. הגודל, הצבע, המרווח וההיסט האנכי נקראים ממאפייני --icon-style-* שניתנים להעברה בירושה עם חלופות הגיוניות, כך שאפשר לשנות את הסגנון של עץ משנה שלם מאב קדמון. הקידומת --icon-style- מונעת התנגשות בין ההגדרות האלה לבין הטוקנים של הסמלים עצמם, שגם הם נמצאים ב---icon-*.

הגדרת טוקנים לעיצוב סמלים

מגדירים סמלי SVG וכל שינוי של --icon-style-* כמאפיינים מותאמים אישית שניתן לעשות בהם שימוש חוזר, באמצעות מזהי URI של נתוני SVG מוטבעים או כתובות URL יחסיות לשורש (למשל, url("/icons/trash.svg")). כשסמל נטען מקובץ SVG חיצוני, משתמשים בכתובת URL יחסית לשורש או במזהה URI של נתונים מוטמעים. כתובת URL יחסית שמשתמשים בה באמצעות מאפיין מותאם אישית יכולה להתייחס לגיליון הסגנונות שבו משתמשים בסמל, ולא לגיליון הסגנונות שבו מוגדר המאפיין המותאם אישית. אם גיליונות הסגנונות האלה נמצאים בספריות שונות, יכול להיות שכתובת ה-URL תפנה לקובץ הלא נכון.

כדי לשמור על כתובת URL שקשורה לגיליון הסגנונות שמגדיר את אסימון הסמל, מאפיין רשום אמור לעזור: הערך המחושב של <url> או <image> רשום נפתר ביחס לגיליון הסגנונות שהצהיר עליו. אל תסתמכו על רישום הטוקן בתור <image> עם כתובת ה-URL ב-initial-value. דפדפני Chrome ו-Safari פותרים כתובות URL של initial-value מול המסמך, כך שהן פועלות רק ב-Firefox. בנוסף, Chrome פותר ערכים שהוצהרו ב-:root מול המסמך, אלא אם התחביר מתחיל ב-<url>.

אם אין ברירה אלא להשתמש בכתובות URL יחסיות, כמו גיליונות סגנונות בספריות שונות, צריך לרשום את הטוקן בתחביר שמתחיל ב-<url> ולהצהיר על כתובת ה-URL בפועל ב-:root. הפעולה הזו פותרת את הבעיה בגיליון הסגנונות המוצהר בכל דפדפן:

@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");
}

כל סמל נמצא עכשיו בשני מקומות, ויש הרבה יותר קוד סטנדרטי, אז זה כדאי רק כשלפרויקט יש גיליונות סגנונות בספריות שונות ומספר סמלים שאפשר לנהל. אם יש יותר מכמה סמלים, רושמים את האסימונים של --icon-* (לא כולל --icon-style-*) מסקריפט עם CSS.registerProperty, באותו תחביר, במקום לכתוב כל @property באופן ידני. מאפיינים רשומים הם גלובליים ולא פועלים ב-shadow roots, כך שהם לא מתאימים לכל פרויקט. אם אפשר להשתמש בכתובות 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 הופכים את ההקצאה הזו לסמל שעבר עיבוד.

קודם צריך לרשום את --icon-start ואת --icon-end ב-@property. התחביר "*" מקבל כל ערך, ו-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: לאלמנט מדומה ריק יש גודל מינימלי של אפס, כך שבתוך כפתור גמיש הוא הדבר הראשון שמתכווץ כשהתווית ארוכה, לפעמים עד שהוא נעלם. גודל הסמל הוא החלטה עיצובית, ולא משהו שצריך להשתנות בגלל אילוצי פריסה.
  • ‫pointer-events: none: מונע מהסמל ללכוד קליקים או להפוך ל-event.target.
  • ‫forced-color-adjust: none: במצב צבעים מאולצים, הדפדפן מחליף את background-color, וכך סמל עם מסכה נעלם. מילוי המסכה נשמר, ו-currentColor עדיין פועל לפי צבע הטקסט שנכפה.
  • ‫content: "" / var(--icon-style-alt, ""): טקסט חלופי לתוכן שנוצר מאפשר לפקד שמכיל רק סמל לקבל את תווית הנגישות שלו מהסמל באמצעות --icon-style-alt (ראו "פקדים שמכילים רק סמלים" בהמשך). טקסט חלופי ריק כברירת מחדל מסתיר סמלים דקורטיביים מטכנולוגיה מסייעת.

כדי להוסיף סמל בסוף, פועלים לפי אותה תבנית עם --icon-end ו-::after במקום --icon-start ו-::before. משתמשים ב-mask: var(--icon-end) ומוסיפים רווחים באמצעות margin-inline-start במקום margin-inline-end.

רכיבים עצמאיים ורכיבים עם סמלים בלבד

בסמלים עצמאיים או בלחצנים שמכילים רק סמל, צריך להחיל את .icon ישירות על הרכיב הריק (הלחצן עצמו, במקרה של פקד שמכיל רק סמל) ולהגדיר את הסמל שלו באמצעות --icon. כאן, --icon הוא המאפיין ברמה הגבוהה יותר. הכלל .icon:empty ממפה אותו ל---icon-start בשביל סמל ראשי:

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

דוגמאות לשימוש

ברירות המחדל של הרכיב בגיליון הסגנונות

היתרון העיקרי של סמלים כמאפיינים מותאמים אישית הוא שהם משתתפים ב-cascade. מגדירים סמל בסלקטור וכל רכיב תואם מקבל אותו, בלי שינויים בתגי העיצוב:

.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 הוא דקורטיבי. בסמלי מידע עצמאיים, חושפים את הרכיב .icon באמצעות role="img" ו-aria-label:

.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 וכל מאפיין מותאם אישית. מעבר למאפייני התצוגה של SVG, ב-CSS מודרני אפשר לשנות גם הרבה מאפייני גיאומטריה, כך שסמל שאתם שולטים בו יכול לחשוף בדיוק את ה-hooks שאתם צריכים, למשל stroke-width="var(--icon-stroke, 2)" או אפילו d: var(--icon-path, path(…)) בנתיב מסוים.

המאפיין d של CSS לא נתמך ב-Safari, לכן כדאי להשתמש בהתאמה האישית הזו רק אם פער התמיכה הזה מקובל עליכם. אם אין לכם שליטה על הסמל, צריך להטמיע אותו.

‫<use> דברים שחשוב לדעת:

  • המאפיין viewBox חייב להיות ב-<symbol> או ב-<svg> שאליו מפנים. ה-<svg> החיצוני שעוטף את <use> לא מקבל אותו מההפניה, לכן צריך להגדיר את width ואת height בעטיפה ולשמור את viewBox עם הסמל.
  • הסגנונות לא חוצים את הגבול באף כיוון: <style> בתוך ספרייט חיצוני לא חל על הדף, וגיליונות הסגנונות של הדף לא חלים בתוך העץ שאליו מפנים. רק מאפיינים שעוברים בירושה ומאפיינים מותאמים אישית עוברים הלאה.
  • הפניות חיצוניות הן מאותו מקור בלבד. ספרייטים ממקורות שונים נכשלים בלי להציג הודעה.
  • ספרייטים שמוטמעים במסמך חולקים את מרחב השמות id של הדף, לכן צריך להשתמש במזהי סמלים של מרחב שמות כדי למנוע התנגשויות.

מסמנים רכיבי SVG דקורטיביים באמצעות aria-hidden="true" ונותנים לפקד שמכיל אותם תווית נגישות משלו, כמו במערכת הסמלים של CSS.

שיטות חלופיות

מאפיינים מותאמים אישית רשומים

בלי @property, המאפיינים --icon-start ו---icon-end מתנהגים כמו מאפיינים מותאמים אישית רגילים שעוברים בירושה, כך שסמל שמוגדר ברכיב מסוים יופיע גם ברכיבים הצאצאים שלו. כדי למנוע את זה, צריך לאפס את המאפיינים של הצאצאים בתוך שאילתת הסגנון (@container style(--icon-start) { * { --icon-start: initial; } }), כמו בפתרון העקיף של Safari שמתואר בהמשך.

שאילתות סגנון של מאגרים

כדי לתמוך בדפדפנים שלא תומכים בשאילתות סגנון של קונטיינרים, אפשר להשתמש בבוררים של רכיבים או מחלקות כ-fallback hooks. חשוב להשאיר את בחירת הסמלים במאפיינים המותאמים אישית. סלקטורים של סגנון מוטבע בלבד מפספסים סמלים שהוקצו באמצעות כללי 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);
}

לסמלים בסוף, משתמשים באותו class-hook חלופי עם .icon-end,‏ --icon-end ו-::after במקום .icon-start,‏ --icon-start ו-::before. כדי להוסיף רווחים לתוכן שאינו ריק, צריך להשתמש ב-margin-inline-start במקום ב-margin-inline-end.

התבנית הזו של image-set() הופכת את content ללא חוקי כשהמאפיין המותאם אישית לא מוגדר, כך שהוא חוזר ל-none והרכיב המדומה נעלם בלי להוסיף או להסיר מחלקה. הוא לא מסיר את הסגנונות האחרים של הרכיב המדומה ולא מונע התנגשויות עם כללים אחרים של ::before או ::after, לכן צריך להשתמש ב-hooks האלה רק כשהרכיבים המדומים האלה זמינים לסמלים.

פתרונות עקיפים בדפדפן 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 מצמצמת התנגשויות בין אנימציות.

באג מספר 325900 ב-WebKit עלול למנוע את ההפעלה של שינוי ישיר של --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;
      }
    }
  }
}

האיפוס מונע את הפצת ערך הסמל לצאצאים. אם שומרים אותו בשכבה, הצהרות על סמלים שלא נמצאים בשכבה מקבלות עדיפות על פני האיפוס.