AppleApricotBananaBlueberryCherryDateFigGrape
Shredder
React + Tailwind
import { useId, useState, type CSSProperties } from 'react';
const MAX = 14;
const STRIPS = [-3.5, 0, 3.5];
const css = `
.field-shredder { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); --body: light-dark(#42454c, #232529); --lit: light-dark(#fbfcfd, #f1f2f3); position: relative; width: 262px; height: 216px; }
.field-shredder__label { position: absolute; left: 18px; top: 0; color: light-dark(#5f626a, #a7abb2); font: 500 11px/14px system-ui, sans-serif; }
.field-shredder__slip { position: absolute; left: 18px; top: 18px; width: 194px; height: 32px; border-radius: 3px; background: var(--paper); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--ink) 14%, var(--paper)), 0 2px 4px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25)); cursor: text; }
.field-shredder__slip:has(.field-shredder__input:focus-visible) { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.field-shredder__empty { position: absolute; left: 14px; top: 0; color: color-mix(in oklab, var(--ink) 55%, var(--paper)); font: 500 13px/32px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; pointer-events: none; }
.field-shredder__input { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: transparent; caret-color: transparent; font-size: 16px; outline: none; cursor: text; }
.field-shredder__input::selection { background: transparent; }
.field-shredder__eye { position: absolute; right: 18px; top: 20px; z-index: 3; display: grid; place-items: center; width: 30px; height: 28px; border: 0; border-radius: 7px; background: light-dark(#ebedf0, #232529); color: light-dark(#50535b, #c6c8cd); cursor: pointer; transition: background-color 0.2s, color 0.2s; }
.field-shredder__eye:hover { color: light-dark(#373a40, #f1f2f3); }
.field-shredder__eye:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.field-shredder__eye svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.field-shredder__eye .field-shredder__slash { stroke-dasharray: 1; stroke-dashoffset: 0; transition: stroke-dashoffset 0.25s; }
.field-shredder[data-shown] .field-shredder__slash { stroke-dashoffset: 1; }
/* One tile per character: fed down into the slot, or pulled back up onto the slip. */
.field-shredder__tile { position: absolute; top: 24px; z-index: 1; width: 13px; height: 22px; color: var(--ink); font: 600 15px/22px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; text-align: center; pointer-events: none; animation: field-shredder-feed 0.8s cubic-bezier(0.5, 0, 0.6, 1) forwards; }
@keyframes field-shredder-feed {
0%, 42% { opacity: 1; transform: none; }
72% { opacity: 1; transform: translateY(40px) scaleY(0.5); }
100% { opacity: 0; transform: translateY(48px) scaleY(0.2); }
}
.field-shredder[data-shown] .field-shredder__tile { animation: field-shredder-pop 0.25s ease-out both; }
.field-shredder[data-shown] .field-shredder__tile--rise { animation: field-shredder-unshred 0.5s cubic-bezier(0.2, 0.8, 0.3, 1) both; }
@keyframes field-shredder-unshred { from { opacity: 0; transform: translateY(40px) scaleY(0.4); } }
@keyframes field-shredder-pop { from { opacity: 0; transform: translateY(-4px); } }
/* What stays on the slip: a dot per character once its letter has gone through, and the caret. */
.field-shredder__dot { position: absolute; top: 13px; width: 6px; height: 6px; margin-left: 3.5px; border-radius: 50%; background: var(--ink); pointer-events: none; animation: field-shredder-dot 0.25s cubic-bezier(0.3, 1.6, 0.5, 1) 0.42s both; }
@keyframes field-shredder-dot { from { opacity: 0; transform: scale(0); } }
.field-shredder[data-shown] .field-shredder__dot { display: none; }
.field-shredder__caret { position: absolute; top: 7px; width: 2px; height: 18px; margin-left: 1px; border-radius: 1px; background: var(--ink); opacity: 0; pointer-events: none; transition: left 0.15s ease-out; }
.field-shredder:has(.field-shredder__input:focus) .field-shredder__caret { animation: field-shredder-blink 1s steps(1) infinite; }
@keyframes field-shredder-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
/* What comes out underneath: three strips per character that drop onto the pile. */
.field-shredder__strip { position: absolute; margin-left: var(--dx); top: 106px; z-index: 1; width: 2.2px; height: var(--h); border-radius: 1px; background: var(--paper); box-shadow: 0 0 0 0.6px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)), 0 1px 1px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); opacity: 0; animation: field-shredder-fall 0.7s cubic-bezier(0.5, 0, 0.8, 0.5) 0.6s forwards; }
@keyframes field-shredder-fall {
0% { opacity: 0; transform: translateY(-8px); }
15% { opacity: 1; }
100% { opacity: 1; transform: translate(var(--drift), var(--land)) rotate(var(--rot)); }
}
.field-shredder[data-shown] .field-shredder__strip { animation: none; opacity: 0; }
.field-shredder__machine { position: absolute; left: 14px; top: 62px; z-index: 2; width: 234px; height: 46px; border-radius: 6px 6px 3px 3px; background: linear-gradient(var(--body), color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417))); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 16%, transparent), 0 8px 12px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-shredder__machine::before { content: ''; position: absolute; left: 14px; right: 14px; top: 6px; height: 4px; border-radius: 2px; background: light-dark(#373a40, #121417); box-shadow: inset 0 1px 2px light-dark(rgba(0, 0, 0, 0.24), rgba(0, 0, 0, 0.8)); }
.field-shredder__led { position: absolute; right: 14px; bottom: 12px; width: 6px; height: 6px; border-radius: 50%; background: var(--lit); box-shadow: 0 0 6px var(--lit); }
.field-shredder__brand { position: absolute; left: 16px; bottom: 9px; color: color-mix(in oklab, var(--lit) 55%, transparent); font: 700 8px/1 system-ui, sans-serif; letter-spacing: 0.24em; }
.field-shredder__bin { position: absolute; left: 28px; top: 108px; width: 206px; height: 84px; border: 1.5px solid light-dark(#cbced3, #393c41); border-top: 0; border-radius: 0 0 10px 10px; background: repeating-linear-gradient(90deg, transparent 0 9px, color-mix(in oklab, light-dark(#cbced3, #393c41) 45%, transparent) 9px 10px); }
.field-shredder__read { position: absolute; left: 0; right: 0; bottom: 0; color: light-dark(#5f626a, #a7abb2); font: 500 11.5px system-ui, sans-serif; text-align: center; }
.field-shredder__read b { color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
@media (prefers-reduced-motion: reduce) {
.field-shredder * { animation-duration: 0.01s !important; animation-delay: 0s !important; transition: none !important; }
}
`;
export function Preview() {
const id = useId();
const [value, setValue] = useState('moonlight7');
const [shown, setShown] = useState(false);
// Showing pulls back up only what was already shredded; letters typed after that just appear.
const [risen, setRisen] = useState(0);
return (
<>
<style href="kit-inputs-shredder" precedence="kit">
{css}
</style>
<div className="field-shredder" data-shown={shown || undefined}>
<span className="field-shredder__machine" aria-hidden="true">
<span className="field-shredder__brand">SHRED</span>
<span className="field-shredder__led" />
</span>
<span className="field-shredder__bin" aria-hidden="true" />
<span className="field-shredder__label" aria-hidden="true">
Password
</span>
<label className="field-shredder__slip" htmlFor={id}>
{!value && <span className="field-shredder__empty">Type a password</span>}
{[...value].map((_, i) => (
<i key={`${i}-${shown}`} className="field-shredder__dot" style={{ left: 10 + i * 13 }} aria-hidden="true" />
))}
<i className="field-shredder__caret" style={{ left: 10 + value.length * 13 }} aria-hidden="true" />
<input
id={id}
className="field-shredder__input"
type={shown ? 'text' : 'password'}
autoComplete="new-password"
maxLength={MAX}
aria-label="Password"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</label>
{[...value].map((ch, i) => (
<span key={`${i}-${shown}`} className={`field-shredder__tile${i < risen ? ' field-shredder__tile--rise' : ''}`} style={{ left: 28 + i * 13 }} aria-hidden="true">
{ch}
</span>
))}
{/* Strips are their own elements, so they stay on the pile after the letter is gone. */}
{[...value].map((_, i) =>
STRIPS.map((dx, k) => (
<i
key={`${i}-${k}-${shown}`}
className="field-shredder__strip"
aria-hidden="true"
style={{ left: 28 + i * 13 + 6.5, '--dx': `${dx}px`, '--h': `${12 + ((i * 3 + k * 5) % 6)}px`, '--drift': `${((i + k) % 3) - 1}px`, '--land': `${64 - Math.floor(i / 3) * 3 - k}px`, '--rot': `${((i * 7 + k * 13) % 50) - 25}deg` } as CSSProperties}
/>
)),
)}
<button type="button" className="field-shredder__eye" aria-label="Show password" aria-pressed={shown} onClick={() => {
setRisen(value.length);
setShown((s) => !s);
}}>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
<circle cx={12} cy={12} r={3} />
<path className="field-shredder__slash" pathLength={1} d="M4 4L20 20" />
</svg>
</button>
<p className="field-shredder__read" aria-hidden="true">
<b>{value.length}</b> {value.length === 1 ? 'character' : 'characters'}, {shown ? 'showing' : 'shredded'}
</p>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldShredder(el) on the mounted element instead.
HTML
<div class="field-shredder">
<span class="field-shredder__machine" aria-hidden="true">
<span class="field-shredder__brand">SHRED</span>
<span class="field-shredder__led"></span>
</span>
<span class="field-shredder__bin" aria-hidden="true"></span>
<span class="field-shredder__label" aria-hidden="true">Password</span>
<label class="field-shredder__slip" for="field-shredder">
<i class="field-shredder__dot" style="left:10px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:23px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:36px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:49px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:62px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:75px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:88px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:101px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:114px" aria-hidden="true"></i>
<i class="field-shredder__dot" style="left:127px" aria-hidden="true"></i>
<i class="field-shredder__caret" style="left:140px" aria-hidden="true"></i>
<input id="field-shredder" class="field-shredder__input" type="password" autoComplete="new-password" maxLength="14" aria-label="Password" value="moonlight7"/>
</label>
<span class="field-shredder__tile" style="left:28px" aria-hidden="true">m</span>
<span class="field-shredder__tile" style="left:41px" aria-hidden="true">o</span>
<span class="field-shredder__tile" style="left:54px" aria-hidden="true">o</span>
<span class="field-shredder__tile" style="left:67px" aria-hidden="true">n</span>
<span class="field-shredder__tile" style="left:80px" aria-hidden="true">l</span>
<span class="field-shredder__tile" style="left:93px" aria-hidden="true">i</span>
<span class="field-shredder__tile" style="left:106px" aria-hidden="true">g</span>
<span class="field-shredder__tile" style="left:119px" aria-hidden="true">h</span>
<span class="field-shredder__tile" style="left:132px" aria-hidden="true">t</span>
<span class="field-shredder__tile" style="left:145px" aria-hidden="true">7</span>
<i class="field-shredder__strip" aria-hidden="true" style="left:34.5px;--dx:-3.5px;--h:12px;--drift:-1px;--land:64px;--rot:-25deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:34.5px;--dx:0px;--h:17px;--drift:0px;--land:63px;--rot:-12deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:34.5px;--dx:3.5px;--h:16px;--drift:1px;--land:62px;--rot:1deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:47.5px;--dx:-3.5px;--h:15px;--drift:0px;--land:64px;--rot:-18deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:47.5px;--dx:0px;--h:14px;--drift:1px;--land:63px;--rot:-5deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:47.5px;--dx:3.5px;--h:13px;--drift:-1px;--land:62px;--rot:8deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:60.5px;--dx:-3.5px;--h:12px;--drift:1px;--land:64px;--rot:-11deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:60.5px;--dx:0px;--h:17px;--drift:-1px;--land:63px;--rot:2deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:60.5px;--dx:3.5px;--h:16px;--drift:0px;--land:62px;--rot:15deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:73.5px;--dx:-3.5px;--h:15px;--drift:-1px;--land:61px;--rot:-4deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:73.5px;--dx:0px;--h:14px;--drift:0px;--land:60px;--rot:9deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:73.5px;--dx:3.5px;--h:13px;--drift:1px;--land:59px;--rot:22deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:86.5px;--dx:-3.5px;--h:12px;--drift:0px;--land:61px;--rot:3deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:86.5px;--dx:0px;--h:17px;--drift:1px;--land:60px;--rot:16deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:86.5px;--dx:3.5px;--h:16px;--drift:-1px;--land:59px;--rot:-21deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:99.5px;--dx:-3.5px;--h:15px;--drift:1px;--land:61px;--rot:10deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:99.5px;--dx:0px;--h:14px;--drift:-1px;--land:60px;--rot:23deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:99.5px;--dx:3.5px;--h:13px;--drift:0px;--land:59px;--rot:-14deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:112.5px;--dx:-3.5px;--h:12px;--drift:-1px;--land:58px;--rot:17deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:112.5px;--dx:0px;--h:17px;--drift:0px;--land:57px;--rot:-20deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:112.5px;--dx:3.5px;--h:16px;--drift:1px;--land:56px;--rot:-7deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:125.5px;--dx:-3.5px;--h:15px;--drift:0px;--land:58px;--rot:24deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:125.5px;--dx:0px;--h:14px;--drift:1px;--land:57px;--rot:-13deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:125.5px;--dx:3.5px;--h:13px;--drift:-1px;--land:56px;--rot:0deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:138.5px;--dx:-3.5px;--h:12px;--drift:1px;--land:58px;--rot:-19deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:138.5px;--dx:0px;--h:17px;--drift:-1px;--land:57px;--rot:-6deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:138.5px;--dx:3.5px;--h:16px;--drift:0px;--land:56px;--rot:7deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:151.5px;--dx:-3.5px;--h:15px;--drift:-1px;--land:55px;--rot:-12deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:151.5px;--dx:0px;--h:14px;--drift:0px;--land:54px;--rot:1deg"></i>
<i class="field-shredder__strip" aria-hidden="true" style="left:151.5px;--dx:3.5px;--h:13px;--drift:1px;--land:53px;--rot:14deg"></i>
<button type="button" class="field-shredder__eye" aria-label="Show password" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"></path>
<circle cx="12" cy="12" r="3"></circle>
<path class="field-shredder__slash" pathLength="1" d="M4 4L20 20"></path>
</svg>
</button>
<p class="field-shredder__read" aria-hidden="true">
<b>10</b>
characters, shredded
</p>
</div>CSS
.field-shredder { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); --body: light-dark(#42454c, #232529); --lit: light-dark(#fbfcfd, #f1f2f3); position: relative; width: 262px; height: 216px; }
.field-shredder__label { position: absolute; left: 18px; top: 0; color: light-dark(#5f626a, #a7abb2); font: 500 11px/14px system-ui, sans-serif; }
.field-shredder__slip { position: absolute; left: 18px; top: 18px; width: 194px; height: 32px; border-radius: 3px; background: var(--paper); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--ink) 14%, var(--paper)), 0 2px 4px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25)); cursor: text; }
.field-shredder__slip:has(.field-shredder__input:focus-visible) { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.field-shredder__empty { position: absolute; left: 14px; top: 0; color: color-mix(in oklab, var(--ink) 55%, var(--paper)); font: 500 13px/32px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; pointer-events: none; }
.field-shredder__input { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: transparent; caret-color: transparent; font-size: 16px; outline: none; cursor: text; }
.field-shredder__input::selection { background: transparent; }
.field-shredder__eye { position: absolute; right: 18px; top: 20px; z-index: 3; display: grid; place-items: center; width: 30px; height: 28px; border: 0; border-radius: 7px; background: light-dark(#ebedf0, #232529); color: light-dark(#50535b, #c6c8cd); cursor: pointer; transition: background-color 0.2s, color 0.2s; }
.field-shredder__eye:hover { color: light-dark(#373a40, #f1f2f3); }
.field-shredder__eye:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
.field-shredder__eye svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.field-shredder__eye .field-shredder__slash { stroke-dasharray: 1; stroke-dashoffset: 0; transition: stroke-dashoffset 0.25s; }
.field-shredder[data-shown] .field-shredder__slash { stroke-dashoffset: 1; }
/* One tile per character: fed down into the slot, or pulled back up onto the slip. */
.field-shredder__tile { position: absolute; top: 24px; z-index: 1; width: 13px; height: 22px; color: var(--ink); font: 600 15px/22px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; text-align: center; pointer-events: none; animation: field-shredder-feed 0.8s cubic-bezier(0.5, 0, 0.6, 1) forwards; }
@keyframes field-shredder-feed {
0%, 42% { opacity: 1; transform: none; }
72% { opacity: 1; transform: translateY(40px) scaleY(0.5); }
100% { opacity: 0; transform: translateY(48px) scaleY(0.2); }
}
.field-shredder[data-shown] .field-shredder__tile { animation: field-shredder-pop 0.25s ease-out both; }
.field-shredder[data-shown] .field-shredder__tile--rise { animation: field-shredder-unshred 0.5s cubic-bezier(0.2, 0.8, 0.3, 1) both; }
@keyframes field-shredder-unshred { from { opacity: 0; transform: translateY(40px) scaleY(0.4); } }
@keyframes field-shredder-pop { from { opacity: 0; transform: translateY(-4px); } }
/* What stays on the slip: a dot per character once its letter has gone through, and the caret. */
.field-shredder__dot { position: absolute; top: 13px; width: 6px; height: 6px; margin-left: 3.5px; border-radius: 50%; background: var(--ink); pointer-events: none; animation: field-shredder-dot 0.25s cubic-bezier(0.3, 1.6, 0.5, 1) 0.42s both; }
@keyframes field-shredder-dot { from { opacity: 0; transform: scale(0); } }
.field-shredder[data-shown] .field-shredder__dot { display: none; }
.field-shredder__caret { position: absolute; top: 7px; width: 2px; height: 18px; margin-left: 1px; border-radius: 1px; background: var(--ink); opacity: 0; pointer-events: none; transition: left 0.15s ease-out; }
.field-shredder:has(.field-shredder__input:focus) .field-shredder__caret { animation: field-shredder-blink 1s steps(1) infinite; }
@keyframes field-shredder-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
/* What comes out underneath: three strips per character that drop onto the pile. */
.field-shredder__strip { position: absolute; margin-left: var(--dx); top: 106px; z-index: 1; width: 2.2px; height: var(--h); border-radius: 1px; background: var(--paper); box-shadow: 0 0 0 0.6px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)), 0 1px 1px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); opacity: 0; animation: field-shredder-fall 0.7s cubic-bezier(0.5, 0, 0.8, 0.5) 0.6s forwards; }
@keyframes field-shredder-fall {
0% { opacity: 0; transform: translateY(-8px); }
15% { opacity: 1; }
100% { opacity: 1; transform: translate(var(--drift), var(--land)) rotate(var(--rot)); }
}
.field-shredder[data-shown] .field-shredder__strip { animation: none; opacity: 0; }
.field-shredder__machine { position: absolute; left: 14px; top: 62px; z-index: 2; width: 234px; height: 46px; border-radius: 6px 6px 3px 3px; background: linear-gradient(var(--body), color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417))); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 16%, transparent), 0 8px 12px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-shredder__machine::before { content: ''; position: absolute; left: 14px; right: 14px; top: 6px; height: 4px; border-radius: 2px; background: light-dark(#373a40, #121417); box-shadow: inset 0 1px 2px light-dark(rgba(0, 0, 0, 0.24), rgba(0, 0, 0, 0.8)); }
.field-shredder__led { position: absolute; right: 14px; bottom: 12px; width: 6px; height: 6px; border-radius: 50%; background: var(--lit); box-shadow: 0 0 6px var(--lit); }
.field-shredder__brand { position: absolute; left: 16px; bottom: 9px; color: color-mix(in oklab, var(--lit) 55%, transparent); font: 700 8px/1 system-ui, sans-serif; letter-spacing: 0.24em; }
.field-shredder__bin { position: absolute; left: 28px; top: 108px; width: 206px; height: 84px; border: 1.5px solid light-dark(#cbced3, #393c41); border-top: 0; border-radius: 0 0 10px 10px; background: repeating-linear-gradient(90deg, transparent 0 9px, color-mix(in oklab, light-dark(#cbced3, #393c41) 45%, transparent) 9px 10px); }
.field-shredder__read { position: absolute; left: 0; right: 0; bottom: 0; color: light-dark(#5f626a, #a7abb2); font: 500 11.5px system-ui, sans-serif; text-align: center; }
.field-shredder__read b { color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
@media (prefers-reduced-motion: reduce) {
.field-shredder * { animation-duration: 0.01s !important; animation-delay: 0s !important; transition: none !important; }
}JavaScript
function initFieldShredder(root) {
var STRIPS = [-3.5, 0, 3.5];
var slip = root.querySelector('.field-shredder__slip');
var input = root.querySelector('.field-shredder__input');
var caret = root.querySelector('.field-shredder__caret');
var eye = root.querySelector('.field-shredder__eye');
var read = root.querySelector('.field-shredder__read');
var shown = false;
// Showing pulls back up only what was already shredded; letters typed after that just appear.
var risen = 0;
var tiles = [].slice.call(root.querySelectorAll('.field-shredder__tile'));
var strips = [].slice.call(root.querySelectorAll('.field-shredder__strip'));
var dots = [].slice.call(root.querySelectorAll('.field-shredder__dot'));
var empty = document.createElement('span');
empty.className = 'field-shredder__empty';
empty.textContent = 'Type a password';
function tile(i, ch) {
var t = document.createElement('span');
t.className = 'field-shredder__tile' + (i < risen ? ' field-shredder__tile--rise' : '');
t.style.left = 28 + i * 13 + 'px';
t.setAttribute('aria-hidden', 'true');
t.textContent = ch;
return t;
}
// Strips are their own elements, so they stay on the pile after the letter is gone.
function strip(i, k) {
var s = document.createElement('i');
s.className = 'field-shredder__strip';
s.setAttribute('aria-hidden', 'true');
s.style.cssText = 'left:' + (28 + i * 13 + 6.5) + 'px;--dx:' + STRIPS[k] + 'px;--h:' + (12 + ((i * 3 + k * 5) % 6)) + 'px;--drift:' + (((i + k) % 3) - 1) + 'px;--land:' + (64 - Math.floor(i / 3) * 3 - k) + 'px;--rot:' + (((i * 7 + k * 13) % 50) - 25) + 'deg';
return s;
}
function dot(i) {
var d = document.createElement('i');
d.className = 'field-shredder__dot';
d.style.left = 10 + i * 13 + 'px';
d.setAttribute('aria-hidden', 'true');
return d;
}
function render(fresh) {
var value = input.value;
if (fresh) {
tiles.concat(strips, dots).forEach(function (n) {
n.remove();
});
tiles = [];
strips = [];
dots = [];
}
// One tile, three strips and one dot per character: keep the ones already there.
while (tiles.length > value.length) tiles.pop().remove();
while (dots.length > value.length) dots.pop().remove();
while (strips.length > value.length * 3) strips.pop().remove();
for (var i = 0; i < value.length; i++) {
if (tiles[i]) tiles[i].textContent = value[i];
else root.insertBefore((tiles[i] = tile(i, value[i])), strips[0] || eye);
if (!dots[i]) slip.insertBefore((dots[i] = dot(i)), caret);
for (var k = 0; k < 3; k++) if (!strips[i * 3 + k]) root.insertBefore((strips[i * 3 + k] = strip(i, k)), eye);
}
if (value) empty.remove();
else slip.insertBefore(empty, slip.firstChild);
caret.style.left = 10 + value.length * 13 + 'px';
read.innerHTML = '<b>' + value.length + '</b> ' + (value.length === 1 ? 'character' : 'characters') + ', ' + (shown ? 'showing' : 'shredded');
}
function onInput() {
render(false);
}
function onEye() {
risen = input.value.length;
shown = !shown;
root.toggleAttribute('data-shown', shown);
eye.setAttribute('aria-pressed', String(shown));
input.type = shown ? 'text' : 'password';
render(true);
}
input.addEventListener('input', onInput);
eye.addEventListener('click', onEye);
return function () {
input.removeEventListener('input', onInput);
eye.removeEventListener('click', onEye);
};
}
document.querySelectorAll('.field-shredder').forEach(initFieldShredder);Goal: Create a "Shredder" input.
Visual behavior: A password field over a paper shredder: each character feeds down into the slot and comes out as strips on the pile, leaving a dot on the slip. Show pulls the letters back out whole.
Technique:
- A real password input lies transparent over the paper slip. The letters, dots, caret and strips are drawn from its value, and the eye button switches it between password and text.
- Each character is a tile on the slip that feeds down into the slot (it drops, squashes and fades); a dot takes its place once it has gone through, as any password field shows.
- Three paper strips per character are their own elements, so they stay on the pile after the letter has gone. Each lands at its own drift, depth and angle, set from a few variables.
- Show runs the feed in reverse for the letters already shredded and lets new ones simply appear; hiding again feeds them all back through.
Accessibility: A native password input named "Password" with autocomplete new-password; focus outlines the slip and shows a caret. The eye is a real button named "Show password" with aria-pressed. The tiles, strips, dots and count are aria-hidden. Under prefers-reduced-motion every step happens at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Shredder" input.
Visual behavior: A password field over a paper shredder: each character feeds down into the slot and comes out as strips on the pile, leaving a dot on the slip. Show pulls the letters back out whole.
Technique:
- A real password input lies transparent over the paper slip. The letters, dots, caret and strips are drawn from its value, and the eye button switches it between password and text.
- Each character is a tile on the slip that feeds down into the slot (it drops, squashes and fades); a dot takes its place once it has gone through, as any password field shows.
- Three paper strips per character are their own elements, so they stay on the pile after the letter has gone. Each lands at its own drift, depth and angle, set from a few variables.
- Show runs the feed in reverse for the letters already shredded and lets new ones simply appear; hiding again feeds them all back through.
Accessibility: A native password input named "Password" with autocomplete new-password; focus outlines the slip and shows a caret. The eye is a real button named "Show password" with aria-pressed. The tiles, strips, dots and count are aria-hidden. Under prefers-reduced-motion every step happens at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A password field over a paper shredder: each character feeds down into the slot and comes out as strips on the pile, leaving a dot on the slip. Show pulls the letters back out whole.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- state change, click, keyframe, focus
- Techniques
- keyframes, state machine, css variables, transform only
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native password input named "Password" with autocomplete new-password; focus outlines the slip and shows a caret. The eye is a real button named "Show password" with aria-pressed. The tiles, strips, dots and count are aria-hidden. Under prefers-reduced-motion every step happens at once.