Image Dithering
Installation
npm i @paper-design/shaders-react
Code
import { ImageDithering } from '@paper-design/shaders-react'; <ImageDithering> <div className="demo"> <style>{`.demo { --bg: #f0efe4; --text: #222; --panel: #e2dfcf; --muted: #666; --shade: rgb(0 0 0 / 20%); --yellow: #ffd23f; --pink: #ff8fab; --blue: #7aa7ff; --green: #5fd49a; --like: #ff5e5e; --skeleton: #808080; display: flex; flex-direction: column; height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font: 500 28px/1.2 Matter, system-ui; font-feature-settings: "ss01"; user-select: none } .demo button { padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer } .demo header { position: relative; display: flex; align-items: center; gap: 28px; padding: 20px 32px } .demo header::after { content: ""; position: absolute; top: 100%; left: 0; right: 0; z-index: 1; height: 32px; background: linear-gradient(var(--shade), transparent); pointer-events: none } .demo .burger { display: grid; gap: 9px; width: 40px; margin-right: auto } .demo .burger span { height: 5px; border-radius: 3px; background: var(--text); transition: translate 300ms, rotate 300ms, opacity 300ms } .demo.menu-open .burger span:nth-child(1) { translate: 0 14px; rotate: 45deg } .demo.menu-open .burger span:nth-child(2) { opacity: 0 } .demo.menu-open .burger span:nth-child(3) { translate: 0 -14px; rotate: -45deg } .demo header img { width: 56px; height: 56px; border: 3px solid var(--pink); border-radius: 50%; object-fit: cover } .demo .body { flex: 1; display: flex; min-height: 0 } .demo nav { position: relative; z-index: 2; display: grid; align-content: start; gap: 12px; width: 0; padding: 32px 0; overflow: hidden; background: var(--panel); transition: width 400ms } .demo.menu-open nav { width: 280px } .demo nav span { display: flex; align-items: center; gap: 16px; height: 48px; padding: 0 32px } .demo nav span::before { content: ""; flex-shrink: 0; width: 28px; height: 28px; border-radius: 8px; background: var(--skeleton) } .demo nav span::after { content: ""; flex-shrink: 0; width: 120px; height: 18px; border-radius: 9px; background: var(--skeleton) } .demo nav span:nth-child(2)::after { width: 90px } .demo nav span:nth-child(3)::after { width: 140px } .demo nav span:nth-child(4)::after { width: 70px } .demo main { flex: 1; display: grid; grid-template-columns: 1fr minmax(max-content, 1fr); align-items: center; gap: 48px; min-width: 0; padding: 32px 48px } .demo .details, .demo .likes { display: grid; justify-items: start } .demo :is(.detail, .like) { --shown: 0; --delay: 0ms; --step: 0ms; --duration: 150ms; grid-area: 1 / 1; z-index: var(--shown); pointer-events: none } .demo .detail > *, .demo .like { opacity: var(--shown); translate: 0 calc((1 - var(--shown)) * 16px); transition: opacity var(--duration) calc(var(--delay) + var(--i, 0) * var(--step)), translate var(--duration) calc(var(--delay) + var(--i, 0) * var(--step)) cubic-bezier(0.2, 0.8, 0.2, 1) } .demo .detail > :nth-child(2) { --i: 1 } .demo .like { --i: 2 } .demo:has(label:nth-of-type(1) :checked) :is(.detail, .like):nth-child(1), .demo:has(label:nth-of-type(2) :checked) :is(.detail, .like):nth-child(2), .demo:has(label:nth-of-type(3) :checked) :is(.detail, .like):nth-child(3), .demo:has(label:nth-of-type(4) :checked) :is(.detail, .like):nth-child(4) { --mark: 100%; --shown: 1; --delay: 150ms; --step: 90ms; --duration: 600ms; pointer-events: auto } .demo :is(.detail, .like, label):nth-of-type(1) { --tile: var(--yellow) } .demo :is(.detail, .like, label):nth-of-type(2) { --tile: var(--blue) } .demo :is(.detail, .like, label):nth-of-type(3) { --tile: var(--pink) } .demo :is(.detail, .like, label):nth-of-type(4) { --tile: var(--green) } .demo h1 { margin: 0 0 16px; font-size: 72px; line-height: 1 } .demo h1 span { background: linear-gradient(transparent 55%, var(--tile) 55% 90%, transparent 90%) no-repeat 0 0 / var(--mark, 0%) 100%; box-decoration-break: clone; -webkit-box-decoration-break: clone; transition: background-size 600ms 350ms cubic-bezier(0.2, 0.8, 0.2, 1) } .demo p { margin: 0 0 32px; color: var(--muted); font-size: 36px } .demo .like { display: flex; align-items: center; gap: 16px; font-size: 36px } .demo .like .heart { display: grid; width: 44px; height: 44px } .demo .like svg { width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linejoin: round; transition: fill 150ms, stroke 150ms, scale 200ms } .demo .like:hover svg { scale: 1.1 } .demo .like.liked svg { fill: var(--like); stroke: var(--like); animation: like-pop 300ms ease-out } .demo .like .count { min-width: 3ch; font-family: 'Paper Mono', ui-monospace, monospace } .demo .playlists { display: grid; gap: 14px } .demo label { padding: 18px 28px; border-radius: 20px; background: var(--tile); color: #1a1a1a; font-size: 32px; white-space: nowrap; cursor: pointer; transition: box-shadow 200ms } .demo label:has(:checked) { box-shadow: 0 0 0 4px var(--bg), 0 0 0 8px var(--text) } .demo input { display: none } @keyframes like-pop { 50% { scale: 1.15 } }`}</style> <header> <button className="burger" aria-label="Menu" onClick={(event) => { event.currentTarget.closest('.demo').classList.toggle('menu-open'); }} > <span /> <span /> <span /> </button> <img src="/images/image-filters/001.webp" alt="" /> </header> <div className="body"> <nav> <span /> <span /> <span /> <span /> </nav> <main> <div className="info"> <div className="details"> <div className="detail"> <h1> <span>Morning coffee</span> </h1> <p>Slow jazz and soft piano to ease into the day</p> </div> <div className="detail"> <h1> <span>Deep focus</span> </h1> <p>Wordless ambient loops that keep you in the zone</p> </div> <div className="detail"> <h1> <span>Night drive</span> </h1> <p>Synthwave for empty roads and city lights</p> </div> <div className="detail"> <h1> <span>Garden party</span> </h1> <p>Sunny bossa nova and disco for long afternoons</p> </div> </div> <div className="likes"> <button className="like" onClick={(event) => { const isLiked = event.currentTarget.classList.toggle('liked'); const count = event.currentTarget.querySelector('.count'); count.textContent = String(Number(count.textContent) + (isLiked ? 1 : -1)); }} > <span className="heart"> <svg viewBox="0 0 24 24"> <path d="M12 21s-8-5.3-8-11a4.5 4.5 0 0 1 8-2.8A4.5 4.5 0 0 1 20 10c0 5.7-8 11-8 11z" /> </svg> </span> <span className="count"> 128 </span> </button> <button className="like" onClick={(event) => { const isLiked = event.currentTarget.classList.toggle('liked'); const count = event.currentTarget.querySelector('.count'); count.textContent = String(Number(count.textContent) + (isLiked ? 1 : -1)); }} > <span className="heart"> <svg viewBox="0 0 24 24"> <path d="M12 21s-8-5.3-8-11a4.5 4.5 0 0 1 8-2.8A4.5 4.5 0 0 1 20 10c0 5.7-8 11-8 11z" /> </svg> </span> <span className="count"> 342 </span> </button> <button className="like" onClick={(event) => { const isLiked = event.currentTarget.classList.toggle('liked'); const count = event.currentTarget.querySelector('.count'); count.textContent = String(Number(count.textContent) + (isLiked ? 1 : -1)); }} > <span className="heart"> <svg viewBox="0 0 24 24"> <path d="M12 21s-8-5.3-8-11a4.5 4.5 0 0 1 8-2.8A4.5 4.5 0 0 1 20 10c0 5.7-8 11-8 11z" /> </svg> </span> <span className="count"> 76 </span> </button> <button className="like" onClick={(event) => { const isLiked = event.currentTarget.classList.toggle('liked'); const count = event.currentTarget.querySelector('.count'); count.textContent = String(Number(count.textContent) + (isLiked ? 1 : -1)); }} > <span className="heart"> <svg viewBox="0 0 24 24"> <path d="M12 21s-8-5.3-8-11a4.5 4.5 0 0 1 8-2.8A4.5 4.5 0 0 1 20 10c0 5.7-8 11-8 11z" /> </svg> </span> <span className="count"> 51 </span> </button> </div> </div> <div className="playlists"> <label> <input type="radio" name="playlist" defaultChecked /> Morning coffee </label> <label> <input type="radio" name="playlist" /> Deep focus </label> <label> <input type="radio" name="playlist" /> Night drive </label> <label> <input type="radio" name="playlist" /> Garden party </label> </div> </main> </div> </div> </ImageDithering>
Shader Props
Common Props
Description
A dithering image filter with support for 4 dithering modes and multiple color palettes (2-color, 3-color, and multicolor options, using either predefined colors or colors sampled from the original image).