A Figma plugin that creates dynamic backgrounds, textures, and animated fills using real-time GLSL shaders with WebGL rendering.
✨ Real-time Shader Preview - See your GLSL shaders render live with WebGL
🤖 AI Generation - Generate shaders from text descriptions using Google Gemini models
🏗️ Layer Builder - Visually compose shaders using stackable effects (gradients, noise, shapes) without coding
🎨 Dynamic Uniforms - Create float sliders and vec3/vec4 color pickers on the fly
📝 Advanced Code Editor - Edit shaders with GLSL syntax highlighting (Ace Editor)
🎭 Preset Library - Browse and load curated shader presets by category
💾 Save & Load Shaders - Persistent shader storage in Figma documents
⏸️ Animation Controls - Pause/play shader animations
🎬 Video Export - Export shaders as 1080p WebM videos (normal/bounce modes)
🔧 Zero Configuration - No GLSL knowledge required to start
📦 Export to Figma - Apply shader output as image fills to rectangles
Download and install Node.js (includes NPM):
https://nodejs.org/en/download/
-
Clone this repository
-
Install dependencies:
npm install
-
Build the plugin:
npm run build
For active development with auto-recompile:
npm run build:watchThen in Figma:
- Plugins → Development → Import plugin from manifest
- Select
manifest.jsonfrom this directory - Make changes to any file in
src/ - Plugin auto-recompiles (watch terminal for "Compiled successfully")
- Reload plugin in Figma: Cmd/Ctrl+Option/Alt+P → Rerun plugin
- Run the plugin in Figma
- Adjust uniforms using the sliders:
uSpeed: Animation speeduLineCount: Number of wave linesuAmplitude: Wave heightuYOffset: Vertical offset
- Click "Create Rectangle" to apply shader as image fill
- Click the + button in the Parameters panel
- Choose uniform type:
- float: Slider control (min/max/step)
- color (RGB/RGBA): vec3/vec4 color picker with alpha
- Configure properties:
- Name: GLSL identifier (e.g.,
uMyParam) - Min/Max/Step: Value range (float only)
- Initial Value: Starting value or color
- Name: GLSL identifier (e.g.,
- Click Add - uniform auto-injected into shader
Save a shader:
- Click 💾 Save button in Parameters panel
- Enter name and optional description
- Optionally include thumbnail (auto-captured from canvas)
- Shader saved to Figma document storage
Load a saved shader:
- Click Advanced Editor ▼ → My Shaders
- Browse saved shaders (sort by name/newest/oldest)
- Click card to load shader with all uniforms
- Delete unwanted shaders with × button
- Click Advanced Editor ▼ → Presets
- Filter by category: All / Waves / Noise / Patterns / Effects
- Click preset card to load shader
- Modify uniforms and shader code as needed
Create complex effects by stacking layers without writing code.
- Switch the toggle at the top from Code to Builder.
- Click + Add Layer to open the effect picker.
- Choose an effect (e.g., "Gradient", "Perlin Noise", "Circle").
- Drag and drop layers to reorder them (top layers blend over bottom ones).
- Adjust layer properties:
- Blend Mode: Screen, Overlay, Multiply, etc.
- Opacity: Transparency of the layer.
- Effect Parameters: Specific controls for the effect (Scale, Color, Speed).
- The shader code is automatically generated in the background.
Generate custom GLSL shaders using Google's Gemini AI models.
- Click the ✨ AI button in the top toolbar.
- Enter your Google Gemini API Key.
- Note: API keys are stored in memory only and are discarded when the plugin closes for security.
- Select a model:
- Gemini 2.5 Flash: Fast and efficient.
- Gemini 2.5 Pro: Balanced performance.
- Gemini 3 Pro (Preview): Most capable model.
- Describe the shader you want (e.g., "A cyberpunk neon grid with scanlines").
- Click Generate. The AI will write the GLSL code and set up dynamic uniforms automatically.
- Click "Advanced Editor" button
- Edit GLSL fragment shader code with syntax highlighting
- Click "Apply Shader" to recompile
- Errors display in dismissible popup overlay
This plugin uses a two-process architecture with React 19.2 and Tailwind CSS v4:
- Plugin Sandbox (
src/plugin/controller.ts): Figma API access, runs in restricted environment - UI Iframe (
src/app/): React app with WebGL rendering, runs in browser context
Communication via postMessage between contexts.
The codebase follows clean separation of concerns:
- App.tsx (~610 lines): Component composition and layout (includes Layer Builder & AI state)
- Custom Hooks (
hooks/): Stateful logic, lifecycle management, WebGL rendering engine - Handler Factories (
handlers/): Business logic with dependency injection for testability - Components (
components/): UI rendering only, including modular color picker - Utilities (
utils/): Pure functions with no side effects - File Size Guideline: 550-line maximum enforced across all modules
- React Hooks:
useStatefor UI state,useReffor WebGL context - Custom Hooks:
useShaderEngine(WebGL rendering),useShaderLifecycle(effects),useSyncedRef(ref syncing) - Handler Factories: Dependency injection pattern for uniform CRUD, shader loading, Figma API, modals, video export
- Dynamic Uniforms: Array of configurable uniforms with CRUD operations
- Shader Storage: Saved to Figma document via
figma.root.getPluginData()/setPluginData()
Ref Synchronization Pattern:
const paramsRef = useSyncedRef(params); // Always has current value
// Animation loop reads from paramsRef.currentDynamic Uniform Injection:
- Uniforms auto-declared in shader via
buildFragmentSource() - Inserted after
precision mediump float;statement - Supports
float,vec3,vec4types
Message Protocol:
create-rectangle→render-shader→shader-rendered→ Apply image fillsave-shader/load-shaders/delete-shaderfor storage operations
- React 19.2 - UI framework with hooks
- TypeScript 5.3 - Type-safe development
- Tailwind CSS v4.1 - Utility-first styling with CSS-first config
- Webpack 4 - Module bundler with PostCSS integration
- WebGL - Real-time shader rendering
- Ace Editor - GLSL syntax highlighting
src/
├── app/
│ ├── components/ # React components
│ │ ├── color-picker/ # Modular color picker (6 files)
│ │ ├── layers/ # Layer Builder components
│ │ │ ├── LayerPanel.tsx # Main layer list interface
│ │ │ ├── LayerProperties.tsx # Layer settings
│ │ │ └── EffectPicker.tsx # Effect selection modal
│ │ ├── video-export/ # Video export utilities
│ │ ├── ControlPanel.tsx # Dynamic uniform controls sidebar
│ │ ├── ShaderCanvas.tsx # WebGL canvas with pause/play
│ │ ├── ShaderModal.tsx # Advanced editor (Ace Editor)
│ │ ├── SliderControl.tsx # Reusable slider with delete
│ │ ├── ColorControl.tsx # vec3/vec4 color picker (100 lines)
│ │ ├── UniformConfigModal.tsx # Add new uniform dialog
│ │ ├── BaseModal.tsx # Reusable modal wrapper
│ │ ├── PresetGallery.tsx # Preset shader browser
│ │ ├── PresetCard.tsx # Individual preset card
│ │ ├── SaveShaderModal.tsx # Save shader dialog
│ │ ├── SavedShadersGallery.tsx # Saved shader browser
│ │ ├── VideoExportModal.tsx # Video export settings dialog
│ │ └── [Icon components] # Plus, Delete, Save, Edit, Video, etc.
│ ├── hooks/ # Custom React hooks
│ │ ├── useShaderEngine.ts # WebGL rendering engine (175 lines)
│ │ ├── useShaderLifecycle.ts # Lifecycle management (189 lines)
│ │ ├── useSyncedRef.ts # Ref syncing utility (30 lines)
│ │ └── index.ts # Hook exports
│ ├── handlers/ # Business logic factories
│ │ ├── uniformHandlers.ts # Uniform CRUD (60 lines)
│ │ ├── layerHandlers.ts # Layer CRUD (80 lines)
│ │ ├── shaderLoadHandlers.ts # Preset/shader loading (70 lines)
│ │ ├── figmaHandlers.ts # Figma API (50 lines)
│ │ ├── modalHandlers.ts # Modal operations (50 lines)
│ │ ├── videoExportHandler.ts # Video export (50 lines)
│ │ └── index.ts # Handler exports
│ ├── utils/ # Pure utility functions
│ │ ├── shaderUtils.ts # Shader utilities (110 lines)
│ │ └── layerShaderGenerator.ts # Layer composition logic (175 lines)
│ ├── generated/ # Auto-generated files
│ │ ├── preset-thumbnails.ts # Preset thumbnail data
│ │ └── preset-thumbnails.json # Thumbnail source
│ ├── App.tsx # Component composition ONLY (294 lines)
│ ├── webgl.ts # WebGL/shader rendering logic (240 lines)
│ ├── shaders.ts # GLSL shader source constants
│ ├── presets.ts # Preset shader definitions
│ ├── layerTemplates.tsx # Layer effect definitions
│ ├── types.ts # TypeScript type definitions
│ ├── constants.ts # App-wide constants
│ ├── styles.css # Tailwind CSS v4 with Figma dark theme
│ ├── index.tsx # React entry point
│ └── index.html # HTML template
└── plugin/
└── controller.ts # Figma plugin controller (350 lines)
change logs/ # Refactoring documentation
├── APP_REFACTORING_2025-11.md
├── BUILD_SYSTEM.md
├── REACT_MIGRATION.md
└── REFACTORING_SUMMARY.md
dist/ # Build output (auto-generated)
├── code.js # Compiled plugin (~7 KB)
└── ui.html # Compiled UI with inlined JS (~1.5 MB)
npm run build # Production build (minified)
npm run build:watch # Development build with auto-recompile
npm run watch # Alias for build:watch
npm run lint # Check code for errors
npm run lint:fix # Auto-fix linting issues
npm run fmt # Format code with PrettierUniforms are automatically injected into your shader:
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;
uniform float uSpeed; // Auto-injected
uniform float uLineCount; // Auto-injected
uniform float uAmplitude; // Auto-injected
void main() {
// Your shader code here
// Access uniforms directly
}initWebGL()- Initialize WebGL context and compile shadersbuildFragmentSource()- Inject dynamic uniforms into shader sourceinjectUniforms()- Low-level uniform injection (base uniforms excluded)stripInjectedUniforms()- Remove auto-injected uniforms from coderenderShader()- Render single frame with current uniform values (supports float/vec3/vec4)recompileShader()- Hot-reload shader with new codecaptureShaderAsImage()- Capture canvas as PNG for Figma
type UniformType = "float" | "vec3" | "vec4";
type UniformValue = number | [number, number, number] | [number, number, number, number];
interface DynamicUniform {
id: string;
name: string;
type: UniformType;
value: UniformValue;
min: number;
max: number;
step: number;
}useShaderEngine - WebGL rendering engine (175 lines):
const { getCurrentTime, renderLoop, captureShader, handleRecompileShader } = useShaderEngine({
canvasRef, shaderStateRef, paramsRef, dynamicUniformsRef, customFragmentShaderRef
});useShaderLifecycle - Component lifecycle management (189 lines):
- WebGL initialization on mount
- Figma postMessage event handling
- Shader recompilation on uniform changes
- Cleanup on unmount
useSyncedRef - Keep ref in sync with state value (30 lines):
const countRef = useSyncedRef(count);
// countRef.current always has latest count, even in callbacksAll handlers use dependency injection for testability:
const { addUniform, updateUniform, removeUniform } = createUniformHandlers(
dynamicUniforms, setDynamicUniforms, setOpenModal, setCriticalError
);
const { loadPreset, loadSavedShader } = createShaderLoadHandlers(
setDynamicUniforms, customFragmentShaderRef, handleRecompileShader, ...
);
const { handleApplyToSelection, handleCreateRectangle } = createFigmaHandlers(
captureShader, pausedTimeRef, setPausedTime, params, ...
);
const { handleExportVideo } = createVideoExportHandler(
setIsExportingVideo, setCriticalError, captureShader, ...
);Official Figma plugin documentation:
https://www.figma.com/plugin-docs/
figma.createRectangle()- Create rectangle nodesfigma.createImage()- Convert image data to Figma imagefigma.ui.postMessage()- Send messages to UIfigma.closePlugin()- Close plugin window
Located in manifest.json:
- ID:
1570153150483583043 - Name: Shader Studio
- Network Access: None (all assets inline)
- Capabilities: Basic rectangle and image creation
- Ensure Node.js v16+ is installed
- Delete
node_modules/anddist/, then runnpm install - Check for TypeScript errors:
npm run lint
- Check browser console (right-click plugin → Inspect)
- Errors display in red popup overlay
- Look for
[functionName]prefixed console logs
- Verify
manifest.jsonis in root directory - Rebuild plugin:
npm run build - Check
dist/code.jsanddist/ui.htmlexist
Run npm run build:watch and reload plugin after each compile.
- Right-click plugin window → Inspect for DevTools
- Console shows WebGL errors, shader compilation issues
- Inline source maps enabled in development builds
- Current: ~1.5 MB (includes React + Tailwind + Ace Editor + react-colorful)
- Plugin code: ~4 KB (controller only)
- Performance warnings disabled in
webpack.config.js
Recent Refactorings (November 2025):
- ✅ Major architectural refactoring: App.tsx reduced from 640 to 294 lines (54% reduction)
- ✅ Modular architecture: Extracted business logic into 7 modules (hooks + handlers)
- ✅ Custom hooks pattern: useShaderEngine (175 lines), useShaderLifecycle (189 lines)
- ✅ Handler factories: Dependency injection for uniform CRUD, shader loading, Figma API, modals, video export
- ✅ Color picker modularization: Refactored into 6 separate files with HSV state management
- ✅ Video export feature: 1080p WebM export with normal/bounce playback modes
- ✅ File size guideline: 550-line maximum enforced across all modules
- ✅ App.tsx composition-only: No inline functions, pure component layout
See ROADMAP.md for detailed plans. Recently completed:
✅ Multi-type uniforms - vec3/vec4 color pickers with RGB/RGBA support
✅ Preset library - Curated shaders organized by category
✅ Save/load shaders - Persistent storage in Figma documents
✅ Thumbnail generation - Auto-capture shader previews
✅ Video export - 1080p WebM videos with normal/bounce modes
✅ Modular architecture - 54% reduction in App.tsx complexity
Planned features:
- 🖼️ Texture support (sampler2D uniforms)
- 🎵 Audio reactivity
- 🤖 AI shader generation
- 📤 HTML export for web embedding
- 🎨 Gradient editor for color uniforms
This project uses:
- ESLint for code linting
- Prettier for code formatting
- TypeScript strict mode
Format code before committing:
npm run lint:fix
npm run fmtMIT License - see LICENSE file for details.
Copyright (c) 2025 iSandRocks
Free to use, modify, and distribute. Commercial use allowed.
Built with ❤️ for the Figma community