Skip to main content

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 transform and opacity for animations (GPU-accelerated)
  • Limit animated icons on screen at once
  • Use will-change sparingly for animation hints

Frequently Asked Questions