Writing code components for use with Plasmic

Often, you can just directly register your usual code components as-is for use with Plasmic. This works great for any design systems components or presentational components you already have.

But if you are tailoring components explicitly to be used with Plasmic, you can make them better integrated with the Studio by using these additional APIs or following these tips.

Allow positioning and styling via className prop

If your code component renders some DOM elements, then you should allow your Studio users to have some control over its styling — for example, setting its width and height, its alignment in a flex container, etc.

To do so, your component should expose a className prop that is passed onto the root DOM element:

Copy
function MyComponent({ className }: { className?: string }) {
return <div className={className}>{/* ... other stuff goes here */}</div>;
}

This only styles the root element (instead of any arbitrary element in your code component). Usually that makes sense — your component should already come with its own styles and layout, and usually you only want to allow positioning-related styles to be set on the root element.

Detect if your component is rendering in Plasmic Studio

You might want to disable some functionality inside the component when it is used inside Plasmic Studio artboards — for example, disable slow scroll animations, videos, or certain compute-heavy effects.

For that reason, we provide a React context PlasmicCanvasContext whose value is true only when the component is rendering in the Editor.

components/MyComponent.tsx
Copy
import { PlasmicCanvasContext } from '@plasmicapp/loader-nextjs';
import { SomeComponent }
function MyComponentWrapper() {
const inEditor = useContext(PlasmicCanvasContext);
return <MyComponent animated={!inEditor} />;
}

Sometimes, it makes sense to even expose a prop for your component that Plasmic users can toggle to turn certain features on and off:

components/MyComponent.tsx
Copy
import { PlasmicCanvasContext } from '@plasmicapp/loader-nextjs';
function MyComponentWrapper(props: {showAnimation: boolean}) {
const inEditor = useContext(PlasmicCanvasContext);
// Show animation if not used in Plasmic, or if Plasmic user explicitly
// asks to show animation
return <MyComponent animated={!inEditor || props.showAnimation} />;
}

Communicate between components via React Contexts

Your code components can communicate with each other through React Contexts. One common pattern is for one code component to fetch and provide some data via a React Context, and for other code components to read from that Context and display a piece of that data.

For example, here we have a component ProductProvider that fetches and provides data for a product, and components like ProductTitle or ProductPrice that renders the product title and price:

Copy
const ProductContext = React.createContext<Product | undefined>(undefined);
function ProductProvider({ slug, children }: { slug: string; children: ReactNode }) {
const data = useFetchProductData(props.slug);
return <ProductContext.Provider value={data}>{children}</ProductContext.Provider>;
}
function ProductTitle({ className }: { className?: string }) {
const product = React.useContext(ProductContext);
// Use a default string in case this component is used outside of ProductContext
const title = product?.title ?? 'Product Title';
return <div className={className}>{title}</div>;
}
function ProductPrice({ className }: { className?: string }) {
const product = React.useContext(ProductContext);
const price = product?.price ?? 100;
return <div className={className}>{formatCurrency(price)}</div>;
}

Repeating slot content for each element of a list

One important use of code components is for fetching and using dynamic data from your own data store. Often, you will fetch a collection of something, and would like to repeat the slot content once for every element in the collection. For example, you might have fetched a list of products, and want to render something once per product.

To do this, use the repeatedElement() function. This ensures an editing experience in Plasmic Studio where the user can select and make edits just the first replica of the slot contents.

components/ProductCollection.tsx
Copy
import { repeatedElement } from '@plasmicapp/loader-nextjs';
import { ProductContext } from './ProductContext.tsx';
function ProductCollection({
collectionSlug,
children,
}: {
children?: ReactNode;
collectionSlug?: string;
}) {
const data = useFetchProductCollection(collectionSlug);
return (
<>
// Repeat whatever is in the "children" slot, once for every product.
// But wrap the slot content in a ProductContext.Provider, so that
// the slot content can look up what product it is for by reading
// from ProductContext.
{data?.productList.map((productData, i) => (
<ProductContext.Provider value={productData} key={productData.id}>
{repeatedElement(i, children)}
</ProductContext.Provider>
))}
</>
);
}

The repeatedElement function expects two parameters: a boolean isPrimary and a React node elt:

  • isPrimary: Indicates the primary copy to be edited in the editor (usually the first copy). Should be true for only one of the copies.
  • elt: The element to be cloned. If the value provided is not a valid ReactElement, the original value is returned. Otherwise, React.cloneElement is used to clone the element, applying required tweaks to elements from the Editor.

Learn more about incorporating dynamic data into your Plasmic designs.

Applying global themes / contexts to code components

Since your code components are rendered on your host page, it will have access to whatever CSS or React contexts that exist on the host page. So if your code components require contexts to work, simply make sure they are provided when rendering PlamsicCanvasHost:

Copy
<ThemeContext.Provider>
<PlasmicCanvasHost />
</ThemeContext.Provider>

Any component instances used in your Plasmic designs will have access to the context as per usual.

Custom behaviors (attachments)

You may want your code components to be used as wrapper components (like animations, effects, behaviors, etc). To do that, you can register them as “attachments”, and they will be available on the right panel of Studio, in the Custom behaviors section.

Notice that these components can have any number of props, but can only have a single slot prop called “children”.

For example,

Copy
// Registering MyAnimation as an attachment
registerComponent(MyAnimation, {
name: 'My Animation',
isAttachment: true,
props: {
maxAngleX: 'number',
maxAngleY: 'number',
color: 'string',
children: 'slot'
}
});

See our custom behaviors page for more information.

Applying Plasmic styles to parts of your component

Normally, Plasmic isolates its styles from your code.

However, you can choose to apply Plasmic’s styles—in particular its default styles for text, h1, etc.—to your codebase by taking a themeResetClass and applying this to the element(s) you want.

One situation where this is useful is when you’re trying to render some generic HTML content that came from Markdown, a CMS, etc., but with styles defined in Plasmic.

Copy
function Modal({ themeResetClass, className, modalContent }) {
const [show, setShow] = React.useState(false);
return (
<>
<button className={className} onClick={() => setShow(true)}>
Show
</button>
{ReactDOM.createPortal(<div className={`modal ${themeResetClass}`}>{modalContent}</div>, document.body)}
</>
);
}
registerComponent(Modal, {
name: 'Modal',
props: {
modalContent: 'slot',
themeResetClass: {
type: 'themeResetClass'
}
}
});

Viewport height elements

If you are creating a component that uses a full screen height element, then these won’t work with design mode artboards, since these artboards grow their own height dynamically to fit the size of the page—yet the size of the page will be pushed taller since this one element in the page now thinks it will need to grow into the available space, and so on forever.

You can either:

  • Stick to normal editing mode instead of using design mode
  • Or, you can define a CSS custom property, --viewport-height: 100vh in your :root, and use var(--viewport-height) instead of 100vh in your designs. In the canvas artboard, Plasmic will override --viewport-height to the appropriate (configurable) fixed height.
  • Or, otherwise detect when you are within the Plasmic canvas (using usePlasmicCanvasContext()) and render something that is not.

Note: vh elements are not the only issue. It’s also possible to have, say, an element near the root of the page that is set to height: 100%.

Data-fetching code components

Continue reading onto the next section to learn how to fetch data from your own data backends in a way that works with SSR/SSG.

Was this page helpful?

Join 3000+ Plasmic users on Slack