Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Recipes – Vev Developer
Skip to content

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;