Branded UI elements and their interactions with Cohere
April 8th, ‘24
Last updated April 12th, ‘24751 words
Branding tends to take place only on the illustration, and theme level for a website. A while back I came across one of my favorite takes on branding at Cohere . I love the concept of leaning in to your branding. Going as far as bespoke UI elements and interactions.
I mean, just look at this site. Everywhere you look is giving this rounded stone vibe. The main navigation and custom font are nice touches.
Double Clicking on the Primary CTA
Specifically, their primary CTA is a buttery hover animation using SVG for the border radii. Very clickable and delightful. With a slight bump animation, your attention is drawn to the craft. Ugh, so good.
Breaking down how they accomplished this is quite straightforward. Cohere's website is built with Tailwind CSS which means this can be built with only CSS in the form of the group-hover
Tailwind modifier. Breaking down the component and rebuilding, this is what I came up with:
Primary button animation
If you're curious to peek behind the curtain at the code for this button, I've added the source below, highlighting the two key lines that make up the hover effect.
CohereArrowButton
1export interface CohereButtonProps2extends DetailedHTMLProps<3ButtonHTMLAttributes<HTMLButtonElement>,4HTMLButtonElement5> { }67export const CohereArrowButton = ({8children,9className,10...props11}: CohereButtonProps) => {12return (13<button14className={clsx(15"sm:w-72 focus:outline-none disabled:cursor-not-allowed group relative inline-flex items-center transition-all duration-200 ease-in-out",16className17)}18{...props}19>20<div className='h-10 flex grow'>21<div className='bg-cohere-volcanic text-cohere-marble h-10 truncate flex grow justify-start items-center px-2 text-xs font-sans font-medium uppercase rounded-l-md'>22Contact Sales23</div>24<RightBottomCollapsedSvg />25</div>2627<div className='h-10 flex -ml-1 items-center'>28<LeftTopCollapsedSvg />29<div className='bg-cohere-volcanic text-cohere-marble h-10 truncate flex items-center pl-0 pr-2 group-hover:pr-4 group-hover:pl-1 transition-all duration-200 ease-in-out rounded-r-md'>30<CohereArrowIcon className='stroke-cohere-marble' />31</div>32</div>33</button>34);35};
tsx
Re-imagining the Secondary CTA
But I really got stuck on the secondary CTA button. It comes across as a basic button initially, then on hover jumps to a branded button. As a group of buttons, I'd make the same decision for the default state of the button being squared off. However the transition between basic button and branded button is BEGGING to be animated.
Let's take a crack at it. This one is not as simple since HTML doesn't currently have the ability to arbitrarily modify points. Inspecting the existing build also has an SVG which gives us a a hint in the right direction.
Sidebar: My animation tooling of choice foundationally is basic CSS then when I need a bit more power all the way to the most complex of web animations is Framer Motion .
Framer Motion has a minimal API that allows us to add a motion.
prefix to HTML. Framer Motion says "HTML & SVG components, optimised for use with gestures and animation. These can be used as drop-in replacements for any HTML & SVG component, all CSS & SVG properties are supported."
Additionally though, clean interpolation between SVG shapes is not a given. We'll use an NPM package called Flubber and create a custom hook.
If I were working with Cohere on this project, this is how I would have built the secondary CTA button:
Secondary button animation
Picking out the interesting bits of code here, first we need to create the custom hook with:
useTransform
from Framer Motion allows you to create a newMotionValue
that updates by transforming the output of otherMotionValue
(s). For example, you can make an opacityMotionValue
that maps a range ofx
position values to a range of opacity values, so whenx
is 0, opacity is 1, and whenx
is 100, opacity is 0. It ensures output values transition smoothly according to specified easing functions or in this case, complex transitions like SVG path morphing.- Flubber's
interpolate
function is taking in path strings and an options object. Relevant to our implementation here,maxSegmentLength
will determine how smooth your animation is although it is at the expense of performance so use mindfully.
useFlubber
1import { useTransform, } from 'framer-motion';2import { interpolate } from "flubber";34export function useFlubber(progress: MotionValue<number>, paths: string[]) {5return useTransform(progress, paths.map((_, index: number) => index), paths, {6mixer: (a, b) => interpolate(a, b, { maxSegmentLength: 0.1 }),7});8}
tsx
Digging into the Secondary CTA button directly now — you'll notice the meat of this component are the helper functions and <motion.path ... />
. Which allows for the dynamic path variable. In the handlers, we're also using animate
from Framer Motion and defining the transition parameters to match those of the Primary CTA button.
CohereButton
1const squaredPath =2"M11 0.5H-1V39.5H11C14.3137 39.5 17 36.8137 17 33.5V6.5C17 3.18629 14.3137 0.5 11 0.5Z";3const bentPath =4"M7.97868 0.5H-1V39.5H2.34436C5.0841 39.5 7.47586 37.6441 8.15635 34.9902L14.7593 9.23863C15.8948 4.81046 12.5501 0.5 7.97868 0.5Z";56export const CohereButton = ({7children,8className,9...props10}: CohereButtonProps) => {11const progress = useMotionValue(0);12const path = useFlubber(progress, [squaredPath, bentPath]);1314const handleMouseEnter = useCallback(() => {15animate(progress, 1, { duration: 0.2, ease: "easeInOut" });16}, [progress]);1718const handleMouseLeave = useCallback(() => {19animate(progress, 0, { duration: 0.2, ease: "easeInOut" });20}, [progress]);2122return (23<button24className='flex items-center bg-cohere-marble text-cohere-volcanic'25onMouseEnter={handleMouseEnter}26onMouseLeave={handleMouseLeave}27{...props}28>29<svg30viewBox='0 0 18 40'31fill='none'32xmlns='http://www.w3.org/2000/svg'33className='h-10 -mr-px'34>35<path36d='M19 0.5H7C3.68629 0.5 1 3.18629 1 6.5V33.5C1 36.8137 3.68629 39.5 7 39.5H19V0.5Z'37className='stroke-cohere-volcanic'38/>39</svg>4041<div42className={clsx(43"h-10 sm:w-72 border-y border-cohere-volcanic text-xs font-sans font-medium uppercase flex items-center sm:justify-start justify-center",44className45)}46>47{children}48</div>4950<svg51viewBox='0 0 18 40'52fill='none'53xmlns='http://www.w3.org/2000/svg'54className='h-10 -ml-px'55>56<motion.path d={path} className='stroke-cohere-volcanic' />57</svg>58</button>59);60};
tsx
One side note to mention — instead of using border radii on this button, I opted for both horizontal sides to be SVGs in order to keep the same visual balance of the button without hack-y code.
There are a bunch of considerations including accessibility, and user expectations that should go into whether this kind of component should also make it into a product interface. That being said, I think it is super clever and inspirational to push the boundaries like this for a marketing website. Props Cohere team, thanks for the inspiration!