CSS Techniques for Icon Styling
Published on
CSS gives you enough control to style icons without reopening the source file every time the interface changes. That matters when icons need to follow theme colours, button states, responsive sizing or a bit of motion, while still remaining reusable.
Styling inline SVG icons
Inline SVG is the flexible option. Every element inside it can be targeted with ordinary CSS selectors, so colour, stroke and size stay in the stylesheet instead of being baked into the asset.
Basic colour styling
/* Set fill colour */
.icon path {
fill: #333;
}
/* Set stroke colour */
.icon path {
stroke: #333;
stroke-width: 2;
fill: none;
}
/* Using currentColor */
.icon {
color: #333;
}
.icon path {
fill: currentColor;
}currentColor is usually the cleanest choice because the icon follows the parent text colour automatically. Less duplication. Fewer awkward overrides later.
CSS custom properties for theming
CSS variables keep icon palettes consistent across themes. Set the values once, then let the theme switch do the rest.
:root {
--icon-primary: #333;
--icon-secondary: #666;
--icon-accent: #0066cc;
}
[data-theme="dark"] {
--icon-primary: #fff;
--icon-secondary: #aaa;
--icon-accent: #66b3ff;
}
.icon-primary {
fill: var(--icon-primary);
}
.icon-secondary {
fill: var(--icon-secondary);
}Change the theme and the icons update with it. No JavaScript. No manual colour swaps.
Sizing with CSS
Icon size is easy to control, but the unit you pick changes how the asset behaves in context.
/* Fixed size */
.icon {
width: 24px;
height: 24px;
}
/* Relative to font size */
.icon {
width: 1em;
height: 1em;
}
/* Responsive sizing */
.icon {
width: clamp(16px, 4vw, 32px);
height: clamp(16px, 4vw, 32px);
}em works well when the icon should sit with text rather than fight it. Fixed sizes are cleaner for toolbars and controls that need exact spacing.
Interactive states
Icons used in buttons or controls usually need hover, active, focus and disabled states. That is not decoration. It is basic interface behaviour.
.icon-button {
color: var(--icon-default);
cursor: pointer;
}
.icon-button:hover {
color: var(--icon-hover);
}
.icon-button:active {
color: var(--icon-active);
transform: scale(0.95);
}
.icon-button:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
.icon-button:disabled {
color: var(--icon-disabled);
cursor: not-allowed;
}CSS transitions
Transitions handle the small stuff well - hover colour shifts, gentle scale changes, that sort of thing.
.icon {
transition: color 0.2s ease, transform 0.2s ease;
}
.icon:hover {
color: var(--accent);
transform: scale(1.1);
}
/* Transition individual SVG elements */
.icon path {
transition: fill 0.2s ease, stroke 0.2s ease;
}CSS animations
For motion that repeats or needs a clear sequence, use keyframes instead of stacking more transitions on top.
Rotation animation
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.icon-loading {
animation: spin 1s linear infinite;
}Pulse animation
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.icon-notification {
animation: pulse 2s ease-in-out infinite;
}Bounce animation
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.icon-arrow:hover {
animation: bounce 0.6s ease infinite;
}Animating SVG elements
When the icon is made from separate paths or lines, those pieces can move independently. That is how you get effects like a checkmark drawing in or a menu icon turning into a close icon.
/* Checkmark draw-in effect */
.icon-check path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
transition: stroke-dashoffset 0.3s ease;
}
.icon-check.visible path {
stroke-dashoffset: 0;
}
/* Morphing between states */
.icon-menu line {
transition: transform 0.3s ease;
transform-origin: center;
}
.icon-menu.open line:nth-child(1) {
transform: rotate(45deg) translate(0, 6px);
}
.icon-menu.open line:nth-child(2) {
opacity: 0;
}
.icon-menu.open line:nth-child(3) {
transform: rotate(-45deg) translate(0, -6px);
}CSS filters
Filters are useful when you need a visual effect without editing the SVG itself, but they are not free. A room full of filtered icons will show it.
/* Drop shadow */
.icon {
filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.2));
}
/* Colour inversion for dark mode */
[data-theme="dark"] .icon {
filter: invert(1);
}
/* Brightness adjustment */
.icon:hover {
filter: brightness(1.2);
}
/* Grayscale for disabled */
.icon:disabled {
filter: grayscale(1) opacity(0.5);
}Alignment and positioning
The icon can be styled perfectly and still look wrong if the alignment is sloppy. Text, buttons and layout all need to agree.
/* Inline with text */
.icon-inline {
vertical-align: middle;
margin-right: 0.5em;
}
/* Flexbox alignment */
.button-with-icon {
display: inline-flex;
align-items: center;
gap: 0.5em;
}
/* Grid alignment */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 48px);
gap: 1rem;
}Responsive icon behaviour
Small screens do not always need the same treatment as larger ones. Sometimes that means a slight size change. Sometimes it means hiding the label until there is room for it.
/* Size adjustments */
.icon {
width: 24px;
}
@media (min-width: 768px) {
.icon {
width: 28px;
}
}
/* Show/hide labels */
.icon-label {
display: none;
}
@media (min-width: 768px) {
.icon-label {
display: inline;
}
}Handling external SVGs
SVGs loaded through <img> are the awkward case. CSS control is limited, so the usual alternatives are inline SVG, <object>, or a CSS mask when the icon is monochrome.
- Use inline SVG - Full CSS control
- Use <object> - Can style with external CSS
- CSS mask-image - Colour monochrome icons
/* CSS mask approach */
.icon-mask {
background-color: currentColor;
-webkit-mask-image: url('icon.svg');
mask-image: url('icon.svg');
-webkit-mask-size: contain;
mask-size: contain;
}Performance considerations
Keep the heavier effects under control. Complex filters on a large set of icons will cost more than most teams expect, and the browser still has to repaint them.
- Avoid complex filters on many icons at once
- Use
transformandopacityfor animations (GPU-accelerated) - Limit animated icons on screen at once
- Use
will-changesparingly for animation hints