Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pnpm dlx shadcn@latest add radiumcoders/23rd.dev/gooey-color-picker
| [Radiant Lines](https://23rd.dev/docs/components/radiant-lines) | `@23rd/radiant-lines` | Hyperspace starfield background; warp speed driven by scroll |
| [Shader Fire](https://23rd.dev/docs/components/shader-fire) | `@23rd/shader-fire` | Sparse 2D fire wash — tongues rise from the bottom behind a landing hero |
| [Shader Gradient](https://23rd.dev/docs/components/shader-gradient) | `@23rd/shader-gradient` | Quiet WebGL wash behind landing heroes, empty states, and marketing sections |
| [Shader Sky](https://23rd.dev/docs/components/shader-sky) | `@23rd/shader-sky` | Clear blue or rain behind a hero — drifting clouds, optional window-glass film |
| [Stretchy Footer](https://23rd.dev/docs/components/stretchy-footer) | `@23rd/stretchy-footer` | Dia-style rubber overscroll; aurora stretches past the bottom, then snaps back |
| [Tangle Footer](https://23rd.dev/docs/components/tangle-footer) | `@23rd/tangle-footer` | Nested SVG text ribbons as a footer |

Expand Down
59 changes: 58 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
}

:root {
color-scheme: light;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
Expand Down Expand Up @@ -134,6 +135,7 @@
}

.dark {
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
Expand Down Expand Up @@ -167,6 +169,61 @@
--sidebar-ring: oklch(0.556 0 0);
}

/* Before `html.dark` exists, :root is light. Keep system-dark users on dark
tokens so a late theme script cannot flash white. `.light` still wins. */
@media (prefers-color-scheme: dark) {
:root:not(.light) {
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--color-fd-background: hsl(0, 0%, 7.04%);
--color-fd-foreground: hsl(0, 0%, 92%);
--color-fd-muted: hsl(0, 0%, 12.9%);
--color-fd-muted-foreground: hsla(0, 0%, 70%, 0.8);
--color-fd-popover: hsl(0, 0%, 11.6%);
--color-fd-popover-foreground: hsl(0, 0%, 86.9%);
--color-fd-card: hsl(0, 0%, 9.8%);
--color-fd-card-foreground: hsl(0, 0%, 98%);
--color-fd-border: hsla(0, 0%, 40%, 20%);
--color-fd-primary: hsl(0, 0%, 98%);
--color-fd-primary-foreground: hsl(0, 0%, 9%);
--color-fd-secondary: hsl(0, 0%, 12.9%);
--color-fd-secondary-foreground: hsl(0, 0%, 92%);
--color-fd-accent: hsla(0, 0%, 40.9%, 30%);
--color-fd-accent-foreground: hsl(0, 0%, 90%);
--color-fd-ring: hsl(0, 0%, 54.9%);
}
}

@layer base {
* {
@apply border-border outline-ring/50;
Expand Down Expand Up @@ -210,7 +267,7 @@
}

html {
@apply font-sans;
@apply bg-background font-sans;
scrollbar-gutter: stable;
}

Expand Down
7 changes: 7 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DocsSearchDialog } from "@/components/docs-search-dialog"
import { JsonLd } from "@/components/json-ld"
import { ThemeProvider } from "@/components/theme-provider"
import { TooltipProvider } from "@/components/ui/tooltip"
import { THEME_BOOTSTRAP_SCRIPT } from "@/lib/theme-bootstrap"
import { Toaster } from "sonner"
import {
OG_IMAGE_SIZE,
Expand Down Expand Up @@ -121,10 +122,16 @@ export default function RootLayout({
suppressHydrationWarning
className={cn("font-sans antialiased", fontMono.variable, geist.variable)}
>
<head>
<script
dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }}
/>
</head>
<body className="flex min-h-screen flex-col">
<JsonLd data={rootJsonLd()} />
<ThemeProvider>
<RootProvider
theme={{ enabled: false }}
search={{
links,
SearchDialog: DocsSearchDialog,
Expand Down
8 changes: 4 additions & 4 deletions components/component-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -470,21 +470,21 @@ export function ControlColors({
const hex = toHex6(color)
return (
<label
key={`${hex}-${index}`}
key={index}
className="relative size-9 cursor-pointer overflow-hidden rounded-2xl ring-1 ring-border/80 transition-[box-shadow] hover:ring-ring/40 focus-within:ring-3 focus-within:ring-ring/30"
>
<span
aria-hidden
className="absolute inset-0"
className="pointer-events-none absolute inset-0"
style={{ backgroundColor: hex }}
/>
<input
type="color"
value={hex}
value={hex.toLowerCase()}
aria-label={`${label} ${index + 1}`}
onChange={(e) => {
const next = colors.map((c, i) =>
i === index ? e.target.value.toUpperCase() : c
i === index ? toHex6(e.target.value) : c
)
onChange(next)
}}
Expand Down
30 changes: 20 additions & 10 deletions components/search-trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"use client"

import { useEffect, useState } from "react"
import { Dialog } from "@base-ui/react/dialog"
import { useSearchContext } from "fumadocs-ui/contexts/search"
import { RiSearchLine } from "@remixicon/react"

export function SearchTrigger() {
const { enabled, dialogHandle, hotKey } = useSearchContext()
const [mounted, setMounted] = useState(false)

useEffect(() => {
setMounted(true)
}, [])

if (!enabled) return null

Expand All @@ -18,16 +24,20 @@ export function SearchTrigger() {
>
<RiSearchLine className="size-4" />
<span className="hidden sm:inline">Search</span>
<span className="hidden items-center gap-0.5 sm:inline-flex">
{hotKey.map((k, i) => (
<kbd
key={i}
className="rounded-md bg-muted px-1.5 py-px font-sans text-[11px] font-medium text-muted-foreground"
>
{k.display}
</kbd>
))}
</span>
{mounted ? (
<span className="hidden items-center gap-0.5 sm:inline-flex">
{hotKey.map((k, i) => (
<kbd
key={i}
className="rounded-md bg-muted px-1.5 py-px font-sans text-[11px] font-medium text-muted-foreground"
>
{k.display}
</kbd>
))}
</span>
) : (
<span className="hidden h-[18px] w-14 sm:inline-flex" />
)}
</Dialog.Trigger>
)
}
13 changes: 10 additions & 3 deletions components/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import * as React from "react"
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"

// next-themes injects an inline <script> to prevent theme flicker.
// React 19 warns about script tags inside components — false positive;
// the script runs correctly during SSR.
// next-themes also injects an inline <script> (see lib/theme-bootstrap.ts for the
// copy that actually runs in <head> before first paint). React 19 warns about
// script tags inside client components — the warning is noisy, not a failure.
if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
const originalError = console.error
console.error = (...args: unknown[]) => {
Expand All @@ -30,6 +30,12 @@ if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
return
}

// Cursor's browser overlay injects `data-cursor-ref` after paint.
// React reports that as a hydration mismatch; it is not the app.
if (message.includes("hydrat") && message.includes("data-cursor-ref")) {
return
}

originalError.apply(console, args)
}
}
Expand All @@ -44,6 +50,7 @@ function ThemeProvider({
defaultTheme="system"
enableSystem
disableTransitionOnChange
scriptProps={{ suppressHydrationWarning: true }}
{...props}
>
<ThemeHotkey />
Expand Down
1 change: 1 addition & 0 deletions content/docs/components/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"---Shaders---",
"shader-gradient",
"shader-fire",
"shader-sky",
"---Footers---",
"tangle-footer",
"stretchy-footer",
Expand Down
138 changes: 138 additions & 0 deletions content/docs/components/shader-sky.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: Shader Sky
description: A WebGL sky for landing heroes — clear blue with drifting clouds in light, storm gray in dark, optional window-glass film.
---

import { ShaderSkyDemo } from "@/registry/shader-sky/shader-sky-demo"

Not a stock photo. Use it as weather behind a hero so light and dark already feel like a forecast. Open blue in light; a gray storm ceiling in dark. Clouds drift continuously, forming and dissolving like weather. Toggle **Glass** for a transparent window film. Press `d` to switch theme. Turn on **Copy** to drop a headline over the sky.

<ShaderSkyDemo />

## Installation

<CliCommand item="shader-sky" />

Or install straight from the public GitHub source registry:

<CliCommand item="shader-sky" github />

## Usage

Pin it behind a relatively positioned section. Keep content in a later stacking context (`relative z-10`). Default `theme="auto"` follows shadcn / next-themes (`class` on `<html>`), so the site theme _is_ the weather: light = clear, dark = rain.

<FrameworkCode>
<FrameworkReact>
```tsx
"use client"

import { ShaderSky } from "@/components/ui/shader-sky"

export function Hero() {
return (
<section className="relative isolate min-h-svh overflow-hidden bg-background">
<ShaderSky />
<div className="relative z-10 mx-auto flex min-h-svh max-w-xl flex-col items-center justify-center px-6 text-center">
<p className="text-xs font-medium tracking-[0.18em] text-muted-foreground uppercase">
New
</p>
<h1 className="mt-3 text-4xl font-medium tracking-tight">
The sky is the theme
</h1>
<p className="mt-3 text-muted-foreground">
Clear in light. Rain in dark. The shader stays behind the copy.
</p>
</div>
</section>
)
}
```
</FrameworkReact>
<FrameworkSvelte>
```svelte
<script>
import ShaderSky from "$lib/components/ui/shader-sky.svelte"
</script>

<section class="relative isolate min-h-svh overflow-hidden bg-background">
<ShaderSky />
<div class="relative z-10 mx-auto flex min-h-svh max-w-xl flex-col items-center justify-center px-6 text-center">
<p class="text-xs font-medium tracking-[0.18em] text-muted-foreground uppercase">
New
</p>
<h1 class="mt-3 text-4xl font-medium tracking-tight">
The sky is the theme
</h1>
<p class="mt-3 text-muted-foreground">
Clear in light. Rain in dark. The shader stays behind the copy.
</p>
</div>
</section>
```
</FrameworkSvelte>
</FrameworkCode>

Force a forecast if the parent theme should not drive it:

<FrameworkCode>
<FrameworkReact>
```tsx
<ShaderSky theme="light" />
<ShaderSky theme="dark" />
```
</FrameworkReact>
<FrameworkSvelte>
```svelte
<ShaderSky theme="light" />
<ShaderSky theme="dark" />
```
</FrameworkSvelte>
</FrameworkCode>

See the same sky through window glass — a transparent dotted film, sky still moving underneath:

<FrameworkCode>
<FrameworkReact>
```tsx
<ShaderSky glass glassSize={7} />
```
</FrameworkReact>
<FrameworkSvelte>
```svelte
<ShaderSky glass glassSize={7} />
```
</FrameworkSvelte>
</FrameworkCode>

Fewer, smaller clouds with a mix of sizes:

<FrameworkCode>
<FrameworkReact>
```tsx
<ShaderSky amount={0.35} scale={0.3} variation={0.8} />
```
</FrameworkReact>
<FrameworkSvelte>
```svelte
<ShaderSky amount={0.35} scale={0.3} variation={0.8} />
```
</FrameworkSvelte>
</FrameworkCode>

## Props

| Prop | Type | Default |
| ------------- | ----------------------------- | -------------------------------- |
| `colors` | `string[]` | zenith / horizon / cloud / shade |
| `speed` | `number` | `0.1` |
| `coverage` | `number` | `0.5` |
| `intensity` | `number` | `0.9` |
| `amount` | `number` | `0.5` |
| `scale` | `number` | `0.4` |
| `variation` | `number` | `0.7` |
| `interactive` | `boolean` | `false` |
| `glass` | `boolean` | `false` |
| `glassSize` | `number` | `7` |
| `theme` | `"light" \| "dark" \| "auto"` | `auto` |
| `className` | `string` | React only |
| `class` | `string` | Svelte only |
Loading
Loading