The designer is built with a flexible layer system that allows you to create custom layers types, render them on the canvas and add actions to them. This guide will show you how to build and use your own layer types.
Layer Type
A layer type in designer is an object that defines the properties of a layer and how it is rendered. It accepts the following properties:
| Property | Type | Description |
|---|---|---|
type | string | A unique identifier for the layer type. |
name | string | The display name shown in the UI. |
icon | ReactNode | Optional icon shown in the toolbar. |
defaultValues | Omit<Layer, "id" | "type"> | Default values applied when a new layer is created. |
render | (layer) => ReactNode | Function that renders the layer on the canvas. |
keybinding | Keybinding | Optional keyboard shortcut to add the layer. |
Organizing Layer Types
Most projects will have more than one layer type, so it's a good idea to organize your layer types into a layers folder. Here's an example of how you can organize your layer types:
The following folder structure is a suggestion for scaling as you add more layer types:
src/
├── layers/
│ ├── index.ts # Barrel that extends DEFAULT_LAYER_TYPES.
│ ├── shape.tsx # One file per custom layer type.
│ ├── sticker.tsx
│ ├── chart.tsx
│ └── video.tsx
└── designer.d.ts # Module augmentation for type safety.Each layer lives in its own file under src/layers/. The index.ts barrel collects them into a single layerTypes array, and designer.d.ts augments the DesignerLayerTypes interface so the rest of your app gets typed Layer values.
Feel free to organize your files however you like — the designer doesn't care where layer types are defined, as long as they're passed to the Designer component.
Creating a Custom Layer
We'll create a new custom layer type called shape. It will be a circle shape that can be resized and rotated.
1. Define the layer type
Create a new file called src/layers/shape.tsx and define the layer type using createLayerType. Let's start with only the required properties: type, name, defaultValues, and render:
import { createLayerType } from "@shadcn/designer";
export const shapeLayer = createLayerType({
type: "shape",
name: "Shape",
defaultValues: {
name: "Shape",
value: "shape",
cssVars: {
"--width": "100px",
"--height": "100px",
"--background-color": "#000000",
},
},
render: (layer) => {
return (
<div
style={{
...layer.contentStyle,
}}
/>
);
},
});Here's what each property does:
typeis the unique identifier the designer uses to discriminate this layer from others.nameis the display name shown in the layers panel and elsewhere in the UI.defaultValuesis the initial state applied to a new shape layer. ThecssVarsdefine the layer's size, color, and border radius — these are exposed torendervialayer.contentStyle.renderreturns the JSX for the layer. Spreadinglayer.contentStyleapplies thecssVarsso the layer reflects user-controlled styling.
2. Extend the default layer types
Create a src/layers/index.ts barrel that extends the default layer types with your custom layer:
import { DEFAULT_LAYER_TYPES } from "@shadcn/designer";
import { shapeLayer } from "./shape";
export const layerTypes = [...DEFAULT_LAYER_TYPES, shapeLayer];Extending the default layer types ensures you can add, remove and override existing layer types easily.
3. Augment the module for type safety
Use InferLayerTypes to extract type information and augment the DesignerLayerTypes interface so that Layer, showForLayerTypes, and other APIs use your custom types:
import type { InferLayerTypes } from "@shadcn/designer";
import type { layerTypes } from "./layers";
declare module "@shadcn/designer" {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface DesignerLayerTypes extends InferLayerTypes<typeof layerTypes> {}
}The empty interface is required for module augmentation — type aliases can't be merged. The eslint-disable comment silences a false positive from the no-empty-object-type rule.
4. Pass the layer types to the Designer
Pass layerTypes to the Designer component:
import { Designer } from "@shadcn/designer";
import { layerTypes } from "./layers";
function MyDesigner() {
return (
<Designer layerTypes={layerTypes}>
<DesignerContent>
<DesignerCanvas>
<DesignerFrame />
</DesignerCanvas>
</DesignerContent>
</Designer>
);
}The designer is now aware of the shape layer and knows how to render it. But there's no way to actually add one to the canvas yet — let's wire up a keyboard shortcut and an icon.
5. Add a keyboard shortcut
Add a keybinding to let users add the layer with a keyboard shortcut. The shortcut is automatically registered and shown in the help dialog:
export const shapeLayer = createLayerType({
type: "shape",
name: "Shape",
keybinding: {
key: "s",
label: "S",
labelMac: "S",
description: "Add Shape",
group: "New Layer",
},
// ...
});Press S on your keyboard to add a new shape layer to the canvas. You should see a black square on the canvas.
6. Add an icon
Pass an icon to make the layer type appear in the <DesignerToolbar />. The icon can be any ReactNode — an inline SVG, a component from your icon library, or even an emoji:
export const shapeLayer = createLayerType({
type: "shape",
name: "Shape",
icon: (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect width="100%" height="100%" />
</svg>
),
// ...
});Click the icon in the toolbar to add a new shape layer to the canvas.
Best Practices
- Unique Type: Always use a unique
typeidentifier for your layer. - Default Values: Provide sensible default values for the layer name, value, and CSS variables.
- Keyboard Shortcuts: Add keyboard shortcuts for better user experience.
- Rendering: Ensure you use the
layer.contentStyleto style the layer.
Remember to test your layers thoroughly with the designer's transformation tools.
Custom Layer Action
Now that we have a custom layer, let's add a custom action to the layer. We'll add a custom action to change the color of the shape.
Let's create a ActionShapeColor component inside src/actions/shape-color.tsx and add the following code.
import { Action, ActionControls, ActionLabel } from "@shadcn/designer/ui";
export function ActionShapeColor() {
return (
<Action>
<ActionLabel>Color</ActionLabel>
<ActionControls>
// Control here.
</ActionControls>
</Action>
);
}To change the color of the shape, we use the CSS variable --background-color. The designer package provides two functions to help us create a custom action to change the color of the shape: createLayerCssVarAction and useLayerCssVarAction.
import {
Action,
ActionControls,
ActionLabel,
Button,
} from "@shadcn/designer/ui";
import { createLayerCssVarAction, useLayerCssVarAction } from "@shadcn/designer";
const backgroundColor = createLayerCssVarAction("--background-color", "#000000");
export function ActionShapeColor() {
const [value, setValue] = useLayerCssVarAction(backgroundColor);
return (
<Action>
<ActionLabel>Color {value}</ActionLabel>
<ActionControls>
<Button onClick={() => setValue("#ff0000")}>Red</Button>
<Button onClick={() => setValue("#00ff00")}>Green</Button>
</ActionControls>
</Action>
);
}Import the ActionShapeColor component into your src/App.tsx file and add the ActionShapeColor component to the <DesignerPane /> with the showForLayerTypes prop set to ["shape"].
We use the showForLayerTypes prop to only show the pane when shape layers are selected.
If you're following along from the Installation guide, add the code to the <PanelRight /> component in src/panel-right.tsx file.
import { Designer, DesignerContent, DesignerCanvas, DesignerFrame, DesignerPanel, DesignerPane, DesignerPaneTitle, DesignerPaneContent } from "@shadcn/designer";
import { ActionShapeColor } from "./actions/shape-color";
export default function App() {
return (
<Designer>
<DesignerContent>
<DesignerCanvas>
<DesignerFrame />
</DesignerCanvas>
<DesignerPanel>
<DesignerPane showForLayerTypes={["shape"]}>
<DesignerPaneTitle>Shape</DesignerPaneTitle>
<DesignerPaneContent>
<ActionShapeColor />
</DesignerPaneContent>
</DesignerPane>
</DesignerPanel>
</DesignerContent>
</Designer>
)
}Add a new Shape layer to the canvas by clicking on the icon in the toolbar. You should see a new Color action in the right panel. Click the Red or Green button to change the shape's color, and the current value will be reflected in the action label.
If you add more than one shape layer to the canvas, you can drag to select multiple layers and change the color of all of them at once.
Acting on the Layer Value
cssVars work well for styling, but for non-style state — like which shape variant to render — store it on layer.value instead. Let's add a Shape Type action that toggles between a square and a circle.
1. Update the value to a structured shape
Change the layer's value from a string to an object so we can store the variant:
export const shapeLayer = createLayerType({
type: "shape",
name: "Shape",
defaultValues: {
name: "Shape",
value: { type: "square" },
cssVars: {
"--width": "100px",
"--height": "100px",
"--background-color": "#000000",
},
},
// ...
});createLayerType infers value as { type: string } from the default values, so render and selectors are typed accordingly.
2. Render based on the value
Use layer.value.type to return a different SVG for each variant:
render: (layer) => {
return (
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
style={{ ...layer.contentStyle }}
>
{layer.value.type === "circle" ? (
<circle cx="50" cy="50" r="50" />
) : (
<rect width="100" height="100" />
)}
</svg>
);
},3. Create the Shape Type action
Create src/actions/shape-type.tsx. The action reads the current value.type from the selected layer and writes the new one through useSetLayersValue:
import { useSelectedLayers, useSetLayersValue } from "@shadcn/designer";
import {
Action,
ActionControls,
ActionLabel,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@shadcn/designer/ui";
export function ActionShapeType() {
const selectedLayers = useSelectedLayers();
const setLayersValue = useSetLayersValue();
const shapeLayers = selectedLayers.filter((layer) => layer.type === "shape");
const layer = shapeLayers[0];
// If no shape layer is selected, return null.
if (!layer) {
return null;
}
return (
<Action>
<ActionLabel>Type</ActionLabel>
<ActionControls>
<Select
value={layer.value.type}
onValueChange={(value) =>
setLayersValue(shapeLayers, { type: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="square">Square</SelectItem>
<SelectItem value="circle">Circle</SelectItem>
</SelectContent>
</Select>
</ActionControls>
</Action>
);
}Filtering to shapeLayers narrows layer.value to the shape's value type and
also gives setLayersValue the right value type for the selected shape layers —
this is the payoff of the module augmentation from earlier.
4. Add the action to the pane
Add <ActionShapeType /> next to <ActionShapeColor /> in the shape pane:
import { ActionShapeColor } from "./actions/shape-color";
import { ActionShapeType } from "./actions/shape-type";
<DesignerPane showForLayerTypes={["shape"]}>
<DesignerPaneTitle>Shape</DesignerPaneTitle>
<DesignerPaneContent>
<ActionShapeType />
<ActionShapeColor />
</DesignerPaneContent>
</DesignerPane>Switch the dropdown between Square and Circle — the canvas re-renders the matching SVG using the same cssVars for size and color.
Next Steps
- See the Custom Layers section for more examples of custom layers with complex actions and controls.
- Learn how to use the theme system to customize the designer's appearance.
- Learn how to use the unit system.
- Learn more about the designer components and hooks.