Component

Word Cloud

A word cloud is a visual representation of text data. It displays a list of words each with a random font size. Then it displays the reveal text.

Demo

Words

word 1word 2word 3word 4word 5word 6word 7word 8word 9word 10

Source Code

import { useMeasuredSize } from 'https://framer.com/m/framer/useMeasuredSize.js'
import { ControlType, addPropertyControls, motion } from 'framer'
import { useEffect, useRef, useState } from 'react'
 
interface WordFrequency {
  word: string
  frequency: number
}
 
const randomBetween = (min: number, max: number) => {
  return Math.floor(Math.random() * (max - min + 1) + min)
}
 
const getFrequency = () => randomBetween(30, 100)
 
const words: WordFrequency[] = [
  { word: 'word 1', frequency: getFrequency() },
  { word: 'word 2', frequency: getFrequency() },
  { word: 'word 3', frequency: getFrequency() },
  { word: 'word 4', frequency: getFrequency() },
  { word: 'word 5', frequency: getFrequency() },
  { word: 'word 6', frequency: getFrequency() },
  { word: 'word 7', frequency: getFrequency() },
  { word: 'word 8', frequency: getFrequency() },
  { word: 'word 9', frequency: getFrequency() },
  { word: 'word 10', frequency: getFrequency() },
]
 
interface WordCloudProps {
  width?: number
  height?: number
  minFontSize: number
  maxFontSize: number
  text?: string
  color: string
  backgroundColor?: string
  speedInSeconds: number
  font?: string
  border?: {
    borderWidth: number
    borderStyle: string
    borderColor: string
  }
  radius?: number
}
 
WordCloud.defaultProps = {
  width: 500,
  height: 500,
  minFontSize: 60,
  maxFontSize: 20,
  backgroundColor: 'transparent',
  text: 'Words',
  color: 'hsl(var(--foreground))',
  speedInSeconds: 3,
  font: undefined,
  border: {
    borderWidth: 2,
    borderStyle: 'solid', // solid, dashed, dotted or double
    borderColor: 'hsl(var(--foreground))',
  },
  radius: 0,
}
 
addPropertyControls(WordCloud, {
  text: {
    type: ControlType.String,
    defaultValue: 'Hey',
    placeholder: 'Type something…',
  },
  color: {
    type: ControlType.Color,
    defaultValue: 'hsl(var(--foreground))',
    optional: true,
  },
  backgroundColor: {
    type: ControlType.Color,
    defaultValue: 'hsl(var(--background))',
    optional: true,
  },
  font: {
    type: ControlType.String,
    defaultValue: WordCloud.defaultProps.font,
  },
  border: {
    type: ControlType.Border,
    defaultValue: {
      borderWidth: 2,
      borderStyle: 'solid', // solid, dashed, dotted or double
      borderColor: 'rgba(250, 250, 250, 1)',
    },
  },
  speedInSeconds: {
    type: ControlType.Number,
    defaultValue: 3,
    step: 1,
    min: 1,
    max: 30,
    displayStepper: true,
  },
  radius: {
    type: ControlType.Number,
    defaultValue: 0,
    step: 1,
    min: 0,
    displayStepper: true,
  },
  maxFontSize: {
    type: ControlType.Number,
    defaultValue: 20,
    step: 1,
    min: 1,
    max: 1000,
    displayStepper: true,
  },
  minFontSize: {
    type: ControlType.Number,
    defaultValue: 100,
    step: 1,
    min: 1,
    max: 1000,
    displayStepper: true,
  },
})
 
/**
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight any
 */
export default function WordCloud(props: WordCloudProps) {
  const container = useRef<HTMLDivElement>(null)
  const size = useMeasuredSize()
  const width = size?.width ?? WordCloud.defaultProps.width
  const height = size?.height ?? WordCloud.defaultProps.height
 
  const maxFrequency = Math.max(...words.map((w) => w.frequency))
  const minFrequency = Math.min(...words.map((w) => w.frequency))
  const [messy, setMessy] = useState(true)
 
  const containerStyle: React.CSSProperties = {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
    backgroundColor: props.backgroundColor,
    position: 'relative',
    width: '100%',
    height: '100%',
    borderRadius: props.radius,
    transition: 'opacity 0.3s ease-in-out',
    ...(!!props?.border && props?.border),
  }
 
  const wordTransition = {
    transition: 'all 0.5s ease-in-out',
  }
 
  const wordStyle = (fontSize: number, color: string, opacity: number, top: string, left: string): React.CSSProperties => ({
    position: 'absolute',
    display: 'inline-block',
    fontSize: `${fontSize}px`,
    color,
    opacity,
    top,
    left,
    ...wordTransition,
  })
 
  const titleStyle: React.CSSProperties = {
    fontSize: '2.5rem',
    display: 'flex',
    opacity: messy ? 0 : 1,
    justifyContent: 'center',
    alignItems: 'center',
    color: props.color,
    ...wordTransition,
  }
 
  const getFontSize = (frequency: number) => {
    if (maxFrequency === minFrequency) {
      return props.minFontSize
    }
 
    return props.minFontSize + ((frequency - minFrequency) / (maxFrequency - minFrequency)) * (props.maxFontSize - props.minFontSize)
  }
 
  const getRandomPosition = (fontSize: number) => {
    const maxTop = height - fontSize
    const maxLeft = width - fontSize * (words.length / 2)
    return {
      top: `${randomBetween(0, maxTop)}px`,
      left: `${randomBetween(0, maxLeft)}px`,
    }
  }
 
  useEffect(() => {
    const interval = setInterval(() => {
      setMessy((prev) => !prev)
    }, props.speedInSeconds * 1000)
    return () => clearInterval(interval)
  }, [props.speedInSeconds])
 
  return (
    <motion.div ref={container} style={containerStyle}>
      <p style={titleStyle}>{props.text}</p>
      {words.map((word, index) => {
        const fontSize = getFontSize(word.frequency)
        const color = props.color // Keeps the user-defined color
        const { top, left } = getRandomPosition(fontSize)
        return (
          <span key={index} style={wordStyle(fontSize, color, messy ? 1 : 0, top, left)}>
            {word.word}
          </span>
        )
      })}
    </motion.div>
  )
}