From 057c52cdd2c74d5c2268a802f1707863fd7ab210 Mon Sep 17 00:00:00 2001
From: Devin
Date: Thu, 6 Aug 2026 01:07:19 +0000
Subject: [PATCH 1/7] Make Swift and Rust clickable in the landing page SDK
animation
---
fern/assets/styles.css | 65 ++++++++++++++++++++++++++++
fern/products/home/pages/welcome.mdx | 12 +++++
fern/rive-animation.js | 22 ++++++----
3 files changed, 90 insertions(+), 9 deletions(-)
diff --git a/fern/assets/styles.css b/fern/assets/styles.css
index 7f7c9bbbc4..3d96ac7eb3 100644
--- a/fern/assets/styles.css
+++ b/fern/assets/styles.css
@@ -1084,6 +1084,71 @@ h1, h2, h3 {
display: inline-block;
}
+/* Live Swift and Rust tabs layered over the SDK animation.
+ Positions and sizes are percentages of the 369x93 Rive artboard, so they track the canvas as it scales. */
+.sdk-rive .rive-container {
+ container-type: inline-size;
+}
+
+.rive-tab-overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ pointer-events: none;
+}
+
+.rive-tab {
+ position: absolute;
+ top: 34.34%;
+ height: 28.6%;
+ width: 10.46%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1.92cqw;
+ text-decoration: none;
+ background-color: #fcfcfd;
+ pointer-events: auto;
+}
+
+.dark .rive-tab {
+ background-color: #111113;
+}
+
+.rive-tab-swift {
+ left: 76.53%;
+}
+
+.rive-tab-rust {
+ left: 86.99%;
+}
+
+.rive-tab-icon {
+ width: 2.88cqw;
+ height: 2.88cqw;
+ margin: 0;
+ object-fit: contain;
+}
+
+.rive-tab-label {
+ font-family: "JetBrains Mono", ui-monospace, monospace;
+ font-size: 1.73cqw;
+ line-height: 1;
+ color: #4a4a4a;
+}
+
+.dark .rive-tab-label {
+ color: #a9a9a9;
+}
+
+.rive-tab:hover .rive-tab-label {
+ color: #21201c;
+}
+
+.dark .rive-tab:hover .rive-tab-label {
+ color: #fcfcfd;
+}
+
/*** END -- LANDING PAGE STYLING ***/
/* HACK: add a slash to markdown snippet documentation (otherwise it gets parsed by CLI) */
diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx
index 0de8f47702..fa43f2e59f 100644
--- a/fern/products/home/pages/welcome.mdx
+++ b/fern/products/home/pages/welcome.mdx
@@ -45,6 +45,18 @@ layout: custom
+ {/* The Rive file renders Swift and Rust as inert grey artwork, so they are covered with live tabs */}
+
diff --git a/fern/rive-animation.js b/fern/rive-animation.js
index 24b8489c21..900c115058 100644
--- a/fern/rive-animation.js
+++ b/fern/rive-animation.js
@@ -45,15 +45,18 @@
try {
// Set container aspect ratio while respecting existing layout
- const currentWidth = riveContainer.offsetWidth || riveContainer.clientWidth;
- if (currentWidth > 0) {
- // Use existing width, set height based on aspect ratio
- riveContainer.style.height = (currentWidth / aspectRatio) + 'px';
- } else {
- // Fallback: use CSS aspect-ratio if container has no initial width
- riveContainer.style.aspectRatio = aspectRatio.toString();
- riveContainer.style.width = '100%';
- }
+ const syncContainerHeight = () => {
+ const currentWidth = riveContainer.offsetWidth || riveContainer.clientWidth;
+ if (currentWidth > 0) {
+ // Use existing width, set height based on aspect ratio
+ riveContainer.style.height = (currentWidth / aspectRatio) + 'px';
+ } else {
+ // Fallback: use CSS aspect-ratio if container has no initial width
+ riveContainer.style.aspectRatio = aspectRatio.toString();
+ riveContainer.style.width = '100%';
+ }
+ };
+ syncContainerHeight();
// Let Rive handle canvas sizing internally - much simpler!
const r = new rive.Rive({
@@ -107,6 +110,7 @@
// Simple resize handling (following Rive best practices)
const windowResizeHandler = () => {
+ syncContainerHeight();
// Let Rive handle the canvas sizing internally
if (r && r.resizeDrawingSurfaceToCanvas) {
r.resizeDrawingSurfaceToCanvas();
From 1656d59750a26d85e7117fbfdbca3328abf395b6 Mon Sep 17 00:00:00 2001
From: "jon.syla"
Date: Fri, 7 Aug 2026 13:57:58 +0000
Subject: [PATCH 2/7] Rebuild the landing page SDK card in HTML/CSS with Swift
and Rust as real tabs
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
fern/assets/styles.css | 243 ++++++++++++++++++++-------
fern/products/home/pages/welcome.mdx | 87 ++++++++--
fern/rive-animation.js | 43 +----
3 files changed, 264 insertions(+), 109 deletions(-)
diff --git a/fern/assets/styles.css b/fern/assets/styles.css
index 3d96ac7eb3..90f5e82908 100644
--- a/fern/assets/styles.css
+++ b/fern/assets/styles.css
@@ -1016,23 +1016,6 @@ h1, h2, h3 {
-.sdk-rive {
- aspect-ratio: 369/93;
- width: 100%;
- display: block;
- overflow: visible; /* Changed from hidden - was clipping interactive areas */
- transform: translateY(0px);
- transition: transform 250ms ease-out !important;
- will-change: transform;
- /* Ensure click events can reach the canvas */
- pointer-events: auto;
- min-height: 104px;
-}
-
-.sdk-rive:hover {
- transform: translateY(-3px) !important;
-}
-
.docs-rive {
min-height: 213px;
}
@@ -1042,10 +1025,6 @@ h1, h2, h3 {
}
@media (max-width: 640px) {
- .sdk-rive {
- min-height: inherit;
- }
-
.docs-rive {
min-height: inherit;
}
@@ -1084,69 +1063,215 @@ h1, h2, h3 {
display: inline-block;
}
-/* Live Swift and Rust tabs layered over the SDK animation.
- Positions and sizes are percentages of the 369x93 Rive artboard, so they track the canvas as it scales. */
-.sdk-rive .rive-container {
+/* SDK language tabs.
+ Sizes and positions are expressed in --u, one unit of the 369x93 grid the card is laid out on,
+ so everything scales with the card width. */
+.sdk-card {
+ --u: 0.271003cqw; /* 100cqw / 369 */
+ --tab-pitch: 39.03;
+ --tab-first: 9.8;
+ --tab-index: 0;
+ --underline-width: calc(37.1 * var(--u));
+ --sdk-card-bg: #fcfcfd;
+ --sdk-card-subdued-bg: #f5f5f6;
+ --sdk-card-border: #e0e1e6;
+ --sdk-card-skeleton: #cacaca;
+ --sdk-card-text: #191919;
+ --sdk-card-subdued-text: #5a5a5b;
+ --sdk-card-copy: #b3b3b4;
+ --sdk-card-active: #008700;
container-type: inline-size;
+ box-sizing: border-box;
+ aspect-ratio: 369/93;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ border-radius: 4px;
+ /* Drawn inside the box so the 369x93 grid is not shrunk by the border */
+ box-shadow: inset 0 0 0 1px var(--sdk-card-border);
+ background-color: var(--sdk-card-bg);
+ transition: transform 250ms ease-out;
+}
+
+.dark .sdk-card {
+ --sdk-card-bg: #111113;
+ --sdk-card-subdued-bg: #1b1b1d;
+ --sdk-card-border: #303136;
+ --sdk-card-skeleton: #47484f;
+ --sdk-card-text: #e7e7e7;
+ --sdk-card-subdued-text: #a3a4a8;
+ --sdk-card-copy: #2d2e33;
+ --sdk-card-active: #9cdd8d;
+}
+
+.sdk-card:hover {
+ transform: translateY(-3px);
}
-.rive-tab-overlay {
+.sdk-card * {
+ box-sizing: border-box;
+}
+
+/* Visually hidden, but still focusable and operable with the arrow keys */
+.sdk-card-radio {
position: absolute;
- inset: 0;
- z-index: 2;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
pointer-events: none;
}
-.rive-tab {
- position: absolute;
- top: 34.34%;
- height: 28.6%;
- width: 10.46%;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1.92cqw;
- text-decoration: none;
- background-color: #fcfcfd;
- pointer-events: auto;
+.sdk-card-header {
+ flex: none;
+ height: calc(19.25 * var(--u));
+ padding: calc(8.6 * var(--u)) 0 0 calc(6.15 * var(--u));
+ border-bottom: 1px solid var(--sdk-card-border);
+}
+
+.sdk-card-skeleton {
+ display: block;
+ width: calc(51.6 * var(--u));
+ height: calc(2 * var(--u));
+ border-radius: 999px;
+ background-color: var(--sdk-card-skeleton);
}
-.dark .rive-tab {
- background-color: #111113;
+.sdk-card-tabs {
+ position: relative;
+ flex: 1;
+ display: flex;
+ padding: 0 calc(7.9 * var(--u)) 0 calc(var(--tab-first) * var(--u));
+ border-bottom: 1px solid var(--sdk-card-border);
}
-.rive-tab-swift {
- left: 76.53%;
+.sdk-card-tab {
+ flex: 1;
+ display: grid;
+ grid-template-rows: calc(13.7 * var(--u)) auto;
+ justify-items: center;
+ align-items: center;
+ gap: calc(0.5 * var(--u));
+ padding-top: calc(10.9 * var(--u));
+ margin: 0;
+ cursor: pointer;
+ transform-origin: 50% 60%;
+ transition: transform 150ms ease-out;
}
-.rive-tab-rust {
- left: 86.99%;
+.sdk-card-tab:hover {
+ transform: scale(1.048);
}
-.rive-tab-icon {
- width: 2.88cqw;
- height: 2.88cqw;
+.sdk-card-tab-icon {
+ width: var(--icon-size);
+ height: var(--icon-size);
margin: 0;
object-fit: contain;
}
-.rive-tab-label {
- font-family: "JetBrains Mono", ui-monospace, monospace;
- font-size: 1.73cqw;
- line-height: 1;
- color: #4a4a4a;
+.sdk-card-tab-typescript { --icon-size: calc(10.63 * var(--u)); }
+.sdk-card-tab-python { --icon-size: calc(13.62 * var(--u)); }
+.sdk-card-tab-go { --icon-size: calc(13.06 * var(--u)); }
+.sdk-card-tab-java { --icon-size: calc(13.66 * var(--u)); }
+.sdk-card-tab-dotnet { --icon-size: calc(13.27 * var(--u)); }
+.sdk-card-tab-php { --icon-size: calc(13.68 * var(--u)); }
+.sdk-card-tab-ruby { --icon-size: calc(10.57 * var(--u)); }
+.sdk-card-tab-swift { --icon-size: calc(10.07 * var(--u)); }
+.sdk-card-tab-rust { --icon-size: calc(10.02 * var(--u)); }
+
+.sdk-card-tab-label {
+ font-family: var(--font-code);
+ font-size: calc(6.16 * var(--u));
+ line-height: calc(7.3 * var(--u));
+ color: var(--sdk-card-text);
+ white-space: nowrap;
}
-.dark .rive-tab-label {
- color: #a9a9a9;
+.sdk-card-underline {
+ position: absolute;
+ bottom: -1px; /* sits on the separator, like the animation did */
+ left: calc((var(--tab-first) + (var(--tab-index) + 0.5) * var(--tab-pitch)) * var(--u) - var(--underline-width) / 2);
+ width: var(--underline-width);
+ height: calc(0.9 * var(--u));
+ background-color: var(--sdk-card-active);
+ transition: left 150ms ease-out, width 150ms ease-out;
}
-.rive-tab:hover .rive-tab-label {
- color: #21201c;
+#sdk-card-python:checked ~ .sdk-card-tabs { --tab-index: 1; --underline-width: calc(22.4 * var(--u)); }
+#sdk-card-go:checked ~ .sdk-card-tabs { --tab-index: 2; --underline-width: calc(19 * var(--u)); }
+#sdk-card-java:checked ~ .sdk-card-tabs { --tab-index: 3; --underline-width: calc(19 * var(--u)); }
+#sdk-card-dotnet:checked ~ .sdk-card-tabs { --tab-index: 4; --underline-width: calc(18.7 * var(--u)); }
+#sdk-card-php:checked ~ .sdk-card-tabs { --tab-index: 5; --underline-width: calc(18.5 * var(--u)); }
+#sdk-card-ruby:checked ~ .sdk-card-tabs { --tab-index: 6; --underline-width: calc(19.9 * var(--u)); }
+#sdk-card-swift:checked ~ .sdk-card-tabs { --tab-index: 7; --underline-width: calc(18.6 * var(--u)); }
+#sdk-card-rust:checked ~ .sdk-card-tabs { --tab-index: 8; --underline-width: calc(19 * var(--u)); }
+
+#sdk-card-typescript:focus-visible ~ .sdk-card-tabs .sdk-card-tab-typescript,
+#sdk-card-python:focus-visible ~ .sdk-card-tabs .sdk-card-tab-python,
+#sdk-card-go:focus-visible ~ .sdk-card-tabs .sdk-card-tab-go,
+#sdk-card-java:focus-visible ~ .sdk-card-tabs .sdk-card-tab-java,
+#sdk-card-dotnet:focus-visible ~ .sdk-card-tabs .sdk-card-tab-dotnet,
+#sdk-card-php:focus-visible ~ .sdk-card-tabs .sdk-card-tab-php,
+#sdk-card-ruby:focus-visible ~ .sdk-card-tabs .sdk-card-tab-ruby,
+#sdk-card-swift:focus-visible ~ .sdk-card-tabs .sdk-card-tab-swift,
+#sdk-card-rust:focus-visible ~ .sdk-card-tabs .sdk-card-tab-rust {
+ outline: 2px solid var(--sdk-card-active);
+ outline-offset: calc(-1 * var(--u));
+ border-radius: 2px;
}
-.dark .rive-tab:hover .rive-tab-label {
- color: #fcfcfd;
+.sdk-card-command {
+ flex: none;
+ height: calc(30.5 * var(--u));
+ display: flex;
+ align-items: center;
+ gap: calc(4 * var(--u));
+ padding: 0 calc(8.12 * var(--u)) 0 calc(7 * var(--u));
+ background-color: var(--sdk-card-subdued-bg);
+}
+
+.sdk-card-command-text {
+ flex: 1;
+ display: none;
+ overflow: hidden;
+ padding: 0;
+ border: none;
+ border-radius: 0;
+ box-shadow: none;
+ background: none;
+ font-family: var(--font-code);
+ font-size: calc(6.97 * var(--u));
+ line-height: 1.2;
+ white-space: nowrap;
+ color: var(--sdk-card-subdued-text);
+}
+
+#sdk-card-typescript:checked ~ .sdk-card-command .sdk-card-command-typescript,
+#sdk-card-python:checked ~ .sdk-card-command .sdk-card-command-python,
+#sdk-card-go:checked ~ .sdk-card-command .sdk-card-command-go,
+#sdk-card-java:checked ~ .sdk-card-command .sdk-card-command-java,
+#sdk-card-dotnet:checked ~ .sdk-card-command .sdk-card-command-dotnet,
+#sdk-card-php:checked ~ .sdk-card-command .sdk-card-command-php,
+#sdk-card-ruby:checked ~ .sdk-card-command .sdk-card-command-ruby,
+#sdk-card-swift:checked ~ .sdk-card-command .sdk-card-command-swift,
+#sdk-card-rust:checked ~ .sdk-card-command .sdk-card-command-rust {
+ display: block;
+}
+
+.sdk-card-copy {
+ flex: none;
+ width: calc(6.88 * var(--u));
+ height: calc(8 * var(--u));
+ color: var(--sdk-card-copy);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .sdk-card,
+ .sdk-card-tab,
+ .sdk-card-underline {
+ transition: none;
+ }
}
/*** END -- LANDING PAGE STYLING ***/
diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx
index fa43f2e59f..b7cb464871 100644
--- a/fern/products/home/pages/welcome.mdx
+++ b/fern/products/home/pages/welcome.mdx
@@ -41,22 +41,77 @@ layout: custom
- {/* Rive Animation */}
-
-
-
- {/* The Rive file renders Swift and Rust as inert grey artwork, so they are covered with live tabs */}
-
+ {/* SDK language tabs */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Typescript
+
+
+
+ Python
+
+
+
+ GO
+
+
+
+ Java
+
+
+
+ .NET
+
+
+
+ PHP
+
+
+
+ Ruby
+
+
+
+ Swift
+
+
+
+
+ Rust
+
+
+
+
+
+
npm install your-company
+
pip install your-company
+
go get github.com/your-company/go-sdk
+
{'compile "com.your-company:java-sdk:1.0.0"'}
+
dotnet add package YourCompany.Net --version 1.0.0
+
composer require your-company/your-company-php
+
gem install your_company
+
{'.package(url: "https://github.com/your-company/swift-sdk")'}
+
cargo add your-company
+
+
+
+
diff --git a/fern/rive-animation.js b/fern/rive-animation.js
index 900c115058..960766ed98 100644
--- a/fern/rive-animation.js
+++ b/fern/rive-animation.js
@@ -45,18 +45,15 @@
try {
// Set container aspect ratio while respecting existing layout
- const syncContainerHeight = () => {
- const currentWidth = riveContainer.offsetWidth || riveContainer.clientWidth;
- if (currentWidth > 0) {
- // Use existing width, set height based on aspect ratio
- riveContainer.style.height = (currentWidth / aspectRatio) + 'px';
- } else {
- // Fallback: use CSS aspect-ratio if container has no initial width
- riveContainer.style.aspectRatio = aspectRatio.toString();
- riveContainer.style.width = '100%';
- }
- };
- syncContainerHeight();
+ const currentWidth = riveContainer.offsetWidth || riveContainer.clientWidth;
+ if (currentWidth > 0) {
+ // Use existing width, set height based on aspect ratio
+ riveContainer.style.height = (currentWidth / aspectRatio) + 'px';
+ } else {
+ // Fallback: use CSS aspect-ratio if container has no initial width
+ riveContainer.style.aspectRatio = aspectRatio.toString();
+ riveContainer.style.width = '100%';
+ }
// Let Rive handle canvas sizing internally - much simpler!
const r = new rive.Rive({
@@ -110,7 +107,6 @@
// Simple resize handling (following Rive best practices)
const windowResizeHandler = () => {
- syncContainerHeight();
// Let Rive handle the canvas sizing internally
if (r && r.resizeDrawingSurfaceToCanvas) {
r.resizeDrawingSurfaceToCanvas();
@@ -240,27 +236,6 @@
return;
}
- // SDK Animation with native Rive interactions
- createRiveAnimation({
- canvasSelector: '#sdk-rive-canvas',
- riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68802bc752aef23fab76e6fc_12235f640f9ad3339b42e8d026c6345a_sdk-rive.riv',
- aspectRatio: 369/93,
- stateMachine: "State Machine 1",
- fallbackImages: [
- {
- src: './images/sdks-preview-light.svg',
- className: 'sdks-preview-img dark:hidden',
- alt: 'SDK Generation Preview'
- },
- {
- src: './images/sdks-preview-dark.svg',
- className: 'sdks-preview-img hidden dark:block',
- alt: 'SDK Generation Preview'
- }
- ]
- // No eventHandlers - this animation only uses native interactions
- });
-
// Docs Animation
createRiveAnimation({
canvasSelector: '#docs-rive-canvas',
From a7a5bc9fff817a8db86e0eacdcc837c97120d2fc Mon Sep 17 00:00:00 2001
From: "jon.syla"
Date: Fri, 7 Aug 2026 14:26:29 +0000
Subject: [PATCH 3/7] Keep the .NET install command's double hyphen literal
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
fern/products/home/pages/welcome.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx
index b7cb464871..0c7b2ec2bb 100644
--- a/fern/products/home/pages/welcome.mdx
+++ b/fern/products/home/pages/welcome.mdx
@@ -103,7 +103,7 @@ layout: custom
pip install your-company
go get github.com/your-company/go-sdk
{'compile "com.your-company:java-sdk:1.0.0"'}
- dotnet add package YourCompany.Net --version 1.0.0
+ {'dotnet add package YourCompany.Net --version 1.0.0'}
composer require your-company/your-company-php
gem install your_company
{'.package(url: "https://github.com/your-company/swift-sdk")'}
From f0de8959096bc0414fcb5b31e8e3f7e79dc87100 Mon Sep 17 00:00:00 2001
From: "jon.syla"
Date: Fri, 7 Aug 2026 14:32:02 +0000
Subject: [PATCH 4/7] Draw the SDK card outline over the command row so it
closes at the bottom
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
fern/assets/styles.css | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/fern/assets/styles.css b/fern/assets/styles.css
index 90f5e82908..374849ac52 100644
--- a/fern/assets/styles.css
+++ b/fern/assets/styles.css
@@ -1082,18 +1082,28 @@ h1, h2, h3 {
--sdk-card-active: #008700;
container-type: inline-size;
box-sizing: border-box;
+ position: relative;
aspect-ratio: 369/93;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 4px;
- /* Drawn inside the box so the 369x93 grid is not shrunk by the border */
- box-shadow: inset 0 0 0 1px var(--sdk-card-border);
background-color: var(--sdk-card-bg);
transition: transform 250ms ease-out;
}
+/* Drawn over the rows rather than as a border on the card, so the 369x93 grid keeps its
+ full size and the opaque command row cannot paint over the bottom of the outline. */
+.sdk-card::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border: 1px solid var(--sdk-card-border);
+ border-radius: 4px;
+ pointer-events: none;
+}
+
.dark .sdk-card {
--sdk-card-bg: #111113;
--sdk-card-subdued-bg: #1b1b1d;
From 4ce834136292412a75d0a2e159f9dd03b7d12950 Mon Sep 17 00:00:00 2001
From: "jon.syla"
Date: Sun, 9 Aug 2026 07:18:00 +0000
Subject: [PATCH 5/7] Draw the SDK card in on load, matching the Rive intro
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
fern/assets/styles.css | 63 +++++++++++++++++++++++++++-
fern/products/home/pages/welcome.mdx | 22 ++++++----
2 files changed, 75 insertions(+), 10 deletions(-)
diff --git a/fern/assets/styles.css b/fern/assets/styles.css
index 374849ac52..b1704b7a7a 100644
--- a/fern/assets/styles.css
+++ b/fern/assets/styles.css
@@ -1241,8 +1241,30 @@ h1, h2, h3 {
background-color: var(--sdk-card-subdued-bg);
}
+/* Wraps every command so the typing reveal runs once on load, rather than restarting each time
+ a command is shown. Shrinks to the visible command, which makes the clip-path steps below
+ land on character boundaries. */
+.sdk-card-command-typing {
+ display: block;
+ width: fit-content;
+ max-width: 100%;
+ min-width: 0;
+ overflow: hidden;
+ animation: sdk-card-type-in 400ms steps(24) 120ms backwards;
+}
+
+/* 24 steps, one per character of the Typescript command shown on load */
+@keyframes sdk-card-type-in {
+ from {
+ clip-path: inset(0 100% 0 0);
+ }
+
+ to {
+ clip-path: inset(0);
+ }
+}
+
.sdk-card-command-text {
- flex: 1;
display: none;
overflow: hidden;
padding: 0;
@@ -1271,17 +1293,56 @@ h1, h2, h3 {
.sdk-card-copy {
flex: none;
+ margin-left: auto;
width: calc(6.88 * var(--u));
height: calc(8 * var(--u));
color: var(--sdk-card-copy);
}
+/* Draws the card in on load: a curtain in the row background colours that retreats to the right,
+ revealing the outline, separators and tabs in turn. Its soft trailing edge is what makes each
+ tab fade up rather than snap in, and it starts one softness-width off the left edge so the
+ card is fully covered on the first frame. */
+.sdk-card-intro {
+ --draw-softness: 34.5%;
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ pointer-events: none;
+ background: linear-gradient(
+ to bottom,
+ var(--sdk-card-bg) calc(62.5 * var(--u)),
+ var(--sdk-card-subdued-bg) calc(62.5 * var(--u))
+ );
+ -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--draw-softness));
+ mask-image: linear-gradient(to right, transparent, #000 var(--draw-softness));
+ animation: sdk-card-draw-in 700ms cubic-bezier(0.15, 0.2, 0.4, 1) forwards;
+}
+
+@keyframes sdk-card-draw-in {
+ from {
+ transform: translateX(calc(-1 * var(--draw-softness)));
+ }
+
+ to {
+ transform: translateX(100%);
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
.sdk-card,
.sdk-card-tab,
.sdk-card-underline {
transition: none;
}
+
+ .sdk-card-intro {
+ display: none;
+ }
+
+ .sdk-card-command-typing {
+ animation: none;
+ }
}
/*** END -- LANDING PAGE STYLING ***/
diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx
index 0c7b2ec2bb..868c6af3cc 100644
--- a/fern/products/home/pages/welcome.mdx
+++ b/fern/products/home/pages/welcome.mdx
@@ -99,20 +99,24 @@ layout: custom
-
npm install your-company
-
pip install your-company
-
go get github.com/your-company/go-sdk
-
{'compile "com.your-company:java-sdk:1.0.0"'}
-
{'dotnet add package YourCompany.Net --version 1.0.0'}
-
composer require your-company/your-company-php
-
gem install your_company
-
{'.package(url: "https://github.com/your-company/swift-sdk")'}
-
cargo add your-company
+
+ npm install your-company
+ pip install your-company
+ go get github.com/your-company/go-sdk
+ {'compile "com.your-company:java-sdk:1.0.0"'}
+ {'dotnet add package YourCompany.Net --version 1.0.0'}
+ composer require your-company/your-company-php
+ gem install your_company
+ {'.package(url: "https://github.com/your-company/swift-sdk")'}
+ cargo add your-company
+
+
+
{/* Language Icons */}
From 067b8f1295086c94118c41362d2c1564e5d7a24b Mon Sep 17 00:00:00 2001
From: "jon.syla"
Date: Sun, 9 Aug 2026 15:05:08 +0000
Subject: [PATCH 6/7] Start the SDK card draw-in fully covering the card
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
fern/assets/styles.css | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/fern/assets/styles.css b/fern/assets/styles.css
index b1704b7a7a..0795184d60 100644
--- a/fern/assets/styles.css
+++ b/fern/assets/styles.css
@@ -1304,9 +1304,12 @@ h1, h2, h3 {
tab fade up rather than snap in, and it starts one softness-width off the left edge so the
card is fully covered on the first frame. */
.sdk-card-intro {
- --draw-softness: 34.5%;
+ --draw-softness: calc(127 * var(--u));
position: absolute;
- inset: 0;
+ top: 0;
+ bottom: 0;
+ left: calc(-1 * var(--draw-softness));
+ width: calc(369 * var(--u) + var(--draw-softness));
z-index: 2;
pointer-events: none;
background: linear-gradient(
@@ -1321,7 +1324,7 @@ h1, h2, h3 {
@keyframes sdk-card-draw-in {
from {
- transform: translateX(calc(-1 * var(--draw-softness)));
+ transform: translateX(0);
}
to {
From 80f1bde6e26aaeb9d6af24dde3db64afe3fb8955 Mon Sep 17 00:00:00 2001
From: "jon.syla"
Date: Tue, 11 Aug 2026 20:31:36 +0000
Subject: [PATCH 7/7] Start the SDK card draw-in from JS once the page is
loaded and the card is on screen
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
fern/assets/styles.css | 17 +++++++--
fern/docs.yml | 2 +
fern/sdk-card-draw-in.js | 80 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 96 insertions(+), 3 deletions(-)
create mode 100644 fern/sdk-card-draw-in.js
diff --git a/fern/assets/styles.css b/fern/assets/styles.css
index 0795184d60..40e20a92a2 100644
--- a/fern/assets/styles.css
+++ b/fern/assets/styles.css
@@ -1250,6 +1250,9 @@ h1, h2, h3 {
max-width: 100%;
min-width: 0;
overflow: hidden;
+}
+
+.sdk-card[data-drawing-in] .sdk-card-command-typing {
animation: sdk-card-type-in 400ms steps(24) 120ms backwards;
}
@@ -1302,8 +1305,12 @@ h1, h2, h3 {
/* Draws the card in on load: a curtain in the row background colours that retreats to the right,
revealing the outline, separators and tabs in turn. Its soft trailing edge is what makes each
tab fade up rather than snap in, and it starts one softness-width off the left edge so the
- card is fully covered on the first frame. */
+ card is fully covered on the first frame. sdk-card-draw-in.js adds the attribute once the page
+ is loaded and the card is on screen, the way the Rive canvases start once their runtime is
+ fetched; without it the animation would run against the browser's first frame, which on a
+ refresh can be composited before the page is presented and so go unseen. */
.sdk-card-intro {
+ display: none;
--draw-softness: calc(127 * var(--u));
position: absolute;
top: 0;
@@ -1319,6 +1326,10 @@ h1, h2, h3 {
);
-webkit-mask-image: linear-gradient(to right, transparent, #000 var(--draw-softness));
mask-image: linear-gradient(to right, transparent, #000 var(--draw-softness));
+}
+
+.sdk-card[data-drawing-in] .sdk-card-intro {
+ display: block;
animation: sdk-card-draw-in 700ms cubic-bezier(0.15, 0.2, 0.4, 1) forwards;
}
@@ -1339,11 +1350,11 @@ h1, h2, h3 {
transition: none;
}
- .sdk-card-intro {
+ .sdk-card[data-drawing-in] .sdk-card-intro {
display: none;
}
- .sdk-card-command-typing {
+ .sdk-card[data-drawing-in] .sdk-card-command-typing {
animation: none;
}
}
diff --git a/fern/docs.yml b/fern/docs.yml
index 0dbe7a493e..e028885611 100644
--- a/fern/docs.yml
+++ b/fern/docs.yml
@@ -189,6 +189,8 @@ settings:
js:
- path: ./rive-animation.js
strategy: afterInteractive
+ - path: ./sdk-card-draw-in.js
+ strategy: afterInteractive
agents:
page-directive: "For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt."
diff --git a/fern/sdk-card-draw-in.js b/fern/sdk-card-draw-in.js
new file mode 100644
index 0000000000..1ce525ec8a
--- /dev/null
+++ b/fern/sdk-card-draw-in.js
@@ -0,0 +1,80 @@
+// Plays the landing page SDK card's draw-in, the counterpart to the Rive canvases' intro.
+// The animation itself lives in styles.css and is gated on [data-drawing-in]: left to CSS alone it
+// starts against the browser's first frame of the document, which on a reload can be composited
+// before the page is presented and while the card is still scrolled out of view, so the reveal is
+// over by the time anyone sees the card. This waits for the page to load and the card to be on
+// screen, which is also what makes the Rive intros visible on every refresh.
+(function () {
+ const DRAWING_ATTRIBUTE = 'data-drawing-in';
+ const observed = new WeakSet();
+
+ function play(card) {
+ card.setAttribute(DRAWING_ATTRIBUTE, '');
+ const intro = card.querySelector('.sdk-card-intro');
+ if (!intro) {
+ return;
+ }
+ // Drop the attribute once the curtain has cleared, so the overlay leaves the layer tree and
+ // the animation can be played again if the card is re-created by a client-side navigation.
+ intro.addEventListener(
+ 'animationend',
+ () => card.removeAttribute(DRAWING_ATTRIBUTE),
+ { once: true }
+ );
+ }
+
+ function playWhenOnScreen(card) {
+ if (observed.has(card)) {
+ return;
+ }
+ observed.add(card);
+
+ if (typeof IntersectionObserver === 'undefined') {
+ play(card);
+ return;
+ }
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ for (const entry of entries) {
+ if (entry.isIntersecting) {
+ observer.disconnect();
+ play(card);
+ }
+ }
+ },
+ { threshold: 0.5 }
+ );
+ observer.observe(card);
+ }
+
+ function initializeCards() {
+ document.querySelectorAll('.sdk-card').forEach(playWhenOnScreen);
+ }
+
+ function whenLoaded(callback) {
+ if (document.readyState === 'complete') {
+ callback();
+ } else {
+ window.addEventListener('load', callback, { once: true });
+ }
+ }
+
+ whenLoaded(() => {
+ initializeCards();
+
+ // Catch cards rendered later, either by hydration or by a client-side navigation. Mutations
+ // arrive in bursts, so collapse each burst into one pass.
+ let scheduled = false;
+ new MutationObserver(() => {
+ if (scheduled) {
+ return;
+ }
+ scheduled = true;
+ requestAnimationFrame(() => {
+ scheduled = false;
+ initializeCards();
+ });
+ }).observe(document.body, { subtree: true, childList: true });
+ });
+})();