A beautiful, interactive 3.5" floppy disk React component for retro-themed UIs.
Perfect for file managers, software libraries, game launchers, and nostalgic interfaces.
- Highly Customizable - Size variants, color themes, and structured label content
- Interactive - Hover animations plus click and double-click handlers
- Flexible Sizing - From tiny (60px) to hero (600px) or custom pixel values
- Performant - Optimized for rendering multiple instances in lists/grids
- Accessible - ARIA labels and keyboard navigation support
- TypeScript - Full type definitions included
- Zero Dependencies - Only requires React
npm install retro-floppy
# or
yarn add retro-floppy
# or
pnpm add retro-floppyimport { FloppyDisk } from 'retro-floppy';
import 'retro-floppy/dist/retro-floppy.css';
function App() {
return (
<FloppyDisk
size="medium"
label={{
name: 'Second Reality',
author: 'Future Crew',
year: '1993',
description: 'Legendary 1993 demo by Future Crew',
type: 'ZIP',
size: '1.44 MB',
}}
onClick={() => console.log('Disk clicked!')}
/>
);
}<FloppyDisk
size="small"
label={{
name: 'My Application',
author: 'Version 1.0',
}}
diskType="HD"
capacity="1.44 MB"
/>const applications = [
{ id: 1, name: 'Photoshop 3.0', author: 'Adobe Systems' },
{ id: 2, name: 'Doom', author: 'id Software' },
{ id: 3, name: 'Windows 95', author: 'Microsoft' },
];
function SoftwareLibrary() {
const [selected, setSelected] = useState<number | null>(null);
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: '20px',
}}
>
{applications.map((app) => (
<FloppyDisk
key={app.id}
size="small"
label={{
name: app.name,
author: app.author,
}}
selected={selected === app.id}
onClick={() => setSelected(app.id)}
onDoubleClick={() => alert(`Launching ${app.name}`)}
/>
))}
</div>
);
}<FloppyDisk
size="large"
label={{
name: 'Custom Theme',
}}
theme={{
diskColor: '#1a1a1a',
slideColor: '#ffd700',
backgroundColor: '#f0f0f0',
labelColor: '#ffffcc',
labelTextColor: '#333333',
}}
/><div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{files.map((file) => (
<div
key={file.id}
style={{ display: 'flex', alignItems: 'center', gap: '15px' }}
>
<FloppyDisk size="tiny" variant="compact" label={{ name: file.name }} />
<span>{file.name}</span>
<span>{file.size}</span>
</div>
))}
</div>The disk uses a default theme that works well in both light and dark UIs.
You can customize colors via the theme prop to match your design.
The component includes several built-in theme presets for different aesthetics:
Classic floppy disk appearance optimized for light backgrounds.
import { FloppyDisk, LIGHT_FLOPPY_THEME } from 'retro-floppy';
<FloppyDisk
label={{ name: 'My App', author: 'Developer' }}
theme={LIGHT_FLOPPY_THEME}
/>;Sleek dark appearance optimized for dark backgrounds.
import { FloppyDisk, DARK_FLOPPY_THEME } from 'retro-floppy';
<FloppyDisk
label={{ name: 'My App', author: 'Developer' }}
theme={DARK_FLOPPY_THEME}
/>;Vibrant cyberpunk aesthetic with magenta and cyan accents.
import { FloppyDisk, NEON_THEME } from 'retro-floppy';
<FloppyDisk
label={{ name: 'My App', author: 'Developer' }}
theme={NEON_THEME}
/>;Classic 90s beige computer aesthetic with warm, vintage colors.
import { FloppyDisk, RETRO_THEME } from 'retro-floppy';
<FloppyDisk
label={{ name: 'My App', author: 'Developer' }}
theme={RETRO_THEME}
/>;Soft, modern colors with gradient labels.
import { FloppyDisk, PASTEL_THEME } from 'retro-floppy';
<FloppyDisk
label={{ name: 'My App', author: 'Developer' }}
theme={PASTEL_THEME}
/>;Create your own theme by providing a custom theme object:
<FloppyDisk
label={{ name: 'Custom', author: 'Me' }}
theme={{
diskColor: '#ff6b6b',
slideColor: '#4ecdc4',
backgroundColor: '#ffe66d',
labelColor: '#ffffff',
labelTextColor: '#2c3e50',
enableGradient: false,
}}
/>When using gradients, you can customize the generation:
<FloppyDisk
label={{ name: 'Gradient Demo', author: 'Dev' }}
theme={{
enableGradient: true,
gradientType: 'linear', // 'linear', 'radial', 'conic', or 'auto'
gradientOptions: {
seed: 12345, // Custom seed for reproducible gradients
colors: ['#ff6b6b', '#4ecdc4', '#45b7d1'], // Custom color palette
angle: 135, // Angle for linear gradients (0-360)
},
}}
/>| Prop | Type | Default | Description |
|---|---|---|---|
size |
'tiny' | 'small' | 'medium' | 'large' | 'hero' | number |
'medium' |
Size of the disk. Predefined or custom px value |
label |
FloppyLabel |
undefined |
Structured label data for the disk (name, author, year, description, type, size) |
diskType |
'HD' | 'DD' |
'HD' |
High Density or Double Density |
capacity |
string |
'1.44 MB' |
Storage capacity display (overrides label.size if provided) |
theme |
FloppyTheme |
Default theme | Color customization object |
animation |
AnimationConfig |
{} |
Animation timing and easing configuration |
variant |
'interactive' | 'static' | 'compact' |
'interactive' |
Interaction mode |
selected |
boolean |
false |
Whether the disk is selected |
disabled |
boolean |
false |
Whether the disk is disabled |
loading |
boolean |
false |
Shows subtle pulse animation |
error |
boolean |
false |
Shows red label with white text |
onClick |
() => void |
- | Click handler |
onDoubleClick |
() => void |
- | Double-click handler |
onHover |
(isHovered: boolean) => void |
- | Hover state change handler |
onFocus |
(isFocused: boolean) => void |
- | Focus state change handler |
className |
string |
'' |
Additional CSS class |
style |
React.CSSProperties |
- | Inline styles for root element |
data-testid |
string |
- | Test ID for testing libraries |
data-disk-id |
string |
- | Custom disk ID for tracking |
badge |
React.ReactNode |
- | Badge content (top-right corner) |
children |
React.ReactNode |
- | Custom overlay content |
ariaLabel |
string |
Auto-generated | Accessible label override |
| Size | Pixels | Use Case |
|---|---|---|
tiny |
60px | Compact lists, icons |
small |
120px | Grid views, thumbnails |
medium |
200px | Featured items, cards |
large |
400px | Detail views, showcases |
hero |
600px | Landing pages, heroes |
| Custom | Any number | Specific requirements |
interface FloppyTheme {
diskColor?: string; // Main disk body color
slideColor?: string; // Metal slide color
backgroundColor?: string; // Background/cutout color
labelColor?: string; // Label paper color
labelTextColor?: string; // Label text color
enableGradient?: boolean; // Enable dynamic gradient backgrounds (default: false)
gradientType?: 'linear' | 'radial' | 'conic' | 'auto'; // Gradient type (default: 'auto')
gradientOptions?: GradientOptions; // Custom gradient options
}
interface GradientOptions {
seed?: number; // Custom seed for gradient generation
colors?: string[]; // Custom color palette (HSL or hex colors)
angle?: number; // Gradient angle for linear gradients (0-360 degrees)
}interface AnimationConfig {
hoverDuration?: number; // Hover animation duration in ms (default: 500)
slideDuration?: number; // Slide animation duration in ms (default: 500)
easing?: string; // Animation easing function (default: 'linear')
disableAnimations?: boolean; // Disable all animations (default: false)
}- Hover to slide out the metal shutter
- Click to trigger the
onClickhandler - Double-click to trigger the
onDoubleClickhandler
- No hover animations
- Click handlers still work
- Best for performance-critical lists
- Reduced label area
- Smaller slide track
- Optimized for tight spaces
// Loading state with pulse animation
<FloppyDisk loading label={{ name: "Uploading..." }} />
// Error state with red label and white text
<FloppyDisk error label={{ name: "Failed" }} />// Simple badge
<FloppyDisk
label={{ name: "New Release" }}
badge={<span style={{ background: 'red', color: 'white', padding: '2px 6px', borderRadius: '4px' }}>NEW</span>}
/>
// Custom overlay
<FloppyDisk label={{ name: "Locked" }}>
<div style={{ fontSize: '48px' }}>π</div>
</FloppyDisk>Note: The lock emoji above is just an example. For production use, consider using an icon library or SVG instead.
<FloppyDisk
label={{ name: 'Interactive' }}
onHover={(isHovered) => console.log('Hover:', isHovered)}
onFocus={(isFocused) => console.log('Focus:', isFocused)}
onClick={() => console.log('Clicked')}
onDoubleClick={() => console.log('Double-clicked')}
/>// Custom animation timing
<FloppyDisk
animation={{
hoverDuration: 1000,
easing: 'ease-in-out'
}}
/>
// Disable animations
<FloppyDisk animation={{ disableAnimations: true }} />// Using inline styles
<FloppyDisk
style={
{
'--floppy-border-radius': '10%',
'--floppy-hover-scale': '1.1',
'--floppy-shadow-blur': '10px',
} as React.CSSProperties
}
/>;
// Using CSS module classes
import { floppyDiskStyles } from 'retro-floppy';
// Access individual classes for advanced styling
<div className={floppyDiskStyles.silhouette}>...</div>;You can customize the appearance using CSS custom properties:
--floppy-size /* Disk size in px */
--floppy-border /* Border thickness */
--floppy-border-radius /* Corner radius (default: 3%) */
--floppy-color /* Main disk color */
--floppy-highlight /* Highlight color */
--floppy-shadow /* Shadow color */
--floppy-hover-scale /* Hover scale factor (default: 1.02) */
--floppy-hover-brightness /* Hover brightness (default: 1.05) */
--animation-duration /* Animation duration (default: 500ms) */
--animation-easing /* Animation easing (default: linear) */
--slide-color /* Metal slide color */
--bg-color /* Background color */
--label-color /* Label background */
--label-text-color /* Label text color */
--label-text-shadow /* Label text shadow */When rendering many disks (50+), consider:
import { memo } from 'react';
const MemoizedFloppyDisk = memo(FloppyDisk);
// Use in your list
{
disks.map((disk) => <MemoizedFloppyDisk key={disk.id} {...disk} />);
}// Use CSS custom properties for responsive sizing
<div style={{ '--floppy-size': 'clamp(80px, 15vw, 200px)' }}>
<FloppyDisk size={120} />
</div><FloppyDisk
label={{ name: 'Important Document' }}
ariaLabel="Important Document floppy disk, double-click to open"
onClick={handleSelect}
onDoubleClick={handleOpen}
/>- The entire floppy is a focusable
figurewithrole="button". ariaLabelcontrols the screen reader label; if omitted, it falls back to thelabeltext.- Keyboard users can press Enter or Space to trigger
onClickwhenvariant !== 'static'anddisabledisfalse. - A visible focus outline appears when navigating with the keyboard.
# Install dependencies
npm install
# Build the component
npm run build
# Run the example
cd example
npm install
npm run devnpm run buildThis creates:
dist/index.cjs- CommonJS bundledist/index.esm.js- ES Module bundledist/index.d.ts- TypeScript definitionsdist/retro-floppy.css- Extracted CSS styles for the component (import this in your app)
Contributions are welcome! Please feel free to submit a Pull Request.
See CONTRIBUTING.md for development instructions and guidelines and CODE_OF_CONDUCT.md for expected behavior.
This project is licensed under the MIT License β see the LICENSE file for details.
Inspired by the iconic 3.5" floppy disk that stored our precious data in the 80s and 90s.
Made with β€οΈ by Cameron Rye