Knife Switch
import { useState } from 'react';
const css = `
@keyframes toggle-knife-close {
0% { transform: rotate(-50deg); animation-timing-function: cubic-bezier(0.6, 0, 1, 0.7); }
60% { transform: rotate(1deg); animation-timing-function: ease-out; }
75% { transform: rotate(-5deg); animation-timing-function: ease-in; }
88% { transform: rotate(0.5deg); }
100% { transform: rotate(0deg); }
}
@keyframes toggle-knife-open { 0% { transform: rotate(0deg); } 100% { transform: rotate(-50deg); } }
@keyframes toggle-knife-spark {
0% { opacity: 0; transform: scale(0.3) rotate(0deg); }
15% { opacity: 1; transform: scale(1.1) rotate(10deg); }
100% { opacity: 0; transform: scale(1.6) rotate(25deg); }
}
.toggle-knife { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-knife__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-knife__track { position: relative; width: 86px; height: 56px; border-radius: 6px; }
.toggle-knife__input:focus-visible + .toggle-knife__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-knife__plate {
position: absolute;
left: 0;
right: 0;
bottom: 4px;
height: 12px;
border-radius: 4px;
background: light-dark(#ebedf0, #232529);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.toggle-knife__lamp { position: absolute; left: 30px; top: 4px; width: 22px; height: 4px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); transition: background-color 0.2s, box-shadow 0.2s; }
.toggle-knife__hinge { position: absolute; left: 6px; bottom: 12px; width: 12px; height: 12px; border-radius: 50%; background: light-dark(#6d7179, #7d8087); box-shadow: inset 0 0 0 3px light-dark(#cbced3, #393c41); z-index: 2; }
.toggle-knife__clip {
position: absolute;
left: 64px;
bottom: 14px;
width: 10px;
height: 10px;
border: 2px solid light-dark(#6d7179, #7d8087);
border-top: 0;
border-radius: 0 0 3px 3px;
}
.toggle-knife__blade {
position: absolute;
left: 12px;
bottom: 16px;
width: 64px;
height: 4px;
border-radius: 2px;
background: linear-gradient(light-dark(#42454c, #e5e6e9), light-dark(#5f626a, #a7abb2));
transform-origin: 0 50%;
transform: rotate(-50deg);
z-index: 1;
}
.toggle-knife__grip {
position: absolute;
right: -4px;
top: -6px;
width: 7px;
height: 16px;
border-radius: 3px;
background: light-dark(#6d7179, #7d8087);
box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2);
}
.toggle-knife__spark {
position: absolute;
left: 57px;
bottom: 6px;
width: 24px;
height: 24px;
border-radius: 50%;
background:
radial-gradient(circle, light-dark(#373a40, #f1f2f3) 0 2px, transparent 3px),
repeating-conic-gradient(light-dark(#373a40, #f1f2f3) 0 5deg, transparent 5deg 40deg);
mask-image: radial-gradient(circle, #000 30%, transparent 70%);
opacity: 0;
pointer-events: none;
}
.toggle-knife__input:checked + .toggle-knife__track .toggle-knife__blade { transform: rotate(0deg); }
.toggle-knife__input:checked + .toggle-knife__track .toggle-knife__lamp { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#50535b, #c6c8cd); transition-delay: 0.3s; }
.toggle-knife[data-armed] .toggle-knife__input:checked + .toggle-knife__track .toggle-knife__blade { animation: toggle-knife-close 0.55s; }
.toggle-knife[data-armed] .toggle-knife__input:not(:checked) + .toggle-knife__track .toggle-knife__blade { animation: toggle-knife-open 0.4s cubic-bezier(0.2, 0.8, 0.3, 1); }
.toggle-knife[data-armed] .toggle-knife__input:checked + .toggle-knife__track .toggle-knife__spark { animation: toggle-knife-spark 0.4s ease-out 0.32s; }
@media (prefers-reduced-motion: reduce) {
.toggle-knife__track * { animation: none !important; transition-delay: 0s !important; }
}
`;
export function KnifeToggle() {
const [armed, setArmed] = useState(false);
return (
<>
<style>
{css}
</style>
<label className="toggle-knife" data-armed={armed || undefined}>
<input className="toggle-knife__input" type="checkbox" role="switch" onChange={() => setArmed(true)} />
<span className="toggle-knife__track" aria-hidden="true">
<span className="toggle-knife__plate">
<span className="toggle-knife__lamp" />
</span>
<span className="toggle-knife__clip" />
<span className="toggle-knife__blade">
<span className="toggle-knife__grip" />
</span>
<span className="toggle-knife__hinge" />
<span className="toggle-knife__spark" />
</span>
Main power
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initToggleKnife(el) on the mounted element instead.
<label class="toggle-knife"><input class="toggle-knife__input" type="checkbox" role="switch"/><span class="toggle-knife__track" aria-hidden="true"><span class="toggle-knife__plate"><span class="toggle-knife__lamp"></span></span><span class="toggle-knife__clip"></span><span class="toggle-knife__blade"><span class="toggle-knife__grip"></span></span><span class="toggle-knife__hinge"></span><span class="toggle-knife__spark"></span></span>Main power</label>@keyframes toggle-knife-close {
0% { transform: rotate(-50deg); animation-timing-function: cubic-bezier(0.6, 0, 1, 0.7); }
60% { transform: rotate(1deg); animation-timing-function: ease-out; }
75% { transform: rotate(-5deg); animation-timing-function: ease-in; }
88% { transform: rotate(0.5deg); }
100% { transform: rotate(0deg); }
}
@keyframes toggle-knife-open { 0% { transform: rotate(0deg); } 100% { transform: rotate(-50deg); } }
@keyframes toggle-knife-spark {
0% { opacity: 0; transform: scale(0.3) rotate(0deg); }
15% { opacity: 1; transform: scale(1.1) rotate(10deg); }
100% { opacity: 0; transform: scale(1.6) rotate(25deg); }
}
.toggle-knife { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-knife__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-knife__track { position: relative; width: 86px; height: 56px; border-radius: 6px; }
.toggle-knife__input:focus-visible + .toggle-knife__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-knife__plate {
position: absolute;
left: 0;
right: 0;
bottom: 4px;
height: 12px;
border-radius: 4px;
background: light-dark(#ebedf0, #232529);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.toggle-knife__lamp { position: absolute; left: 30px; top: 4px; width: 22px; height: 4px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); transition: background-color 0.2s, box-shadow 0.2s; }
.toggle-knife__hinge { position: absolute; left: 6px; bottom: 12px; width: 12px; height: 12px; border-radius: 50%; background: light-dark(#6d7179, #7d8087); box-shadow: inset 0 0 0 3px light-dark(#cbced3, #393c41); z-index: 2; }
.toggle-knife__clip {
position: absolute;
left: 64px;
bottom: 14px;
width: 10px;
height: 10px;
border: 2px solid light-dark(#6d7179, #7d8087);
border-top: 0;
border-radius: 0 0 3px 3px;
}
.toggle-knife__blade {
position: absolute;
left: 12px;
bottom: 16px;
width: 64px;
height: 4px;
border-radius: 2px;
background: linear-gradient(light-dark(#42454c, #e5e6e9), light-dark(#5f626a, #a7abb2));
transform-origin: 0 50%;
transform: rotate(-50deg);
z-index: 1;
}
.toggle-knife__grip {
position: absolute;
right: -4px;
top: -6px;
width: 7px;
height: 16px;
border-radius: 3px;
background: light-dark(#6d7179, #7d8087);
box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2);
}
.toggle-knife__spark {
position: absolute;
left: 57px;
bottom: 6px;
width: 24px;
height: 24px;
border-radius: 50%;
background:
radial-gradient(circle, light-dark(#373a40, #f1f2f3) 0 2px, transparent 3px),
repeating-conic-gradient(light-dark(#373a40, #f1f2f3) 0 5deg, transparent 5deg 40deg);
mask-image: radial-gradient(circle, #000 30%, transparent 70%);
opacity: 0;
pointer-events: none;
}
.toggle-knife__input:checked + .toggle-knife__track .toggle-knife__blade { transform: rotate(0deg); }
.toggle-knife__input:checked + .toggle-knife__track .toggle-knife__lamp { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#50535b, #c6c8cd); transition-delay: 0.3s; }
.toggle-knife[data-armed] .toggle-knife__input:checked + .toggle-knife__track .toggle-knife__blade { animation: toggle-knife-close 0.55s; }
.toggle-knife[data-armed] .toggle-knife__input:not(:checked) + .toggle-knife__track .toggle-knife__blade { animation: toggle-knife-open 0.4s cubic-bezier(0.2, 0.8, 0.3, 1); }
.toggle-knife[data-armed] .toggle-knife__input:checked + .toggle-knife__track .toggle-knife__spark { animation: toggle-knife-spark 0.4s ease-out 0.32s; }
@media (prefers-reduced-motion: reduce) {
.toggle-knife__track * { animation: none !important; transition-delay: 0s !important; }
}function initToggleKnife(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('.toggle-knife').forEach(initToggleKnife);Goal: Create a "Knife Switch" toggle.
Visual behavior: A lab knife switch for main power: the blade drops from upright into the contact clips with a bounce and a spark, and the lamp on the base comes on. Off, it swings back up.
Technique:
- A real native checkbox with role="switch" (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The blade pivots on a hinge at its left end, from rotate(-50deg) to 0. Closing is a keyframe with an accelerating fall, a small rebound and a settle; opening eases back up.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; checked and unchecked use differently named keyframes, so each change restarts the right one.
- The spark is a radial dot plus repeating-conic rays (masked to fade) that bursts once at the clips as the blade lands; the lamp lights after a delay.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion the blade moves instantly and there is no spark.
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 "Knife Switch" toggle.
Visual behavior: A lab knife switch for main power: the blade drops from upright into the contact clips with a bounce and a spark, and the lamp on the base comes on. Off, it swings back up.
Technique:
- A real native checkbox with role="switch" (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The blade pivots on a hinge at its left end, from rotate(-50deg) to 0. Closing is a keyframe with an accelerating fall, a small rebound and a settle; opening eases back up.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; checked and unchecked use differently named keyframes, so each change restarts the right one.
- The spark is a radial dot plus repeating-conic rays (masked to fade) that bursts once at the clips as the blade lands; the lamp lights after a delay.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion the blade moves instantly and there is no spark.
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 lab knife switch for main power: the blade drops from upright into the contact clips with a bounce and a spark, and the lamp on the base comes on. Off, it swings back up.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, spring
- Techniques
- keyframes, state machine, conic gradient
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion the blade moves instantly and there is no spark.