skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
arnavpuri/designskills101 installs

image-treatment

Treat existing images in code: CSS filters, blend modes, duotone, masks and clip-paths, overlays for text legibility, collages, SVG filters, and responsive delivery (picture/srcset/sizes, AVIF/WebP). Use when styling or serving photos on a page; to CREATE new images use image-generation. Trigger phrases: "image effect", "photo filter", "image overlay", "duotone", "image mask", "parallax background", "clip-path", "blend mode", "image grid", "responsive image", "css filter"

How do I install this agent skill?

npx skills add https://github.com/arnavpuri/designskills --skill image-treatment
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    The skill provides instructional content and code snippets for CSS and SVG image styling, posing no security risks.

  • Socketpass

    No alerts

  • Snykpass

    Risk: LOW · No issues

What does this agent skill do?

Image Treatment

Apply stunning visual effects to images using CSS filters, blend modes, masks, and SVG filters.

Prerequisites

  • Read .agents/design-context.md (see design-context) for brand colors (duotone and overlay colors should come from the Color System), style archetype, and imagery direction. Colors below use the shared --brand-* / --gray-* scale.
  • Identify the mood and purpose: editorial, product, marketing, artistic.
  • If source images don't exist yet, create them with the image-generation skill (python <image-generation>/scripts/gemini-generate.py), then treat them here. For treatments that must be baked into the file (social images, email, OG images), apply them with Pillow instead of CSS.

Step 1: CSS Filter Combinations

Individual Filters

FilterRangeEffect
brightness()0-2+Darken (< 1) or lighten (> 1)
contrast()0-2+Reduce (< 1) or boost (> 1)
saturate()0-3+Desaturate (0) or vivid (> 1)
grayscale()0-1Remove color
sepia()0-1Warm vintage tone
hue-rotate()0-360degShift all hues
blur()0-20px+Gaussian blur

Preset Filter Recipes

.filter-vintage  { filter: sepia(0.3) contrast(1.1) brightness(1.05) saturate(0.9); }
.filter-cool     { filter: saturate(0.85) contrast(1.1) brightness(1.02) hue-rotate(-10deg); }
.filter-bw       { filter: grayscale(1) contrast(1.4) brightness(1.05); }
.filter-dreamy   { filter: brightness(1.1) contrast(0.9) saturate(1.2) blur(0.5px); }
.filter-faded    { filter: contrast(0.85) brightness(1.1) saturate(0.7); }
.filter-moody    { filter: brightness(0.85) contrast(1.2) saturate(0.8); }
.filter-vivid    { filter: contrast(1.15) saturate(1.4) brightness(1.02); }

Hover Effect with Filters

.image-card img { filter: grayscale(0.8) brightness(0.9); transition: filter 0.4s ease; }
.image-card:hover img { filter: grayscale(0) brightness(1); }

Step 2: Blend Mode Techniques

Common Blend Modes

ModeEffectUse case
multiplyDarkens (white disappears)Color tints, knocking out white backgrounds
screenLightens (black disappears)Glow effects
overlayBoosts contrastColor overlays
soft-lightSubtle overlayGentle tinting
colorApplies hue/sat, preserves luminosityTinting photos
luminosityTakes brightness from the layer, hue/saturation from what's belowRecoloring textures

Color Overlay on Image

.image-container { position: relative; }
.image-container::after {
  content: ''; position: absolute; inset: 0;
  background: var(--brand-500); mix-blend-mode: multiply; opacity: 0.6;
}

Step 3: Duotone Image Effect

Maps the image's lights to one color and darks to another.

CSS-Only Duotone

Grayscale image multiplied onto the highlight color (white → highlight, black → black), then the shadow color lightens the darks (black → shadow). The highlight must be lighter than the shadow in every RGB channel.

.duotone { position: relative; isolation: isolate; background: #e2b94c; /* highlight */ }
.duotone img { display: block; width: 100%; filter: grayscale(1) contrast(1.2); mix-blend-mode: multiply; }
.duotone::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: #1a1a2e; /* shadow */ mix-blend-mode: lighten;
}

SVG Filter Duotone (exact mapping, works on any element)

tableValues are each channel of shadow → highlight in 0-1 (hex ÷ 255): #1a1a2e → #e2b94c.

<svg width="0" height="0" aria-hidden="true" style="position:absolute">
  <filter id="duotone-gold" color-interpolation-filters="sRGB">
    <feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0  0.2126 0.7152 0.0722 0 0  0.2126 0.7152 0.0722 0 0  0 0 0 1 0"/>
    <feComponentTransfer>
      <feFuncR type="table" tableValues="0.102 0.886"/>
      <feFuncG type="table" tableValues="0.102 0.725"/>
      <feFuncB type="table" tableValues="0.180 0.298"/>
    </feComponentTransfer>
  </filter>
</svg>
<img src="photo.jpg" alt="…" style="filter: url(#duotone-gold)">

Popular Duotone Pairs

Shadow colorHighlight colorFeel
#1a1a2e#e2b94cLuxury, gold+navy
#0d1b2a#3498dbTech, cool blue
#2d1b4e#ff6b6bCreative, bold
#1b3a2a#a7f3d0Natural, green

Step 4: Image Masking

clip-path Shapes

.clip-circle   { clip-path: circle(50%); }
.clip-rounded  { clip-path: inset(0 round 1rem); }
.clip-diagonal { clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%); }
.clip-hexagon  { clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); }

mask-image for Gradient Fades

.image-fade-bottom {
  mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
}
.image-vignette {
  mask-image: radial-gradient(ellipse 70% 70% at center, black 50%, transparent 100%);
  -webkit-mask-image: radial-gradient(ellipse 70% 70% at center, black 50%, transparent 100%);
}

Text-Shaped Mask

.image-text-mask {
  background-image: url('photo.jpg'); background-size: cover;
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text; font-size: 8rem; font-weight: 900;
  color: transparent; /* fallback if background-clip: text is unsupported */
}

Only for display-size decorative text; the photo under the letters must still give 3:1 contrast with the page background, and the element keeps real text for screen readers.


Step 5: Background Image Techniques

Full Cover Background

.hero-bg {
  background: url('hero.jpg') center/cover no-repeat;
  min-height: 80vh;
}

Fixed / Parallax Background

.parallax-section {
  background: url('bg.jpg') center/cover no-repeat;
  background-attachment: fixed;
  min-height: 60vh;
}
@media (hover: none), (prefers-reduced-motion: reduce) {
  .parallax-section { background-attachment: scroll; } /* iOS ignores fixed; parallax can trigger vestibular issues */
}

Multiple Background Layers

.layered-bg {
  background:
    linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0.7)),
    url('pattern.svg') repeat center / 64px 64px,
    url('photo.jpg') no-repeat center / cover;
}

Step 6: Image Overlay Patterns

Gradient Overlay (for text readability)

.image-overlay-gradient { position: relative; }
.image-overlay-gradient::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 40%, transparent 100%);
}
.image-overlay-gradient .content { position: relative; z-index: 1; color: white; }

Text over images must still meet WCAG contrast (4.5:1, or 3:1 for ≥24px / ≥18.66px bold) against the lightest pixels behind it. Check the region under the text in the rendered screenshot, not the average of the image; strengthen the overlay until it passes.

Hover Reveal Overlay

.card-image { position: relative; overflow: hidden; }
.card-image .overlay {
  position: absolute; inset: 0;
  background: oklch(from var(--brand-500) l c h / 0.85);
  display: grid; place-items: center; opacity: 0; transition: opacity 0.3s ease;
}
.card-image:hover .overlay { opacity: 1; }

Step 7: Image Grid and Collage Layouts

Masonry-Style Grid

.image-grid-masonry { columns: 3; column-gap: 1rem; }
.image-grid-masonry img { display: block; width: 100%; margin-bottom: 1rem; border-radius: 0.5rem; break-inside: avoid; }
/* Note: columns flow top-to-bottom, so DOM/reading order runs down each column, not across rows */
@media (max-width: 768px) { .image-grid-masonry { columns: 2; } }
@media (max-width: 480px) { .image-grid-masonry { columns: 1; } }

CSS Grid Collage

.image-collage {
  display: grid; grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(3, 200px); gap: 0.5rem;
}
.image-collage img:nth-child(1) { grid-column: 1 / 3; grid-row: 1 / 3; }
.image-collage img { width: 100%; height: 100%; object-fit: cover; border-radius: 0.5rem; }

Step 8: Responsive Image Art Direction

Resolution Switching (same crop, many sizes)

w descriptors list each file's real pixel width; sizes tells the browser how wide the image will be displayed so it can pick before layout. Without sizes, the browser assumes 100vw.

<img src="card-800.jpg"
     srcset="card-400.jpg 400w, card-800.jpg 800w, card-1200.jpg 1200w, card-1600.jpg 1600w"
     sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px"
     alt="Team reviewing designs at a whiteboard" width="800" height="600"
     loading="lazy" decoding="async">

Art Direction + Modern Formats (<picture>)

Browsers take the first matching <source>, so order by media query, then by format (AVIF before WebP). Each source gets its own srcset/sizes; width/height on <source> handle a different aspect ratio per breakpoint.

<picture>
  <source media="(max-width: 640px)" type="image/avif"
          srcset="hero-mobile-640.avif 640w, hero-mobile-1280.avif 1280w" sizes="100vw" width="1280" height="1600">
  <source media="(max-width: 640px)" type="image/webp"
          srcset="hero-mobile-640.webp 640w, hero-mobile-1280.webp 1280w" sizes="100vw" width="1280" height="1600">
  <source type="image/avif" srcset="hero-1600.avif 1600w, hero-2400.avif 2400w" sizes="100vw">
  <source type="image/webp" srcset="hero-1600.webp 1600w, hero-2400.webp 2400w" sizes="100vw">
  <img src="hero-1600.jpg" alt="Describe what the hero shows" width="1600" height="900"
       fetchpriority="high" decoding="async">
</picture>

Key Rules

  1. Always include width and height attributes (the intrinsic aspect ratio) to prevent layout shift; CSS can still make it fluid with width: 100%; height: auto.
  2. Use loading="lazy" for below-fold images. Never lazy-load the LCP/hero image -- leave it eager (the default) and add fetchpriority="high".
  3. Use decoding="async" so decode doesn't block rendering.
  4. Formats: AVIF (smallest, supported in all current major browsers) → WebP → JPEG fallback in <img src>. Use PNG/SVG only for graphics needing transparency or sharp edges; quality ~50-65 for AVIF and ~75-80 for WebP is a good start.
  5. alt describes the image's purpose; decorative images use alt="".

Generate the Variants

from PIL import Image  # pip install Pillow  (AVIF built in since Pillow 11.2; check features.check("avif"))
src = Image.open("hero.png").convert("RGB")
for w in (640, 1280, 1600, 2400):
    im = src.resize((w, round(src.height * w / src.width)), Image.LANCZOS)
    im.save(f"hero-{w}.webp", quality=78)
    im.save(f"hero-{w}.avif", quality=55)
    im.save(f"hero-{w}.jpg", quality=82, optimize=True, progressive=True)

Step 9: SVG Filters for Advanced Effects

Frosted Glass

.frosted-glass { backdrop-filter: blur(12px) saturate(180%); background: rgba(255,255,255,0.7); }
@supports not (backdrop-filter: blur(1px)) { .frosted-glass { background: rgba(255,255,255,0.92); } } /* keep text readable */

Noise/Grain Texture

<svg width="0" height="0" aria-hidden="true" style="position:absolute">
  <filter id="noise">
    <feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>
    <feColorMatrix type="saturate" values="0"/>
    <feBlend in="SourceGraphic" mode="multiply"/>
  </filter>
</svg>
<!-- apply with: .grain { filter: url(#noise); } -->

Drop Shadow (Following Shape)

.shaped-shadow { filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2)); }

Step 10: Object-Fit and Object-Position

ValueBehavior
coverFills container, crops excess (most common)
containFits entirely, may have empty space
fillStretches to fill (distorts)
scale-downLike contain but never scales up
.card-image { width: 100%; height: 200px; object-fit: cover; object-position: center; }
.portrait   { object-fit: cover; object-position: center top; }
.thumbnail  { aspect-ratio: 1; object-fit: cover; border-radius: 0.75rem; }

Quick Reference

  1. Use CSS filter for quick photo effects (combine brightness, contrast, saturate).
  2. Use mix-blend-mode: multiply for color overlays on images.
  3. Duotone = grayscale image + two-color blend layers.
  4. clip-path for geometric masks; mask-image for gradient fades.
  5. Always disable background-attachment: fixed on mobile (@media (hover: none)).
  6. Layer overlays: gradient bottom-to-top for text readability on images.
  7. Use <picture> for art direction (different crops per breakpoint).
  8. Always include width, height, alt, and srcset + sizes on content images; lazy-load only below the fold.
  9. object-fit: cover + aspect-ratio for consistent image containers.
  10. SVG filters (noise, goo, displacement) for effects CSS cannot achieve.

Verify the Treatment

Render the page and screenshot it (npx playwright screenshot --full-page "file://$PWD/index.html" out.png, plus --viewport-size=375,800 for mobile and --color-scheme=dark), then view the images with Read. Check: faces and products aren't cropped by object-fit/clip-path at any width, text over images passes contrast in its worst spot, duotone/filters still look on-brand (not muddy), and nothing looks broken where a blend mode or filter is unsupported. In the browser, confirm the right srcset candidate loads (img.currentSrc) and that the hero is not lazy-loaded.

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/arnavpuri/designskills/image-treatment">View image-treatment on skillZs</a>