Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. The toggle knob serves as the O in ON/OFF. Bold typography with smooth knob animation. Supports light/dark themes.
/*
================================================================================
AI COMPONENT: ChunkyToggle
================================================================================
A toggle switch where:
- The knob doubles as the letter "O" in ON/OFF
- Bold typography with N and FF letters
- Smooth knob animation (280ms ease)
- Light/dark theme support
================================================================================
QUICK REFERENCE FOR AI AGENTS
================================================================================
WHEN USER ASKS TO... MODIFY THIS SECTION
─────────────────────────────────────────────────────────────────────────────
Change overall size → --size variable (default: 48px)
Change colors → --fg (foreground) and --bg (background)
Change animation speed → --speed variable (default: 280ms)
Change animation curve → --ease variable
Change track border thickness → box-shadow in .onoff-toggle__track
Change track roundness → border-radius in .onoff-toggle__track
Change knob size → width/height in ::before (0.78 ratio)
Change knob shape → border-radius in ::before (50% = circle)
Change font → font-family in .onoff-toggle__label
Change font weight → font-weight (700-900 recommended)
Switch theme → theme prop: "light" | "dark"
Handle toggle changes → onChange prop callback
================================================================================
SETUP
================================================================================
1. Create file: components/ChunkyToggle.tsx
2. Copy this entire code block into that file
3. Import: import ChunkyToggle from "@/components/ChunkyToggle"
4. Use: <ChunkyToggle theme="light" onChange={(on) => console.log(on)} />
================================================================================
PROPS
================================================================================
| Prop | Type | Default |
|-------------------|-------------------------|------------------|
| checked | boolean | false |
| label | string | "Toggle switch" |
| theme | "light" | "dark" | "light" |
| onChange | (checked: boolean)=>void| undefined |
| className | string | "" |
================================================================================
SOURCE CODE
================================================================================
*/
"use client";
import React, { useState, useEffect } from "react";
export interface ChunkyToggleProps {
/** Checked state (controlled) */
checked?: boolean;
/** Accessibility label */
label?: string;
/** Theme: light or dark */
theme?: "light" | "dark";
/** Callback when toggle changes */
onChange?: (checked: boolean) => void;
/** Optional className */
className?: string;
}
export default function ChunkyToggle({
checked = false,
label = "Toggle switch",
theme = "light",
onChange,
className = "",
}: ChunkyToggleProps) {
const [isChecked, setIsChecked] = useState<boolean>(checked);
const isDark = theme === "dark";
// Sync internal state with prop changes
useEffect(() => {
setIsChecked(checked);
}, [checked]);
const handleChange = () => {
const newValue = !isChecked;
setIsChecked(newValue);
onChange?.(newValue);
};
return (
<label className={`onoff-toggle ${className}`}>
<input
className="onoff-toggle__track"
type="checkbox"
role="switch"
checked={isChecked}
onChange={handleChange}
/>
<span className="onoff-toggle__label onoff-toggle__label--on" aria-hidden="true">
N
</span>
<span className="onoff-toggle__label onoff-toggle__label--off" aria-hidden="true">
FF
</span>
<span className="onoff-toggle__a11y">{label}</span>
<style jsx>{`
.onoff-toggle {
/* ========== CUSTOMIZE: SIZE ==========
* Adjust this to scale the entire component
*/
--size: 48px;
--light: ${isDark ? "hsl(220, 12%, 93%)" : "hsl(220, 12%, 93%)"};
--dark: ${isDark ? "hsl(220, 12%, 8%)" : "hsl(220, 12%, 8%)"};
--fg: ${isDark ? "var(--light)" : "var(--dark)"};
--bg: ${isDark ? "var(--dark)" : "var(--light)"};
--speed: 280ms;
--ease: cubic-bezier(0.4, 0, 0.2, 1);
display: grid;
grid-template-columns: var(--size) auto;
grid-template-rows: repeat(2, var(--size));
cursor: pointer;
-webkit-tap-highlight-color: transparent;
font-family: system-ui, -apple-system, sans-serif;
}
.onoff-toggle__track {
/* ========== CUSTOMIZE: TRACK ==========
* box-shadow: track border thickness
* border-radius: track roundness (0.48 = pill shape)
*/
box-shadow: 0 0 0 max(2.5px, 0.055em) var(--fg) inset;
border-radius: calc(var(--size) * 0.48);
display: block;
grid-row: 1 / 3;
width: var(--size);
height: calc(var(--size) * 2);
-webkit-appearance: none;
appearance: none;
cursor: pointer;
background: transparent;
transition: box-shadow var(--speed);
}
.onoff-toggle__track::before {
/* ========== CUSTOMIZE: KNOB ==========
* background-color: knob fill color
* border-radius: 50% = circle, lower = rounded square
* margin: gap between knob and track edge
*/
background-color: var(--fg);
border-radius: 50%;
content: "";
display: block;
margin: calc(var(--size) * 0.11);
width: calc(var(--size) * 0.78);
height: calc(var(--size) * 0.78);
transform: translateY(var(--size));
transition: transform var(--speed) var(--ease);
}
.onoff-toggle__track:checked::before {
transform: translateY(0);
}
.onoff-toggle__label {
/* ========== CUSTOMIZE: TYPOGRAPHY ==========
* font-family: change the typeface
* font-weight: boldness (700-900 for chunky look)
* font-size: letter size relative to toggle
*/
display: flex;
align-items: center;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
font-weight: 900;
font-size: calc(var(--size) * 0.82);
letter-spacing: -0.5px;
pointer-events: none;
color: var(--fg);
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
padding-left: calc(var(--size) * 0.12);
}
.onoff-toggle__label--on {
align-self: center;
grid-row: 1;
grid-column: 2;
}
.onoff-toggle__label--off {
align-self: center;
grid-row: 2;
grid-column: 2;
}
.onoff-toggle__a11y {
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
}
/* Hover feedback */
.onoff-toggle:hover .onoff-toggle__track {
box-shadow: 0 0 0 max(3.5px, 0.075em) var(--fg) inset;
}
`}</style>
</label>
);
}
export { ChunkyToggle };/*
================================================================================
USAGE EXAMPLES
================================================================================
*/
import ChunkyToggle from "@/components/ChunkyToggle";
// ============================================================================
// BASIC USAGE - Uncontrolled (manages own state)
// ============================================================================
export default function BasicExample() {
return (
<div className="flex items-center justify-center h-screen bg-gray-100">
<ChunkyToggle />
</div>
);
}
// ============================================================================
// CONTROLLED TOGGLE - Parent manages state
// ============================================================================
import { useState } from "react";
export function ControlledExample() {
const [isOn, setIsOn] = useState(true);
return (
<div style={{ backgroundColor: isOn ? "#f5f5f5" : "#0a0a0a" }}>
<ChunkyToggle
theme={isOn ? "light" : "dark"}
checked={isOn}
onChange={setIsOn}
/>
</div>
);
}
// ============================================================================
// AUTO-THEME TOGGLE - Switch theme based on toggle state
// ============================================================================
export function AutoThemeExample() {
const [isOn, setIsOn] = useState(true);
// ON = light mode, OFF = dark mode
return (
<div
className="min-h-screen flex items-center justify-center transition-colors"
style={{ backgroundColor: isOn ? "#f5f5f5" : "#0a0a0a" }}
>
<ChunkyToggle
theme={isOn ? "light" : "dark"}
checked={isOn}
onChange={setIsOn}
label="Theme toggle"
/>
</div>
);
}
// ============================================================================
// CUSTOMIZATION EXAMPLES (modify CSS variables in the component)
// ============================================================================
/*
CHANGE SIZE:
--size: 32px; // Small
--size: 48px; // Medium (default)
--size: 64px; // Large
--size: 80px; // Extra large
CHANGE COLORS:
--fg: hsl(210, 100%, 50%); // Blue foreground
--fg: hsl(145, 70%, 45%); // Green foreground
--fg: hsl(0, 85%, 60%); // Red foreground
CHANGE ANIMATION:
--speed: 150ms; // Fast
--speed: 280ms; // Default
--speed: 400ms; // Slow
--ease: linear; // No easing
--ease: cubic-bezier(0.4, 0, 0.2, 1); // Material (default)
--ease: cubic-bezier(0.68, -0.55, 0.27, 1.55); // Bouncy
CHANGE KNOB SHAPE:
border-radius: 50%; // Circle (default)
border-radius: 25%; // Rounded square
border-radius: 0; // Square
*/