refactor(docs): unify homepage icon link tiles (#48317)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Docs UI refactor.

## What is the current behavior?

Framework quickstarts and several other homepage grids used `IconPanel`
/ `IconPanelWithIconPicker`, which brought redundant tooltips
(duplicating the visible title), inconsistent hover treatment, and
circular icon tiles that didn't match the intended squarish look.

| Before |
| --- |
| <img width="1744" height="626" alt="CleanShot 2026-07-24 at 14 29
44@2x"
src="https://github.com/user-attachments/assets/1e145be2-19c9-4bd0-bf06-17516acbc774"
/> |

## What is the new behavior?

Introduces a shared `IconLink` used across the docs homepage (and
reference index) so icon+label tiles share the same composition and
hover: icon tile fill matches the surrounding surface (`surface-100`),
with a stronger border on hover, and the row uses `hover:bg-accent`.
Framework quickstarts keep the larger tile size; other sections keep the
smaller size. Also aligns Explore more GlassPanels with Build your
backend by using the same bordered background treatment.

| After |
| --- |
| <img width="2478" height="906" alt="CleanShot 2026-07-24 at 15 09
48@2x"
src="https://github.com/user-attachments/assets/dd516845-c162-47e4-8b86-9a7fa0e73290"
/> |

## Additional context

Removes the unused `IconPanelWithIconPicker` wrapper now that
homepage/reference consumers go through `IconLink`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added reusable `IconLink` UI for consistent docs tiles, including
`IconLinkList`, menu icon, and light/dark icon rendering.
* Introduced `FrameworkQuickstarts` to generate quickstart links based
on feature-flag-enabled SDKs.
* **UI / Improvements**
* Updated the docs home and API reference pages to use the new
list-based icon-link layout.
* Refreshed migration guides and “Explore more”/self-hosting sections
(including accessibility and updated CTA text).
* **Bug Fixes**
  * Migration guide items now omit entries missing required details.
* **Chores / Cleanup**
* Removed the older icon panel picker-based UI component and its usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-07-28 15:12:13 +10:00
1 parent 8aeae070c2
commit 4bda3bfe72
6 files changed
+301 -239

No files matched your search

+63 -67
View File
@@ -2,13 +2,12 @@ import { isFeatureEnabled } from 'common'
import { type Metadata, type ResolvingMetadata } from 'next'
import Link from 'next/link'
import { cn } from 'ui'
import { IconPanel } from 'ui-patterns/IconPanel'
import { TextLink } from 'ui-patterns/TextLink'
import { FrameworkQuickstarts } from '@/components/HomePageCover'
import { FrameworkQuickstarts } from '@/components/FrameworkQuickstarts'
import { MIGRATION_PAGES } from '@/components/Navigation/NavigationMenu/NavigationMenu.constants'
import { GlassPanelWithIconPicker } from '@/features/ui/GlassPanelWithIconPicker'
import { IconPanelWithIconPicker } from '@/features/ui/IconPanelWithIconPicker'
import { IconLinkImage, IconLinkList, IconLinkMenuIcon } from '@/features/ui/IconLink'
import HomeLayout from '@/layouts/HomeLayout'
import { BASE_PATH } from '@/lib/constants'
@@ -88,31 +87,26 @@ const postgresIntegrations = [
title: 'AI & Vectors',
icon: 'ai',
href: '/guides/ai',
description: 'AI toolkit to manage embeddings',
},
{
title: 'Cron',
icon: 'cron',
href: '/guides/cron',
description: 'Schedule and monitor recurring Jobs',
},
{
title: 'Queues',
icon: 'queues',
href: '/guides/queues',
description: 'Durable Message Queues with guaranteed delivery',
},
{
title: 'Data REST API',
icon: 'rest',
href: '/guides/api',
description: 'Access your database through a RESTful API.',
},
{
title: 'GraphQL API',
icon: 'graphql',
href: '/guides/graphql',
description: 'Access your database through a GraphQL API.',
},
]
@@ -224,6 +218,20 @@ const additionalResources = [
},
]
const migrationGuides = [...MIGRATION_PAGES]
.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
.flatMap((guide) => {
if (!guide.name || !guide.url || typeof guide.icon !== 'string') return []
return [
{
title: guide.name,
href: guide.url,
icon: <IconLinkImage path={guide.icon} hasLightIcon={guide.hasLightIcon} />,
},
]
})
const HomePage = () => (
<HomeLayout>
<div className="flex flex-col">
@@ -234,12 +242,12 @@ const HomePage = () => (
Connect a framework
</h2>
<p className="m-0 p-0 text-sm text-foreground-light">
Start with a framework quickstart and connect your project in minutes.
Start with a quickstart guide to connect your project in minutes.
</p>
</div>
<div className="col-span-8 not-prose">
<FrameworkQuickstarts />
<FrameworkQuickstarts labelledBy="connect-a-framework" />
</div>
</div>
)}
@@ -253,7 +261,10 @@ const HomePage = () => (
</p>
</div>
<ul className="col-span-8 grid grid-cols-12 gap-6 not-prose [&_svg]:text-brand-600">
<ul
aria-labelledby="products"
className="col-span-8 grid grid-cols-12 gap-6 not-prose [&_svg]:text-brand-600"
>
{products.map((product) => {
return (
<li key={product.title} className={cn(product.span ?? 'col-span-12 md:col-span-6')}>
@@ -277,18 +288,15 @@ const HomePage = () => (
Extend your database with built-in tools for AI, APIs, scheduled jobs, and queues.
</p>
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{postgresIntegrations.map((integration) => (
<Link
href={integration.href}
key={integration.title}
passHref
className="col-span-6 md:col-span-4"
>
<IconPanelWithIconPicker {...integration} />
</Link>
))}
</div>
<IconLinkList
labelledBy="postgres-integrations"
className="col-span-8 not-prose"
items={postgresIntegrations.map((integration) => ({
title: integration.title,
href: integration.href,
icon: <IconLinkMenuIcon icon={integration.icon} />,
}))}
/>
</div>
<div className="flex flex-col gap-6 border-b py-12 lg:grid lg:grid-cols-12 lg:gap-x-16">
@@ -301,23 +309,17 @@ const HomePage = () => (
</p>
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{clientLibraries
<IconLinkList
labelledBy="client-libraries"
className="col-span-8 not-prose"
items={clientLibraries
.filter((library) => library.enabled)
.map((library) => {
return (
<Link
href={library.href}
key={library.title}
passHref
className="col-span-6 md:col-span-4"
>
<IconPanelWithIconPicker {...library} />
</Link>
)
})}
</div>
.map((library) => ({
title: library.title,
href: library.href,
icon: <IconLinkMenuIcon icon={library.icon} />,
}))}
/>
</div>
{isFeatureEnabled('docs:full_getting_started') && (
<div className="flex flex-col gap-6 border-b py-12 lg:grid lg:grid-cols-12 lg:gap-x-16">
@@ -335,19 +337,11 @@ const HomePage = () => (
/>
</div>
<ul className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{MIGRATION_PAGES.sort((a, b) => (a.name || '').localeCompare(b.name || '')).map(
(guide) => {
return (
<li key={guide.name} className="col-span-6 md:col-span-4">
<Link href={guide.url || '#'} passHref>
<IconPanel {...guide} title={guide.name} background={true} showLink={false} />
</Link>
</li>
)
}
)}
</ul>
<IconLinkList
labelledBy="migrate-to-supabase"
className="col-span-8 not-prose"
items={migrationGuides}
/>
</div>
)}
@@ -361,7 +355,10 @@ const HomePage = () => (
</p>
</div>
<ul className="col-span-8 grid grid-cols-12 gap-6 not-prose">
<ul
aria-labelledby="additional-resources"
className="col-span-8 grid grid-cols-12 gap-6 not-prose"
>
{additionalResources.map((resource) => {
return (
<li key={resource.title} className="col-span-12 md:col-span-6">
@@ -370,7 +367,7 @@ const HomePage = () => (
passHref
target={resource.external ? '_blank' : undefined}
>
<GlassPanelWithIconPicker {...resource} background={false}>
<GlassPanelWithIconPicker {...resource}>
{resource.description}
</GlassPanelWithIconPicker>
</Link>
@@ -392,25 +389,24 @@ const HomePage = () => (
Get started with self-hosting Supabase.
</p>
<TextLink
label="More on Self-Hosting"
label="More on self-hosting"
url="/guides/self-hosting"
className="no-underline text-brand-link text-sm"
/>
</div>
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
<ul className="col-span-full lg:col-span-8 grid grid-cols-12 gap-6">
{selfHostingOptions.map((option) => {
return (
<li key={option.title} className="col-span-6">
<Link href={option.href} passHref>
<IconPanelWithIconPicker {...option} background={true} showLink={false} />
</Link>
</li>
)
})}
</ul>
<div className="col-span-8 grid grid-cols-12 not-prose">
<IconLinkList
labelledBy="self-hosting"
className="col-span-full lg:col-span-8"
itemClassName="col-span-6"
items={selfHostingOptions.map((option) => ({
title: option.title,
href: option.href,
icon: <IconLinkMenuIcon icon={option.icon} />,
}))}
/>
</div>
</div>
)}
+33 -39
View File
@@ -1,7 +1,5 @@
import Link from 'next/link'
import { REFERENCES, clientSdkIds } from '~/content/navigation.references'
import { IconPanelWithIconPicker } from '~/features/ui/IconPanelWithIconPicker'
import { clientSdkIds, REFERENCES } from '~/content/navigation.references'
import { IconLinkList, IconLinkMenuIcon } from '~/features/ui/IconLink'
import { LayoutMainContent } from '~/layouts/DefaultLayout'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
@@ -20,41 +18,37 @@ export default function ReferenceIndexPage() {
Supabase also has a Management API to help with managing your Supabase Platform, and a
CLI for local development and CI workflows.
</p>
<h2 className="mb-8">Client Libraries</h2>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{clientSdkIds.map((sdkId) => {
return (
<Link
key={REFERENCES[sdkId].name}
href={`/reference/${REFERENCES[sdkId].libPath}`}
passHref
className="col-span-6 md:col-span-4"
>
<IconPanelWithIconPicker
title={REFERENCES[sdkId].name}
icon={REFERENCES[sdkId].icon}
/>
</Link>
)
})}
</div>
<h2 className="mb-8">Management API and CLI</h2>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
<Link
href={`/reference/api/introduction`}
passHref
className="col-span-6 md:col-span-4"
>
<IconPanelWithIconPicker title="Management API" icon={REFERENCES['api'].icon} />
</Link>
<Link
href={`/reference/cli/introduction`}
passHref
className="col-span-6 md:col-span-4"
>
<IconPanelWithIconPicker title="CLI" icon={REFERENCES['cli'].icon} />
</Link>
</div>
<h2 id="client-libraries" className="mb-8">
Client Libraries
</h2>
<IconLinkList
labelledBy="client-libraries"
className="not-prose"
items={clientSdkIds.map((sdkId) => ({
title: REFERENCES[sdkId].name,
href: `/reference/${REFERENCES[sdkId].libPath}`,
icon: <IconLinkMenuIcon icon={REFERENCES[sdkId].icon} />,
}))}
/>
<h2 id="management-api-and-cli" className="mb-8">
Management API and CLI
</h2>
<IconLinkList
labelledBy="management-api-and-cli"
className="not-prose"
items={[
{
title: 'Management API',
href: '/reference/api/introduction',
icon: <IconLinkMenuIcon icon={REFERENCES['api'].icon} />,
},
{
title: 'CLI',
href: '/reference/cli/introduction',
icon: <IconLinkMenuIcon icon={REFERENCES['cli'].icon} />,
},
]}
/>
</article>
</LayoutMainContent>
</SidebarSkeleton>
@@ -0,0 +1,92 @@
import { isFeatureEnabled } from 'common'
import { IconLinkImage, IconLinkList } from '@/features/ui/IconLink'
const {
sdkDart: sdkDartEnabled,
sdkKotlin: sdkKotlinEnabled,
sdkSwift: sdkSwiftEnabled,
} = isFeatureEnabled(['sdk:dart', 'sdk:kotlin', 'sdk:swift'])
const frameworks = [
{
name: 'React',
icon: '/docs/img/icons/react-icon',
href: '/guides/getting-started/quickstarts/reactjs',
},
{
name: 'Next.js',
icon: '/docs/img/icons/nextjs-icon',
href: '/guides/getting-started/quickstarts/nextjs',
hasLightIcon: true,
},
{
name: 'TanStack Start',
icon: '/docs/img/icons/tanstack-icon',
href: '/guides/getting-started/quickstarts/tanstack',
hasLightIcon: true,
},
{
name: 'Astro',
icon: '/docs/img/icons/astro-icon',
href: '/guides/getting-started/quickstarts/astrojs',
hasLightIcon: true,
},
{
name: 'Vue',
icon: '/docs/img/icons/vuejs-icon',
href: '/guides/getting-started/quickstarts/vue',
},
{
name: 'Nuxt',
icon: '/docs/img/icons/nuxt-icon',
href: '/guides/getting-started/quickstarts/nuxtjs',
},
{
name: 'iOS Swift',
icon: '/docs/img/icons/swift-icon-orange',
href: '/guides/getting-started/quickstarts/ios-swiftui',
enabled: sdkSwiftEnabled,
},
{
name: 'Android Kotlin',
icon: '/docs/img/icons/kotlin-icon',
href: '/guides/getting-started/quickstarts/kotlin',
enabled: sdkKotlinEnabled,
},
{
name: 'Expo React Native',
icon: '/docs/img/icons/expo-icon',
href: '/guides/getting-started/quickstarts/expo-react-native',
hasLightIcon: true,
},
{
name: 'Flutter',
icon: '/docs/img/icons/flutter-icon',
href: '/guides/getting-started/quickstarts/flutter',
enabled: sdkDartEnabled,
},
{
name: 'Python',
icon: '/docs/img/icons/python-icon',
href: '/guides/getting-started/quickstarts/flask',
},
]
export function FrameworkQuickstarts({ labelledBy }: { labelledBy?: string }) {
return (
<IconLinkList
labelledBy={labelledBy}
size="lg"
items={frameworks
.filter((framework) => framework.enabled !== false)
.map((framework) => ({
title: framework.name,
href: framework.href,
icon: (
<IconLinkImage path={framework.icon} hasLightIcon={framework.hasLightIcon} size="lg" />
),
}))}
/>
)
}
+2 -119
View File
@@ -7,22 +7,14 @@ import {
PromptPanel,
PromptTitle,
} from '~/features/ui/PromptPanel'
import { isFeatureEnabled, useBreakpoint } from 'common'
import { isFeatureEnabled } from 'common'
import { Sparkles, Terminal } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useEffect, useState, type ReactNode } from 'react'
import { IconPanel } from 'ui-patterns/IconPanel'
import { type ReactNode } from 'react'
import { getCustomContent } from '../lib/custom-content/getCustomContent'
import DocsCoverLogo from './DocsCoverLogo'
import { setupCommand, setupCommands, setupPrompt } from './HomePageCover.constants'
const {
sdkDart: sdkDartEnabled,
sdkKotlin: sdkKotlinEnabled,
sdkSwift: sdkSwiftEnabled,
} = isFeatureEnabled(['sdk:dart', 'sdk:kotlin', 'sdk:swift'])
const fullGettingStartedEnabled = isFeatureEnabled('docs:full_getting_started')
function SetupPrompt({ cliCode }: { cliCode: ReactNode }) {
@@ -58,115 +50,6 @@ function SetupPrompt({ cliCode }: { cliCode: ReactNode }) {
)
}
const frameworks = [
{
tooltip: 'ReactJS',
icon: '/docs/img/icons/react-icon',
href: '/guides/getting-started/quickstarts/reactjs',
hasLightIcon: false,
},
{
tooltip: 'Next.js',
icon: '/docs/img/icons/nextjs-icon',
href: '/guides/getting-started/quickstarts/nextjs',
hasLightIcon: false,
},
{
tooltip: 'TanStack Start',
icon: '/docs/img/icons/tanstack-icon',
href: '/guides/getting-started/quickstarts/tanstack',
hasLightIcon: true,
},
{
tooltip: 'Astro.js',
icon: '/docs/img/icons/astro-icon',
href: '/guides/getting-started/quickstarts/astrojs',
hasLightIcon: true,
},
{
tooltip: 'Vue',
icon: '/docs/img/icons/vuejs-icon',
href: '/guides/getting-started/quickstarts/vue',
hasLightIcon: false,
},
{
tooltip: 'Nuxt',
icon: '/docs/img/icons/nuxt-icon',
href: '/guides/getting-started/quickstarts/nuxtjs',
hasLightIcon: false,
},
{
tooltip: 'iOS Swift',
icon: '/docs/img/icons/swift-icon-orange',
href: '/guides/getting-started/quickstarts/ios-swiftui',
enabled: sdkSwiftEnabled,
hasLightIcon: false,
},
{
tooltip: 'Android Kotlin',
icon: '/docs/img/icons/kotlin-icon',
href: '/guides/getting-started/quickstarts/kotlin',
enabled: sdkKotlinEnabled,
hasLightIcon: false,
},
{
tooltip: 'Expo React Native',
icon: '/docs/img/icons/expo-icon',
href: '/guides/getting-started/quickstarts/expo-react-native',
hasLightIcon: true,
},
{
tooltip: 'Flutter',
icon: '/docs/img/icons/flutter-icon',
href: '/guides/getting-started/quickstarts/flutter',
enabled: sdkDartEnabled,
hasLightIcon: false,
},
{
tooltip: 'Python',
icon: '/docs/img/icons/python-icon',
href: '/guides/getting-started/quickstarts/flask',
hasLightIcon: false,
},
]
export function FrameworkQuickstarts() {
const isXs = useBreakpoint(639)
const iconSize = isXs ? 'sm' : 'lg'
const { resolvedTheme } = useTheme()
const [isMounted, setIsMounted] = useState(false)
const isLightMode = isMounted && resolvedTheme === 'light'
useEffect(() => setIsMounted(true), [])
return (
<div className="grid grid-cols-12 gap-6">
{frameworks
.filter((framework) => framework.enabled !== false)
.map((framework) => {
const iconToUse =
framework.hasLightIcon && isLightMode ? `${framework.icon}-light` : framework.icon
return (
<Link
key={framework.tooltip}
href={framework.href}
passHref
className="col-span-6 no-underline md:col-span-4"
>
<IconPanel
iconSize={iconSize}
tooltip={framework.tooltip}
title={framework.tooltip}
icon={iconToUse}
/>
</Link>
)
})}
</div>
)
}
const HomePageCover = ({ title, cliCode }: { title: string; cliCode: ReactNode }) => {
const { homepageHeading } = getCustomContent(['homepage:heading'])
+111
View File
@@ -0,0 +1,111 @@
import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker'
import Link from 'next/link'
import { type ReactNode } from 'react'
import { cn } from 'ui'
const sizeStyles = {
sm: {
tile: 'h-10 w-10',
image: 'w-5',
},
lg: {
tile: 'h-10 w-10 sm:h-16 sm:w-16',
image: 'w-5 sm:w-8',
},
} as const
export type IconLinkSize = keyof typeof sizeStyles
export type IconLinkItem = {
title: string
href: string
icon: ReactNode
className?: string
}
export function IconLink({
href,
title,
icon,
size = 'sm',
className,
}: {
href: string
title: string
icon: ReactNode
size?: IconLinkSize
className?: string
}) {
return (
<Link
href={href}
className={cn(
'group relative -m-3 flex items-center gap-3 rounded-xl p-3 no-underline transition-colors hover:bg-accent',
className
)}
>
<div
className={cn(
'flex shrink-0 items-center justify-center overflow-hidden rounded-lg border bg-surface-100 transition-colors group-hover:border-strong',
sizeStyles[size].tile
)}
>
{icon}
</div>
<span className="text-base text-foreground">{title}</span>
</Link>
)
}
export function IconLinkList({
items,
labelledBy,
className,
itemClassName = 'col-span-6 md:col-span-4',
size = 'sm',
}: {
items: IconLinkItem[]
labelledBy?: string
className?: string
itemClassName?: string
size?: IconLinkSize
}) {
return (
<ul className={cn('grid grid-cols-12 gap-6', className)} aria-labelledby={labelledBy}>
{items.map((item) => (
<li key={item.href} className={cn(itemClassName, item.className)}>
<IconLink href={item.href} title={item.title} icon={item.icon} size={size} />
</li>
))}
</ul>
)
}
export function IconLinkImage({
path,
hasLightIcon,
size = 'sm',
}: {
path: string
hasLightIcon?: boolean
size?: IconLinkSize
}) {
const imgClassName = sizeStyles[size].image
return (
<>
{hasLightIcon ? (
<img src={`${path}-light.svg`} alt="" className={cn(imgClassName, 'dark:hidden')} />
) : null}
<img
src={`${path}.svg`}
alt=""
className={cn(imgClassName, hasLightIcon && 'hidden dark:block')}
/>
</>
)
}
export function IconLinkMenuIcon({ icon }: { icon: string }) {
return <MenuIconPicker icon={icon} width={18} height={18} />
}
@@ -1,14 +0,0 @@
import { type ComponentProps } from 'react'
import { IconPanel } from 'ui-patterns/IconPanel'
import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker'
type IconPanelWithIconPickerProps = Omit<ComponentProps<typeof IconPanel>, 'icon'> & {
icon: string
}
function IconPanelWithIconPicker({ icon, ...props }: IconPanelWithIconPickerProps) {
return <IconPanel icon={<MenuIconPicker icon={icon} width={18} height={18} />} {...props} />
}
export { IconPanelWithIconPicker }