How to Make Duolingo's 3D Button in Rive (No Code)

You know this button. The chunky green one you've pressed a thousand times in Duolingo.
Here's what most people don't know: Duolingo builds its animations in Rive. The characters, the reactions, the lip sync, all of it runs on the exact tool we're about to use. So this isn't a lookalike exercise in some random app. It's the real production pipeline.
In this tutorial we'll build that button from scratch: the 3D depth effect, the real press feel, and the best part, by the end you'll be able to swap the button's color and text in one click without touching the animation itself. No code anywhere.
Why the button feels 3D
The whole effect is one trick: the button is two shapes, and the top one travels 4 pixels.
That's it. A face layer, a base layer sitting 4 pixels lower, and a press that moves the face down until it lands flush on the base. Your brain reads the visible edge as depth, and the travel as a physical click. Duolingo wraps it in good timing and derived colors, and that's exactly what we'll build.
Step 1 - Two shapes
Create a new file with a 300x100 Artboard as the button's container.
- Draw a Rounded Rectangle, 240x40, with a 12 corner radius
- Fill it with a bright green, close to Duolingo's but pick your own shade
- Center it on the Artboard
- Duplicate the rectangle and move the copy 4 pixels down. That's the base
Right now both shapes are the same color, so there's no depth yet. You could just pick a darker green for the base, but don't. There's a better way, and it's the idea that carries this whole tutorial.
Step 2 - Darken, don't recolor
Select the bottom rectangle and add a second Fill on top of its green: solid black, at 20% opacity.
Now the base is a darker version of whatever color sits under it. Not a second green you picked by hand, a shade that is derived from the first one. This matters because in a minute the button's color will come from data, and any color that arrives will automatically get a matching darker base. One color input drives the entire button.
Step 3 - The label
Add a Text element with the word CONTINUE in caps.
Duolingo's typeface is custom, so we'll fake the feel with a free Google font:
- Nunito Black gets you the rounded, chunky look
- Squeeze it to 85% on the X axis only, Duolingo's font is slightly condensed
- Add a bit of letter spacing
- Size it to almost the full width of the button and center it
Play with it until it reads right to you. The recognition comes from the styling (caps, heavy weight, spacing), not from the exact font.
Step 4 - Group it like it moves
When the button is pressed, the face and the label travel together and the base stays put. So the hierarchy should say exactly that:
Button-M- the master groupFace- a group holdingFace Shape(top rectangle) andLabel(the text)Base- the bottom rectangle
Rive Masterclass + Rive Scripting
Master Rive for real products.
Get Both for $197Both courses for $197. Save $50.
Rive Masterclass + Rive Scripting
Master Rive for real products.
Both courses for $197. Save $50.
One group to move, one shape that never moves. Meaningful names now will pay off when we wire things up.
Step 5 - One color, one label, from data
Open the Data panel and rename the default View Model to Button View Model. Add two properties:
buttonColor(Color), default greenbuttonLabel(String), default CONTINUE
Now connect them with Data Binding: bind the green Fill of Face Shape and the green Fill of Base to buttonColor, and bind the text run of Label to buttonLabel.
Run it and change the values. The whole button re-skins itself, face, base shade, and text, from two properties. This is why we split the darkening into its own black layer: both greens point at the same source, and the depth comes along for free in pink, red, or anything else you type in.
Step 6 - Two states, and the animation lives between them
Here's the approach: we don't animate a press. We define two poses and let the transition do the motion.
- Rename the default timeline to
Default, and key theFacegroup at its resting Y - Duplicate it, name it
Pressed, and moveFacedown until it sits flush onBase - In
Pressed, also darken the face: add another black Fill onFace Shape, key its opacity at 0% in Default and 20% in Pressed
(Prefer a brighter press instead? Make that extra Fill white instead of black. Same principle: the press effect is derived from the color, not painted over it.)
In the State Machine, add a Boolean input isPressed, then two transitions: Default to Pressed when isPressed is true, and back when it's false. Set the transition duration to 100ms with cubic easing, and the snap becomes a smooth, natural click.
Step 7 - Make it listen
The button still doesn't respond to a real press, so let's add Listeners:
Pointer DownonFace ShapesetsisPressedto truePointer Upsets it back to false
Why not a Click listener? Because a click fires once. With down and up as separate events, the button stays pressed as long as your finger holds it, exactly like the real thing. Press, hold, release. That's the feel.
Run the State Machine and press the button. Face drops, darkens, springs back on release. And the color and label are still live data, change them mid-run and the press keeps working, untouched.
Where this goes next
This button is part one of a series. Coming next: the success moment, a particle burst and shine clipped to the button's shape, and then making the whole thing a responsive component you can drop into any layout at any size.
Everything here, the State Machine, Data Binding, Listeners, easing, goes much deeper than one button. If you want the full picture, that's exactly what the course below covers.
Get more Rive tips
Weekly tutorials, new lessons, and Rive community highlights — no spam.


