Recipes
A collection of practical recipes showing how to build Vev components. Each recipe is focused and self-contained.
Hello World
The minimal component — a single editable text prop.
import React from 'react';
import { registerVevComponent } from '@vev/react';
type Props = {
text: string;
};
const HelloWorld = ({ text = 'Hello, Vev!' }: Props) => {
return <h1>{text}</h1>;
};
registerVevComponent(HelloWorld, {
name: 'Hello World',
props: [{ name: 'text', type: 'string' }],
});
export default HelloWorld;Image with caption
Using the built-in Image component with an editable caption.
import React from 'react';
import { registerVevComponent, Image } from '@vev/react';
import styles from './ImageCard.module.css';
type Props = {
image: { url: string; key: string };
caption: string;
};
const ImageCard = ({ image, caption }: Props) => {
return (
<figure className={styles.wrapper}>
<Image className={styles.image} src={image} />
{caption && <figcaption className={styles.caption}>{caption}</figcaption>}
</figure>
);
};
registerVevComponent(ImageCard, {
name: 'Image Card',
props: [
{ name: 'image', type: 'image' },
{ name: 'caption', type: 'string' },
],
});
export default ImageCard;Conditional rendering
Show or hide elements using a boolean prop.
import React from 'react';
import { registerVevComponent } from '@vev/react';
type Props = {
title: string;
showBadge: boolean;
badgeLabel: string;
};
const Badge = ({ title, showBadge, badgeLabel = 'New' }: Props) => {
return (
<div>
<h2>{title}</h2>
{showBadge && <span className="badge">{badgeLabel}</span>}
</div>
);
};
registerVevComponent(Badge, {
name: 'Badge',
props: [
{ name: 'title', type: 'string' },
{ name: 'showBadge', type: 'boolean' },
{
name: 'badgeLabel',
type: 'string',
hidden: (ctx) => !ctx.value.showBadge,
},
],
});
export default Badge;Select
A select field that shows different options depending on the selected value.
import React from 'react';
import { registerVevComponent } from '@vev/react';
type Props = {
variant: 'primary' | 'secondary' | 'ghost';
label: string;
};
const Button = ({ variant = 'primary', label = 'Click me' }: Props) => {
return <button className={`btn btn--${variant}`}>{label}</button>;
};
registerVevComponent(Button, {
name: 'Button',
props: [
{ name: 'label', type: 'string' },
{
name: 'variant',
type: 'select',
options: {
display: 'radio',
items: [
{ label: 'Primary', value: 'primary' },
{ label: 'Secondary', value: 'secondary' },
{ label: 'Ghost', value: 'ghost' },
],
},
},
],
});
export default Button;Card list
An array of objects rendered as a list of cards.
import React from 'react';
import { registerVevComponent, Image } from '@vev/react';
import styles from './CardList.module.css';
type Card = {
title: string;
description: string;
image: { url: string; key: string };
};
type Props = {
cards: Card[];
};
const CardList = ({ cards = [] }: Props) => {
return (
<ul className={styles.list}>
{cards.map((card, i) => (
<li key={i} className={styles.card}>
<Image src={card.image} />
<h3>{card.title}</h3>
<p>{card.description}</p>
</li>
))}
</ul>
);
};
registerVevComponent(CardList, {
name: 'Card List',
props: [
{
name: 'cards',
type: 'array',
of: [
{
name: 'card',
type: 'object',
fields: [
{ name: 'title', type: 'string' },
{ name: 'description', type: 'string', options: { multiline: true } },
{ name: 'image', type: 'image' },
],
},
],
},
],
});
export default CardList;Animate on scroll
Trigger an animation when the component enters the viewport using useVisible.
import React, { useRef } from 'react';
import { registerVevComponent, useVisible } from '@vev/react';
import styles from './FadeIn.module.css';
type Props = {
text: string;
};
const FadeIn = ({ text }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const isVisible = useVisible(ref);
return (
<div
ref={ref}
className={`${styles.wrapper} ${isVisible ? styles.visible : styles.hidden}`}
>
{text}
</div>
);
};
registerVevComponent(FadeIn, {
name: 'Fade In',
props: [{ name: 'text', type: 'string' }],
});
export default FadeIn;Responsive component
Render differently based on the current viewport width.
import React from 'react';
import { registerVevComponent, useViewport } from '@vev/react';
type Props = {
desktopText: string;
mobileText: string;
};
const ResponsiveBanner = ({ desktopText, mobileText }: Props) => {
const { width } = useViewport();
const isMobile = width < 768;
return (
<div className="banner">
<p>{isMobile ? mobileText : desktopText}</p>
</div>
);
};
registerVevComponent(ResponsiveBanner, {
name: 'Responsive Banner',
props: [
{ name: 'desktopText', type: 'string' },
{ name: 'mobileText', type: 'string' },
],
});
export default ResponsiveBanner;Editable CSS
Combine editableCSS with a color prop for a styled component the editor can customize.
import React from 'react';
import { registerVevComponent } from '@vev/react';
import styles from './Callout.module.css';
type Props = {
text: string;
accentColor: string;
};
const Callout = ({ text, accentColor }: Props) => {
return (
<div className={styles.callout} style={{ borderColor: accentColor }}>
<p>{text}</p>
</div>
);
};
registerVevComponent(Callout, {
name: 'Callout',
props: [
{ name: 'text', type: 'string', options: { multiline: true } },
{ name: 'accentColor', type: 'color' },
],
editableCSS: [
{ selector: styles.callout, properties: ['background', 'border-radius', 'padding'] },
],
});
export default Callout;Interactions
A modal that can be opened and closed from the Vev editor's Interactions panel.
import React, { useState } from 'react';
import { registerVevComponent, useVevEvent } from '@vev/react';
enum ModalInteraction {
open = 'open',
close = 'close',
}
type Props = {
title: string;
content: string;
};
const Modal = ({ title, content }: Props) => {
const [isOpen, setIsOpen] = useState(false);
useVevEvent(ModalInteraction.open, () => setIsOpen(true));
useVevEvent(ModalInteraction.close, () => setIsOpen(false));
if (!isOpen) return null;
return (
<div className="modal">
<h2>{title}</h2>
<p>{content}</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
);
};
registerVevComponent(Modal, {
name: 'Modal',
props: [
{ name: 'title', type: 'string' },
{ name: 'content', type: 'string', options: { multiline: true } },
],
interactions: [
{ type: ModalInteraction.open, description: 'Open modal' },
{ type: ModalInteraction.close, description: 'Close modal' },
],
});
export default Modal;Events
A countdown timer that dispatches events when it starts and when it reaches zero.
import React, { useState, useEffect } from 'react';
import { registerVevComponent, useDispatchVevEvent } from '@vev/react';
enum TimerEvent {
onStart = 'onStart',
onComplete = 'onComplete',
}
type Props = {
seconds: number;
};
const Countdown = ({ seconds = 10 }: Props) => {
const [count, setCount] = useState(seconds);
const dispatch = useDispatchVevEvent();
useEffect(() => {
setCount(seconds);
dispatch(TimerEvent.onStart);
const interval = setInterval(() => {
setCount((prev) => {
if (prev <= 1) {
clearInterval(interval);
dispatch(TimerEvent.onComplete);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [seconds]);
return <div className="countdown">{count}</div>;
};
registerVevComponent(Countdown, {
name: 'Countdown',
props: [
{
name: 'seconds',
type: 'number',
options: { min: 1, max: 300, display: 'input' },
},
],
events: [
{ type: TimerEvent.onStart, description: 'On start' },
{ type: TimerEvent.onComplete, description: 'On complete' },
],
});
export default Countdown;