Toaster
import { useState } from 'react';
const css = `
@keyframes cb-toaster-pop { 0% { transform: translateY(14px); } 50% { transform: translateY(-9px); } 72% { transform: translateY(-3px); } 86% { transform: translateY(-12px); } 100% { transform: translateY(-4px); } }
@keyframes cb-toaster-sink { from { transform: translateY(-4px); } to { transform: translateY(14px); } }
.cb-toaster { position: relative; display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.cb-toaster__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-toaster__body { position: relative; width: 52px; height: 46px; }
.cb-toaster__input:focus-visible + .cb-toaster__body { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 8px; }
.cb-toaster__toast {
position: absolute;
left: 12px;
top: 4px;
width: 26px;
height: 26px;
border-radius: 8px 8px 3px 3px;
background: light-dark(#6d7179, #7d8087);
box-shadow: inset 0 0 0 2.5px light-dark(#cbced3, #393c41);
transform: translateY(14px);
transition: background-color 0.4s;
}
.cb-toaster__case {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 30px;
border-radius: 12px 12px 6px 6px;
background: linear-gradient(light-dark(#cbced3, #393c41), light-dark(#dbdde1, #2e3035));
box-shadow: inset 0 1px 0 light-dark(#6d7179, #7d8087), 0 8px 14px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
}
.cb-toaster__case::before { content: ''; position: absolute; left: 11px; right: 11px; top: 3px; height: 3px; border-radius: 2px; background: light-dark(#f3f4f7, #1a1c1f); }
.cb-toaster__lever { position: absolute; right: -4px; top: 34px; width: 6px; height: 4px; border-radius: 2px; background: light-dark(#50535b, #c6c8cd); transition: transform 0.3s; }
.cb-toaster__dial { position: absolute; left: 9px; bottom: 8px; width: 7px; height: 7px; border-radius: 50%; background: light-dark(#5f626a, #a7abb2); }
.cb-toaster__input:checked + .cb-toaster__body .cb-toaster__toast { transform: translateY(-4px); background: light-dark(#50535b, #c6c8cd); }
.cb-toaster__input:checked + .cb-toaster__body .cb-toaster__lever { transform: translateY(-12px); }
.cb-toaster[data-armed] .cb-toaster__input:checked + .cb-toaster__body .cb-toaster__toast { animation: cb-toaster-pop 0.55s cubic-bezier(0.3, 0, 0.4, 1); }
.cb-toaster[data-armed] .cb-toaster__input:not(:checked) + .cb-toaster__body .cb-toaster__toast { animation: cb-toaster-sink 0.3s ease-in; }
@media (prefers-reduced-motion: reduce) {
.cb-toaster * { animation: none !important; transition: none !important; }
}
`;
export function ToasterCheckbox() {
const [armed, setArmed] = useState(false);
return (
<>
<style>
{css}
</style>
<label className="cb-toaster" data-armed={armed || undefined}>
<input className="cb-toaster__input" type="checkbox" onChange={() => setArmed(true)} />
<span className="cb-toaster__body" aria-hidden="true">
<span className="cb-toaster__toast" />
<span className="cb-toaster__case">
<span className="cb-toaster__dial" />
</span>
<span className="cb-toaster__lever" />
</span>
Breakfast included
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCbToaster(el) on the mounted element instead.
<label class="cb-toaster"><input class="cb-toaster__input" type="checkbox"/><span class="cb-toaster__body" aria-hidden="true"><span class="cb-toaster__toast"></span><span class="cb-toaster__case"><span class="cb-toaster__dial"></span></span><span class="cb-toaster__lever"></span></span>Breakfast included</label>@keyframes cb-toaster-pop { 0% { transform: translateY(14px); } 50% { transform: translateY(-9px); } 72% { transform: translateY(-3px); } 86% { transform: translateY(-12px); } 100% { transform: translateY(-4px); } }
@keyframes cb-toaster-sink { from { transform: translateY(-4px); } to { transform: translateY(14px); } }
.cb-toaster { position: relative; display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.cb-toaster__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-toaster__body { position: relative; width: 52px; height: 46px; }
.cb-toaster__input:focus-visible + .cb-toaster__body { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 8px; }
.cb-toaster__toast {
position: absolute;
left: 12px;
top: 4px;
width: 26px;
height: 26px;
border-radius: 8px 8px 3px 3px;
background: light-dark(#6d7179, #7d8087);
box-shadow: inset 0 0 0 2.5px light-dark(#cbced3, #393c41);
transform: translateY(14px);
transition: background-color 0.4s;
}
.cb-toaster__case {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 30px;
border-radius: 12px 12px 6px 6px;
background: linear-gradient(light-dark(#cbced3, #393c41), light-dark(#dbdde1, #2e3035));
box-shadow: inset 0 1px 0 light-dark(#6d7179, #7d8087), 0 8px 14px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
}
.cb-toaster__case::before { content: ''; position: absolute; left: 11px; right: 11px; top: 3px; height: 3px; border-radius: 2px; background: light-dark(#f3f4f7, #1a1c1f); }
.cb-toaster__lever { position: absolute; right: -4px; top: 34px; width: 6px; height: 4px; border-radius: 2px; background: light-dark(#50535b, #c6c8cd); transition: transform 0.3s; }
.cb-toaster__dial { position: absolute; left: 9px; bottom: 8px; width: 7px; height: 7px; border-radius: 50%; background: light-dark(#5f626a, #a7abb2); }
.cb-toaster__input:checked + .cb-toaster__body .cb-toaster__toast { transform: translateY(-4px); background: light-dark(#50535b, #c6c8cd); }
.cb-toaster__input:checked + .cb-toaster__body .cb-toaster__lever { transform: translateY(-12px); }
.cb-toaster[data-armed] .cb-toaster__input:checked + .cb-toaster__body .cb-toaster__toast { animation: cb-toaster-pop 0.55s cubic-bezier(0.3, 0, 0.4, 1); }
.cb-toaster[data-armed] .cb-toaster__input:not(:checked) + .cb-toaster__body .cb-toaster__toast { animation: cb-toaster-sink 0.3s ease-in; }
@media (prefers-reduced-motion: reduce) {
.cb-toaster * { animation: none !important; transition: none !important; }
}function initCbToaster(root) {
var input = root.querySelector('input');
if (!input) return;
// Animate only after the first change, so nothing plays on page load.
function arm() { root.setAttribute('data-armed', ''); }
input.addEventListener('change', arm);
return function () { input.removeEventListener('change', arm); };
}
document.querySelectorAll('.cb-toaster').forEach(initCbToaster);Goal: Create a "Toaster" checkbox.
Visual behavior: A breakfast included checkbox: ticking it pops the toast up out of the toaster with a bounce, browned and ready, as the lever goes up.
Technique:
- A real native checkbox (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The toast sits behind the toaster body; ticking it translates up through a keyframe with a double bounce and lightens it as it pops.
- The lever slides up the side with a transition, and unticking sinks the toast back with its own keyframe.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; ticking and unticking use differently named keyframes, so each change restarts the right one.
Accessibility: A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the visual. Under prefers-reduced-motion the toast moves instantly.
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 "Toaster" checkbox.
Visual behavior: A breakfast included checkbox: ticking it pops the toast up out of the toaster with a bounce, browned and ready, as the lever goes up.
Technique:
- A real native checkbox (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The toast sits behind the toaster body; ticking it translates up through a keyframe with a double bounce and lightens it as it pops.
- The lever slides up the side with a transition, and unticking sinks the toast back with its own keyframe.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; ticking and unticking use differently named keyframes, so each change restarts the right one.
Accessibility: A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the visual. Under prefers-reduced-motion the toast moves instantly.
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 breakfast included checkbox: ticking it pops the toast up out of the toaster with a bounce, browned and ready, as the lever goes up.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- click, state change, spring
- Techniques
- keyframes, css transition, pseudo elements
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the visual. Under prefers-reduced-motion the toast moves instantly.