Keyboard Shortcuts
general
Save current work
Ctrl+S
Undo last action
Ctrl+Z
Redo last action
Ctrl+Y
Redo alternative
Ctrl+Shift+Z
Toggle shortcuts guide
Ctrl+/
tools
Brush tool
B
Eraser tool
E
Move tool
M
Lasso tool
L
Pen tool
P
Eyedropper tool
I
canvas
Pan canvas
Space
Zoom in
Ctrl++
Zoom out
Ctrl+-
Reset zoom
Ctrl+0
Rotate canvas
R
Toggle fullscreen
F
layers
Duplicate layer
Ctrl+J
Group layers
Ctrl+G
Lower layer opacity
[
Increase layer opacity
]
New layer
Ctrl+Shift+N
Merge layers
Ctrl+Shift+E
Keyboard Shortcuts
canvas
Pan canvas
Space
Zoom in
Ctrl++
Zoom out
Ctrl+-
Reset zoom
Ctrl+0
Rotate canvas
R
Toggle fullscreen
F
layers
Duplicate layer
Ctrl+J
Group layers
Ctrl+G
Lower layer opacity
[
Increase layer opacity
]
New layer
Ctrl+Shift+N
Merge layers
Ctrl+Shift+E
Installation
Install the following dependencies in your project:
npm install framer-motion lucide-react
Copy and paste the following code into your project.
components/ui/studio-shortcuts-guide.tsx
"use client";
import React, { useState, useMemo } from "react";
import { cn } from "@/lib/utils";
import { Command, Search, X } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
interface ShortcutItemProps {
keys: string[];
description: string;
category: string;
}
interface ShortcutCategory {
[key: string]: Array<{
keys: string[];
description: string;
}>;
}
const DEFAULT_SHORTCUTS: ShortcutCategory = {
general: [
{ keys: ["Ctrl", "S"], description: "Save current work" },
{ keys: ["Ctrl", "Z"], description: "Undo last action" },
{ keys: ["Ctrl", "Y"], description: "Redo last action" },
{ keys: ["Ctrl", "Shift", "Z"], description: "Redo alternative" },
{ keys: ["Ctrl", "/"], description: "Toggle shortcuts guide" },
],
tools: [
{ keys: ["B"], description: "Brush tool" },
{ keys: ["E"], description: "Eraser tool" },
{ keys: ["M"], description: "Move tool" },
{ keys: ["L"], description: "Lasso tool" },
{ keys: ["P"], description: "Pen tool" },
{ keys: ["I"], description: "Eyedropper tool" },
],
};
interface StudioShortcutsGuideProps {
className?: string;
/** Custom shortcuts to merge with or override default shortcuts */
shortcuts?: Partial<ShortcutCategory>;
/** Whether to use only custom shortcuts (true) or merge with defaults (false) */
customOnly?: boolean;
}
type CategoryState = string | null;
type SearchState = string;
const ShortcutItem: React.FC<ShortcutItemProps> = ({ keys, description }) => (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
className="group flex items-center justify-between rounded-lg p-2 transition-colors hover:bg-accent/50"
>
<span className="text-sm text-muted-foreground group-hover:text-foreground">
{description}
</span>
<div className="flex gap-1">
{keys.map((key, index) => (
<React.Fragment key={index}>
<motion.kbd
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="pointer-events-none inline-flex h-7 select-none items-center gap-1 rounded border border-border/40 bg-muted px-2.5 font-mono text-[12px] font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground"
>
{key}
</motion.kbd>
{index < keys.length - 1 && (
<span className="text-muted-foreground">+</span>
)}
</React.Fragment>
))}
</div>
</motion.div>
);
export function StudioShortcutsGuide({
className,
shortcuts: customShortcuts,
customOnly = false,
}: StudioShortcutsGuideProps) {
const [searchQuery, setSearchQuery] = useState<SearchState>("");
const [selectedCategory, setSelectedCategory] = useState<CategoryState>(null);
// Merge default and custom shortcuts or use only custom shortcuts
const shortcuts = useMemo(() => {
if (customOnly) {
return customShortcuts || {};
}
return {
...DEFAULT_SHORTCUTS,
...customShortcuts,
// Deep merge categories if they exist in both
...(customShortcuts &&
Object.entries(customShortcuts).reduce((acc, [category, items]) => {
if (DEFAULT_SHORTCUTS[category]) {
acc[category] = [...DEFAULT_SHORTCUTS[category], ...(items ?? [])];
} else {
acc[category] = items ?? [];
}
return acc;
}, {} as ShortcutCategory)),
};
}, [customShortcuts, customOnly]);
const filterShortcuts = (shortcuts: ShortcutCategory): ShortcutCategory => {
return Object.entries(shortcuts).reduce((acc, [category, items]) => {
if (selectedCategory && category !== selectedCategory) {
return acc;
}
const filteredItems = items.filter(
(item) =>
item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.keys.some((key) =>
key.toLowerCase().includes(searchQuery.toLowerCase()),
),
);
if (filteredItems.length > 0) {
acc[category] = filteredItems;
}
return acc;
}, {} as ShortcutCategory);
};
const filteredShortcuts = filterShortcuts(shortcuts as ShortcutCategory);
const categories = Object.keys(shortcuts);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={cn(
"rounded-md border border-border/40 bg-card text-card-foreground shadow-xs",
className,
)}
>
<div className="flex items-center justify-between border-b border-border/40 rounded-t-md bg-muted/50 px-4 py-2">
<div className="flex items-center">
<Command className="mr-2 h-4 w-4" />
<h3 className="text-sm font-semibold">Keyboard Shortcuts</h3>
</div>
<div className="flex items-center gap-2">
{categories.map((category) => (
<motion.button
key={category}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() =>
setSelectedCategory(
selectedCategory === category ? null : category,
)
}
className={cn(
"rounded-md px-2 py-1 text-xs font-medium transition-colors",
selectedCategory === category
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80",
)}
>
{category}
</motion.button>
))}
</div>
</div>
<div className="relative p-4">
<div className="mb-4 flex items-center gap-2 rounded-md border border-border/40 bg-muted/30 px-3 py-2">
<Search className="h-4 w-4 text-muted-foreground" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search shortcuts..."
className="flex-1 bg-transparent text-sm outline-hidden placeholder:text-muted-foreground"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
)}
</div>
<AnimatePresence mode="wait">
{Object.entries(filteredShortcuts).map(([category, items]) => (
<motion.div
key={category}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="mb-4 last:mb-0"
>
<h4 className="mb-2 text-sm font-medium capitalize text-foreground/70">
{category}
</h4>
<div className="space-y-1">
<AnimatePresence>
{items.map((shortcut, index) => (
<ShortcutItem
key={`${category}-${index}`}
keys={shortcut.keys}
description={shortcut.description}
category={category}
/>
))}
</AnimatePresence>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</motion.div>
);
}
Update the import paths to match your project setup.
import { StudioShortcutsGuide } from "@/components/ui/studio-shortcuts-guide";
Props
StudioShortcutsGuide
Prop name | Type | Default | Description |
---|---|---|---|
shortcuts | Partial<ShortcutCategory> | - | Custom shortcuts to merge with or override default shortcuts |
customOnly | boolean | false | Whether to use only custom shortcuts or merge with defaults |
className | string | - | Additional CSS classes |
Examples
Default Usage
import { StudioShortcutsGuide } from "@/components/ui/studio-shortcuts-guide";
export default function ShortcutsExample() {
return <StudioShortcutsGuide className="w-[500px]" />;
}
With Custom Shortcuts
const customShortcuts = {
typescript: [
{ keys: ["Ctrl", "Space"], description: "Trigger suggestions" },
{ keys: ["F12"], description: "Go to definition" },
],
tools: [
{ keys: ["T"], description: "Text tool" },
{ keys: ["G"], description: "Gradient tool" },
],
};
export default function CustomShortcutsExample() {
return (
<StudioShortcutsGuide className="w-[500px]" shortcuts={customShortcuts} />
);
}
Custom Shortcuts Only
export default function CustomOnlyExample() {
return (
<StudioShortcutsGuide
className="w-[500px]"
shortcuts={customShortcuts}
customOnly
/>
);
}
Features
Search and Filter
The component includes a built-in search functionality that filters shortcuts based on both key combinations and descriptions. Users can also filter shortcuts by category using the category buttons at the top.
Animations
The component uses Framer Motion for smooth animations:
- Fade and slide animations for shortcut items
- Scale animations for keyboard keys on hover
- Smooth transitions for filtering and search results
Keyboard Key Styling
Keys are displayed in a keyboard-like style with:
- Monospace font for consistent width
- Border and background for key-like appearance
- Hover effects for interactivity
- Proper spacing and "+" symbols between multiple keys
Responsive Design
The component is fully responsive and can be customized to any width using the className
prop. It maintains its layout and functionality across different screen sizes.
Types
ShortcutCategory
interface ShortcutCategory {
[key: string]: Array<{
keys: string[];
description: string;
}>;
}
This type defines the structure of shortcuts, where:
- Each category is a key in the object
- Each category contains an array of shortcuts
- Each shortcut has a
keys
array and adescription