Icon usage patterns using Material Symbols v3. Use when adding icons to buttons, navigation, or status indicators. Covers sizing, accessibility, animations, and color integration with project tokens.
Icon implementation patterns for UI components. This project uses Material Symbols v3 as the primary icon system.
material-symbols-v3: Complete icon API reference, variable font axes, icon namesux-accessibility: ARIA requirements for icon buttonsux-animation-motion: Anime.js patterns for icon animationsLocation: css/styles/icons.css
This project uses Material Symbols Outlined (variable font) from Google Fonts:
<span class="icon">home</span>
<span class="icon">settings</span>
<span class="icon">favorite</span>
See material-symbols-v3 skill for complete icon name reference.
<span class="icon icon--sm">info</span> <!-- 20px -->
<span class="icon icon--md">info</span> <!-- 24px (default) -->
<span class="icon icon--lg">info</span> <!-- 40px -->
<span class="icon icon--xl">info</span> <!-- 48px -->
Scale icons with typography tokens:
.icon-fluid {
font-size: var(--step-1);
}
.icon-small {
font-size: var(--step--1);
}
.icon-fixed {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
<button class="btn-icon-text">
<span class="icon" aria-hidden="true">menu_book</span>
<span class="label">Word Phase</span>
</button>
.btn-icon-text {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
}
.btn-icon-text .icon {
flex-shrink: 0;
}
<button class="btn-icon" aria-label="Settings">
<span class="icon" aria-hidden="true">settings</span>
</button>
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: var(--min-touch-target);
min-height: var(--min-touch-target);
padding: var(--space-xs);
}
<a href="/next" class="link-arrow">
Next Phase
<span class="icon" aria-hidden="true">arrow_forward</span>
</a>
<!-- Decorative: has visible label -->
<button>
<span class="icon" aria-hidden="true">settings</span>
Settings
</button>
<!-- Meaningful: icon-only needs label -->
<button aria-label="Settings">
<span class="icon" aria-hidden="true">settings</span>
</button>
<button class="btn-icon">
<span class="icon" aria-hidden="true">settings</span>
<span class="sr-only">Settings</span>
</button>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
clip: rect(0, 0, 0, 0);
}
<span class="status status-complete">
<span class="icon" aria-hidden="true">check_circle</span>
<span>Complete</span>
</span>
<span class="icon icon--outlined">favorite</span> <!-- Outline (default) -->
<span class="icon icon--filled">favorite</span> <!-- Solid fill -->
<span class="icon icon--thin">home</span> <!-- 100 -->
<span class="icon icon--light">home</span> <!-- 300 -->
<span class="icon icon--regular">home</span> <!-- 400 -->
<span class="icon icon--medium">home</span> <!-- 500 -->
<span class="icon icon--bold">home</span> <!-- 700 -->
.icon {
color: inherit;
}
.btn-primary .icon {
color: var(--theme-on-primary);
}
.icon-success { color: var(--color-success); }
.icon-error { color: var(--color-error); }
.icon-warning { color: var(--color-warning); }
[data-phase="word"] .icon { color: var(--color-word); }
[data-phase="collision"] .icon { color: var(--color-collision); }
[data-phase="mutation"] .icon { color: var(--color-mutation); }
[data-phase="story"] .icon { color: var(--color-story); }
.icon {
transition: transform 0.15s ease;
}
.btn:hover .icon {
transform: scale(1.1);
}
.btn:active .icon {
transform: scale(0.95);
}
import { animate } from 'animejs';
import { DURATION, EASE } from '../../utils/animations.js';
animate(this._iconEl, {
rotate: [0, -10, 10, -10, 10, 0],
duration: DURATION.normal,
ease: EASE.smooth
});
import { successBounce } from '../../utils/animations.js';
successBounce(this._iconEl);
// Scale: 1 → 1.2 → 1
.phase-icon {
font-size: var(--step-0);
}
[data-status="complete"] .phase-icon {
color: var(--color-success);
}
[data-status="current"] .phase-icon {
color: var(--theme-primary);
animation: pulse 1.5s ease-in-out infinite;
}
[data-status="locked"] .phase-icon {
opacity: 0.5;
filter: grayscale(0.5);
}
.progress-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--theme-outline-variant);
}
.progress-dot[data-completed] {
background: var(--color-success);
}
.progress-dot[data-current] {
background: var(--theme-primary);
}
<span class="spinner" role="status">
<span class="sr-only">Loading...</span>
</span>
.spinner {
display: inline-block;
width: 1em;
height: 1em;
border: 2px solid var(--theme-outline);
border-top-color: var(--theme-primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { rotate: 360deg; }
}
| Purpose | Icon Name | Usage |
|---|---|---|
| Word phase | menu_book |
Phase indicator |
| Collision phase | swords |
Phase indicator |
| Mutation phase | genetics |
Phase indicator |
| Story phase | auto_stories |
Phase indicator |
| Complete | check_circle |
Status indicator |
| Locked | lock |
Unavailable item |
| Star/achievement | star |
Rewards |
| Settings | settings |
Configuration |
| Home | home |
Navigation |
| Sound on | volume_up |
Audio toggle |
| Sound off | volume_off |
Audio toggle |
@media (forced-colors: active) {
.icon {
forced-color-adjust: auto;
}
}
aria-hidden="true" on decorative iconsaria-label for icon-only buttons.icon--filled for selected/active states