Learn how to work with layers and layer types in designer.

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:

PropertyTypeDescription
typestringA unique identifier for the layer type.
namestringThe display name shown in the UI.
iconReactNodeOptional icon shown in the toolbar.
defaultValuesOmit<Layer, "id" | "type">Default values applied when a new layer is created.
render(layer) => ReactNodeFunction that renders the layer on the canvas.
keybindingKeybindingOptional 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:

src/layers/shape.tsx
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:

  • type is the unique identifier the designer uses to discriminate this layer from others.
  • name is the display name shown in the layers panel and elsewhere in the UI.
  • defaultValues is the initial state applied to a new shape layer. The cssVars define the layer's size, color, and border radius — these are exposed to render via layer.contentStyle.
  • render returns the JSX for the layer. Spreading layer.contentStyle applies the cssVars so 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:

src/layers/index.ts
import { DEFAULT_LAYER_TYPES } from "@shadcn/designer";
import { shapeLayer } from "./shape";
 
export const layerTypes = [...DEFAULT_LAYER_TYPES, shapeLayer];

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:

src/designer.d.ts
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:

src/App.tsx
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:

src/layers/shape.tsx
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:

src/layers/shape.tsx
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

  1. Unique Type: Always use a unique type identifier for your layer.
  2. Default Values: Provide sensible default values for the layer name, value, and CSS variables.
  3. Keyboard Shortcuts: Add keyboard shortcuts for better user experience.
  4. Rendering: Ensure you use the layer.contentStyle to 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.

src/actions/shape-color.tsx
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.

src/actions/shape-color.tsx
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.

src/App.tsx
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:

src/layers/shape.tsx
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:

src/layers/shape.tsx
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:

src/actions/shape-type.tsx
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:

src/App.tsx
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