diff --git a/src/components/ActionCard/ActionCard.stories.tsx b/src/components/ActionCard/ActionCard.stories.tsx
new file mode 100644
index 00000000000..caf535d2e73
--- /dev/null
+++ b/src/components/ActionCard/ActionCard.stories.tsx
@@ -0,0 +1,108 @@
+import { Meta, StoryObj } from "@storybook/nextjs"
+
+import { ButtonLink } from "@/components/ui/buttons/Button"
+
+import ActionCard from "."
+
+import devBlocksImg from "@/public/images/developers-eth-blocks.png"
+import enterpriseImg from "@/public/images/enterprise.png"
+import communityHeroImg from "@/public/images/heroes/community-hero.png"
+
+const meta = {
+ title: "Components / Cards / ActionCard",
+ component: ActionCard,
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ description: {
+ component:
+ "Large dual-pane action card with a tinted image area and content side, used to surface a single high-emphasis link (e.g. community subpage entry points). The whole card is a single link via `LinkBox` / `LinkOverlay`. Use `isRight` and `isBottom` to nudge the image alignment inside its pane.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ title: "Get involved",
+ description:
+ "The Ethereum community includes people from all backgrounds, working on many different projects.",
+ href: "/community/",
+ image: communityHeroImg,
+ alt: "Community illustration",
+ },
+}
+
+export const WithoutDescription: Story = {
+ args: {
+ title: "Get involved",
+ href: "/community/",
+ image: communityHeroImg,
+ alt: "Community illustration",
+ },
+}
+
+export const WithChildren: Story = {
+ args: {
+ title: "Enterprise on Ethereum",
+ description:
+ "Public, permissionless infrastructure for building secure, transparent business applications.",
+ href: "/enterprise/",
+ image: enterpriseImg,
+ alt: "Enterprise illustration",
+ children: (
+
+ Learn more
+
+ ),
+ },
+}
+
+export const ImagePositioning: Story = {
+ args: {
+ title: "Image positioning",
+ description:
+ "Combine `isRight` and `isBottom` to control where the image sits inside its tinted pane.",
+ href: "#",
+ image: devBlocksImg,
+ alt: "Developer blocks illustration",
+ isRight: true,
+ isBottom: false,
+ },
+}
+
+export const CommunityPageStyle = {
+ render: () => (
+
+ ),
+}
diff --git a/src/components/ActionCard.tsx b/src/components/ActionCard/index.tsx
similarity index 97%
rename from src/components/ActionCard.tsx
rename to src/components/ActionCard/index.tsx
index 1e178ad9cba..24410f54e31 100644
--- a/src/components/ActionCard.tsx
+++ b/src/components/ActionCard/index.tsx
@@ -2,12 +2,12 @@ import { StaticImageData } from "next/image"
import type { BaseHTMLAttributes, ElementType, ReactNode } from "react"
import { Image } from "@/components/Image"
+import { Flex } from "@/components/ui/flex"
import InlineLink from "@/components/ui/Link"
import { LinkBox, LinkOverlay } from "@/components/ui/link-box"
import { cn } from "@/lib/utils/cn"
-import { Flex } from "./ui/flex"
export type ActionCardProps = Omit<
BaseHTMLAttributes,
"title"
diff --git a/src/components/CommentCard/CommentCard.stories.tsx b/src/components/CommentCard/CommentCard.stories.tsx
new file mode 100644
index 00000000000..959d93c9ac7
--- /dev/null
+++ b/src/components/CommentCard/CommentCard.stories.tsx
@@ -0,0 +1,69 @@
+import { Meta, StoryObj } from "@storybook/nextjs"
+
+import { VStack } from "@/components/ui/flex"
+
+import CommentCard from "."
+
+const meta = {
+ title: "Components / Cards / CommentCard",
+ component: CommentCard,
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ description: {
+ component:
+ "Quote-style attribution card used inline within long-form content to attribute a statement to a named person. The avatar circle shows the first letter of `name` over an accent fill.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ description:
+ "Ethereum is a global, decentralized platform for money and new kinds of applications.",
+ name: "Tim Beiko",
+ title: "Protocol Coordination, Ethereum Foundation",
+ },
+}
+
+export const LongDescription: Story = {
+ args: {
+ description:
+ "Layer 2 networks settle transactions on Ethereum mainnet while running execution off-chain, which gives users much lower fees and higher throughput without compromising on the security properties of the underlying network. This is the path the ecosystem has converged on for scaling.",
+ name: "Alex Smirnov",
+ title: "Co-founder, deBridge",
+ },
+}
+
+export const InlineWithProse = {
+ render: () => (
+
+
+ Block proposers and attesters earn rewards for participating honestly in
+ consensus. The economic incentives have so far been sufficient to keep
+ the network secure under load.
+
+
+
+ Anyone with 32 ETH can run their own validator, and there are also
+ liquid staking options for smaller stakers.
+
+
+ ),
+}
diff --git a/src/components/GhostCard/GhostCard.stories.tsx b/src/components/GhostCard/GhostCard.stories.tsx
new file mode 100644
index 00000000000..dfd03e3e900
--- /dev/null
+++ b/src/components/GhostCard/GhostCard.stories.tsx
@@ -0,0 +1,78 @@
+import { Meta, StoryObj } from "@storybook/nextjs"
+
+import Emoji from "@/components/Emoji"
+import { VStack } from "@/components/ui/flex"
+
+import GhostCard from "."
+
+const meta = {
+ title: "Components / Cards / GhostCard",
+ component: GhostCard,
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ description: {
+ component:
+ "Card with a subtle offset shadow plate behind it (the 'ghost' layer), used to give a sidebar callout extra visual weight inside long-form content. Accepts arbitrary children -- the wrapper applies the layered look only.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ children: (
+ A simple card surface with a layered shadow plate behind it.
+ ),
+ },
+}
+
+export const WithEmojiAndHeading: Story = {
+ args: {
+ children: (
+ <>
+
+ Bitcoin pizza day
+
+ The first known commercial Bitcoin transaction in 2010 -- two pizzas
+ for 10,000 BTC -- is a reminder that volatile assets make poor units
+ of account. Stablecoins exist to fix that.
+
+ >
+ ),
+ },
+}
+
+export const SideBySide = {
+ render: () => (
+
+
+
+ For savers
+
+ Hold value without exposure to short-term volatility against the US
+ dollar.
+
+
+
+
+ For payments
+
+ Move dollar-denominated value across borders in minutes, settled on
+ public infrastructure.
+
+
+
+ ),
+}
diff --git a/src/components/GhostCard.tsx b/src/components/GhostCard/index.tsx
similarity index 94%
rename from src/components/GhostCard.tsx
rename to src/components/GhostCard/index.tsx
index 68c77866b31..a1c2f9c62f9 100644
--- a/src/components/GhostCard.tsx
+++ b/src/components/GhostCard/index.tsx
@@ -1,8 +1,8 @@
import React from "react"
-import { cn } from "@/lib/utils/cn"
+import { Card } from "@/components/ui/card"
-import { Card } from "./ui/card"
+import { cn } from "@/lib/utils/cn"
interface GhostCardProps extends React.HTMLAttributes {
children: React.ReactNode
diff --git a/src/components/HighlightCard/HighlightCard.stories.tsx b/src/components/HighlightCard/HighlightCard.stories.tsx
new file mode 100644
index 00000000000..ed662b86ffb
--- /dev/null
+++ b/src/components/HighlightCard/HighlightCard.stories.tsx
@@ -0,0 +1,109 @@
+import { Castle, LockKeyhole, Shield } from "lucide-react"
+import { Meta, StoryObj } from "@storybook/nextjs"
+
+import { CardTitle } from "@/components/ui/card"
+
+import { HighlightCard, HighlightCardContent, HighlightStack, IconBox } from "."
+
+const meta = {
+ title: "Components / Cards / HighlightCard",
+ component: HighlightCard,
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ description: {
+ component:
+ "Composition family for the in-content 'highlight' layout: a coloured `IconBox`, an optional `CardTitle`, and `HighlightCardContent` body, optionally stacked through `HighlightStack` to produce the divided list seen on the 'What is Ethereum' / 'What is Ether' pages. None of the parts are linkable on their own -- this is purely a content layout.",
+ },
+ },
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ render: () => (
+
+
+
+
+
+
Censorship resistance
+
+
+ No government or company has control over Ethereum. Decentralization
+ makes it nearly impossible for anyone to stop you from receiving
+ payments or using services on Ethereum.
+
+
+
+
+ ),
+}
+
+export const Stack: Story = {
+ render: () => (
+
+
+
+
+
+
+
Censorship resistance
+
+
+ No single entity can stop you from sending value or interacting
+ with applications on Ethereum.
+
+
+
+
+
+
+
+
+
+
Strong security guarantees
+
+
+ Ethereum is secured by hundreds of thousands of validators
+ distributed worldwide.
+
+
+
+
+
+
+
+
+
+
Reliability
+
+
+ The network has run continuously since 2015 and is designed for
+ long-term operation.
+
+
+
+
+
+ ),
+}
+
+export const IconBoxOnly: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+ ),
+}
diff --git a/src/components/HorizontalCard/HorizontalCard.stories.tsx b/src/components/HorizontalCard/HorizontalCard.stories.tsx
new file mode 100644
index 00000000000..fa5f2e4283c
--- /dev/null
+++ b/src/components/HorizontalCard/HorizontalCard.stories.tsx
@@ -0,0 +1,81 @@
+import { Meta, StoryObj } from "@storybook/nextjs"
+
+import { ButtonLink } from "@/components/ui/buttons/Button"
+import { VStack } from "@/components/ui/flex"
+
+import HorizontalCard from "."
+
+const meta = {
+ title: "Components / Cards / HorizontalCard",
+ component: HorizontalCard,
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ description: {
+ component:
+ "Borderless horizontal item with a large emoji on the left and a title + description on the right. Used in vertical lists where each row needs a glanceable icon, and the surrounding container provides the visual grouping (no card chrome of its own).",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ emoji: ":fuel_pump:",
+ title: "Gas fees",
+ description:
+ "The cost of doing an action on Ethereum, paid in ETH to validators.",
+ },
+}
+
+export const WithoutTitle: Story = {
+ args: {
+ emoji: ":dollar:",
+ description:
+ "USDC is a fully-reserved stablecoin pegged 1:1 to the US dollar.",
+ },
+}
+
+export const WithChildren: Story = {
+ args: {
+ emoji: ":wallet:",
+ title: "Self-custody wallet",
+ description:
+ "A wallet you control directly, with no third party holding your keys.",
+ children: (
+
+ Find a wallet
+
+ ),
+ },
+}
+
+export const TokenList = {
+ render: () => (
+
+
+
+
+
+ ),
+}
diff --git a/src/components/HorizontalCard.tsx b/src/components/HorizontalCard/index.tsx
similarity index 95%
rename from src/components/HorizontalCard.tsx
rename to src/components/HorizontalCard/index.tsx
index 0c76e1e7e38..bead5a9fb95 100644
--- a/src/components/HorizontalCard.tsx
+++ b/src/components/HorizontalCard/index.tsx
@@ -1,8 +1,8 @@
import React, { ReactNode } from "react"
-import { cn } from "@/lib/utils/cn"
+import Emoji from "@/components/Emoji"
-import Emoji from "./Emoji"
+import { cn } from "@/lib/utils/cn"
export interface HorizontalCardProps
extends Omit, "title"> {
diff --git a/src/components/SubpageCard/SubpageCard.stories.tsx b/src/components/SubpageCard/SubpageCard.stories.tsx
new file mode 100644
index 00000000000..50e71ba3546
--- /dev/null
+++ b/src/components/SubpageCard/SubpageCard.stories.tsx
@@ -0,0 +1,95 @@
+import { Code, Compass, GraduationCap, Hammer, Map, Wrench } from "lucide-react"
+import { Meta, StoryObj } from "@storybook/nextjs"
+
+import SubpageCard from "."
+
+const meta = {
+ title: "Components / Cards / SubpageCard",
+ component: SubpageCard,
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ description: {
+ component:
+ "Linkable card with an icon, title and description. The entire card is a link via `LinkBox` / `LinkOverlay`. Pass `inlineLink` to surface a visible CTA inside the card; otherwise the card body acts as the click target invisibly.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ title: "Frameworks",
+ description:
+ "Pre-built tools that handle a lot of the heavy lifting for building a decentralized application.",
+ icon: ,
+ href: "/developers/tools/frameworks/",
+ },
+}
+
+export const WithInlineLink: Story = {
+ args: {
+ title: "The Ethereum roadmap",
+ description:
+ "A look at how Ethereum is being improved over time, and the priorities driving those upgrades.",
+ icon: ,
+ href: "/roadmap/",
+ inlineLink: {
+ text: "See the roadmap",
+ },
+ },
+}
+
+export const ToolsPageGrid = {
+ render: () => (
+
+ }
+ href="/developers/tools/frameworks/"
+ />
+ }
+ href="/developers/tools/ides/"
+ />
+ }
+ href="/developers/tools/block-explorers/"
+ />
+ }
+ href="/developers/tools/testing/"
+ />
+ }
+ href="/developers/tutorials/"
+ />
+ }
+ href="/roadmap/"
+ />
+
+ ),
+}
diff --git a/src/components/SubpageCard.tsx b/src/components/SubpageCard/index.tsx
similarity index 100%
rename from src/components/SubpageCard.tsx
rename to src/components/SubpageCard/index.tsx