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
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: yarn

- name: Enable Corepack
run: corepack enable
cache: pnpm

- name: Install dependencies
run: yarn install --immutable
run: pnpm install --frozen-lockfile

- name: Run tests
run: yarn test
run: pnpm test
12 changes: 4 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ __diff_output__
# build
.next
next-env.d.ts
*.tsbuildinfo

# Yarn 3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# agent state
.claude/
.playwright-mcp/
5 changes: 1 addition & 4 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn precommit
pnpm precommit
2 changes: 1 addition & 1 deletion .lintstagedrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
'**/*.{js,jsx,ts,tsx}': ['jest -c .jest.config.js --findRelatedTests --passWithNoTests', 'prettier --write'],
'**/*.{js,jsx,ts,tsx}': ['jest -c .jest.config.js --findRelatedTests --passWithNoTests', 'oxfmt', 'oxlint'],
};
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
auto-install-peers=true
24 changes: 24 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"arrowParens": "avoid",
"printWidth": 120,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"sortPackageJson": false,
"ignorePatterns": [
"node_modules",
"/.next",
"/.yarn",
"/public",
"*.*",
"!*.css",
"!*.js",
"!*.json",
"!*.jsx",
"!*.less",
"!*.ts",
"!*.tsx",
"!*.yml"
]
}
13 changes: 13 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "oxc", "react", "nextjs", "jsx-a11y", "import", "promise", "jest"],
"categories": {
"correctness": "error",
"suspicious": "warn"
},
"rules": {
"react/react-in-jsx-scope": "off",
"import/no-named-as-default": "off",
"import/no-unassigned-import": "off"
}
}
7 changes: 0 additions & 7 deletions .prettierrc

This file was deleted.

942 changes: 0 additions & 942 deletions .yarn/releases/yarn-4.12.0.cjs

This file was deleted.

7 changes: 0 additions & 7 deletions .yarnrc.yml

This file was deleted.

70 changes: 39 additions & 31 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,56 @@
Note: CLAUDE.md is a symlink to this file. Edit AGENTS.md directly.

styled-components documentation website. Next.js 16 App Router, MDX content, styled-components v6.
styled-components documentation website. Next.js, MDX, styled-components.

## Principles

1. Verify before building -- Read the file before editing it. Read the nav config before adding pages. Read the live code scope before using hooks. Assumptions cause build failures.
## Read first

2. Wire completely -- Adding a docs page requires three changes in one commit: the MDX file, the page component import/render, and the `app/docs.json` nav entry. Missing any one breaks navigation or leaves orphaned content.
Before editing anything that touches styled-components APIs (`createTheme`, `ThemeProvider`, `createGlobalStyle`, `ServerStyleSheet`, `StyleSheetManager`, `stylisPluginRSC`), read `public/llms.txt`. It's the battle-tested v6.4 usage guide with real gotchas: `ThemeProvider` must receive the raw theme object (not the `createTheme` output — passing the output produces self-referential `var(--x, fallback)` CSS), `theme.vars` gives bare custom property names for dark mode overrides, `@property` registration is required for animatable tokens, etc. Trust `llms.txt` over training-data assumptions.

3. Snapshot tests are fragile by design -- They capture rendered CSS including generated class hashes. Any change to styled-components version, font constants, or style values will break snapshots. Run `npx jest -c .jest.config.js --updateSnapshot` and commit the result.
`llms.txt` is a first-class doc — when MDX docs change (API, SSR, theming, gotchas), update `llms.txt` in the same pass. It describes user-facing behavior, not implementation details. The same "artifacts describe behavior, not process" standard applies.

4. The SSR registry is load-bearing -- `lib/registry.tsx` wraps the root layout. It looks like a v6.3.0+ RSC artifact that can be removed, but most components use `'use client'`. Removing it causes FOUC. Do not remove without migrating components to server components first.
## Principles

5. Live code blocks are not regular code blocks -- Examples using ` ```react ` run in the site's live editor with `React`, `styled`, `css`, `keyframes`, `ThemeProvider`, `createGlobalStyle`, and `render()` in scope. Hooks must be qualified: `React.useRef()`, `React.useState()`. Imports are not supported.
1. Verify before building -- read the file before editing. Assumptions cause build failures.
2. Wire completely -- new docs pages need the MDX file, the page component, and a `docs.json` entry.
3. The SSR registry (`lib/registry.tsx`) is load-bearing. Removing it causes FOUC.
4. Live code blocks (` ```react `) run in a scoped editor. Hooks must be `React.useState()`, no imports.
5. Isolate `'use client'` as deeply as possible. The homepage is a server component with client islands.

## Commands

- `yarn` -- install (Yarn PnP, no package-lock.json)
- `npx next build` -- build and verify all pages (static generation)
- `npx jest -c .jest.config.js` -- run tests
- `npx jest -c .jest.config.js --updateSnapshot` -- update snapshots after style changes
- Pre-commit hooks run jest on related files + prettier via lint-staged
- `pnpm install` — install
- `npx next build` — do NOT run while the dev server is active
- `npx jest -c .jest.config.js` — tests

## Architecture

**Tokens:** `createTheme` API from `utils/theme.ts` is the single source of truth. Generates `var(--sc-*)` CSS custom properties. Dark mode overrides in `GlobalStyles.tsx` target `--sc-*` vars under `html.dark` and `@media (prefers-color-scheme: dark)`. Font vars (`--font-sans`, `--font-display`, `--font-mono`) are managed by next/font, not the theme. Display font is Figtree; body font is Inter; mono font is Google Sans Code.

**Theme:** Three states (light/dark/auto). Raw `<script>` in `<head>` before stylesheets sets the class — not `next/script`. `data-theme="dark"` synced for DocSearch. Toggle icon shows current mode.

## Documentation Structure
**Navigation:** Sidebar lives in `ClientLayout` (root layout) — persists across navigations via `SidebarFoldProvider` context. Search (DocSearch, module-level singleton) at top, then Home, Agents, Documentation (expands to categories → sections with scroll-spy), Blog, Ecosystem, Releases, Donate. Navbar is just logo + social + theme toggle. Mobile: hamburger for sidebar, theme toggle far-right. `Link` component uses `next/link` for internal routes and plain `<a>` for external URLs and static files (paths with file extensions like `/llms.txt`); callers pass `variant="inline" | "heading" | "block" | "unstyled"` (the legacy `inline`/`unstyled` boolean props were removed).

- MDX content: `sections/{category}/{topic}.mdx`
- Page components: `app/docs/{category}/page.tsx` (import MDX, render in order)
- Nav config: `app/docs.json` (section titles must exactly match `##` headings in MDX for anchor generation)
- `##` = top-level section heading, `###` = subsection
- Path alias `@/` maps to project root
**Homepage:** Two-column hero (copy left, live editor right) inside `LiveProvider`. Transparent background. Proof badges above CTA buttons. 10-year celebration effect (CSS bloom fireworks, respects reduced-motion). Company logos adapt via `brightness(0)` / `invert(1)`.

## Key Files
**Docs:** MDX in `sections/`, pages in `app/docs/`, nav config in `docs.json`. Heading IDs on the element itself with `scroll-margin-top`. Scroll-spy is a rAF-throttled scroll listener in the sidebar.

- `app/layout.tsx` -- root layout with next/font (Karla + JetBrains Mono via CSS vars `--font-body`, `--font-mono`)
- `lib/registry.tsx` -- SSR style collection for client components (ServerStyleSheet + useServerInsertedHTML)
- `utils/fonts.ts` -- font-family constants referencing CSS variables
- `components/Anchor.tsx` -- section heading with anchor link, used by all doc pages
- `components/ReleaseAnchor.tsx` -- release page heading with date pseudo-element
- `app/releases/page.tsx` -- fetches GitHub API releases, renders with markdown-to-jsx
- `utils/scope.ts` -- defines the scope (available globals) for live code editor blocks
**Z-index:** 10 (celebration/code), 20 (content/hero), 30 (sidebar), 40 (navbar).

## Gotchas

- `app/docs.json` titles are converted to URL hashes via `titleToDash`. Mismatched titles = broken sidebar links.
- The releases page uses `markdown-to-jsx` (not MDX). Compatibility issues with React 19 dev mode are tracked upstream.
- `next.config.mjs` has `compiler: { styledComponents: true }` for SWC transform. This is separate from RSC support.
- Font changes require updating `utils/fonts.ts`, `app/layout.tsx` (next/font config), and all test snapshots.
- `docs.json` titles become URL hashes via `titleToDash`. Mismatches break sidebar links.
- Live editor `scope.ts` derives component IDs from tag/component names. Counters cause hydration mismatches.
- Logo is a CSS 3D Platonic solid (`components/LogoConcepts/PlatonicLogo.tsx`). Interactive: drag-to-spin, click faces for actions, shared rotation singleton (globalThis-persisted for HMR). Face colors from `theme.palette[N]` (auto light/dark).
- Nav/sidebar widths use `sidebarWidth` from `utils/sizes.ts` in plain px — don't use `rem()` for these.
- Borders use opaque `color-mix(in oklch, text 8%, surface)`, not alpha.
- `utils/rem.ts` uses legacy 18px base. Prefer token spacing vars.
- Releases page uses `markdown-to-jsx`, not MDX.
- `${ClientComponent} &` selector interpolation in a styled template calls the referenced component's `.toString()`, which trips RSC's client-reference guard from a server module. Any styled component using this pattern must stay `'use client'`. Currently applies to `CodeBlock.tsx` (uses `${Note} &`).
- Import code mixins (`codeTextMixin`, `editorMixin`) from `components/codeMixins.ts`, not from `LiveEdit`. Importing from `LiveEdit` pulls `react-live-runner` + `sucrase` into the client bundle of every consumer.
- `utils/logoPalette.ts` is the single source of the hue ring. Read it for step count, offset, L/C. Three palette tiers in `utils/theme.ts`: `lightPalette`, `darkPalette` (both CVD-optimized via qlab), and the ring colors from `logoPalette.ts` (bright, for see-through mode). `theme.palette[N]` switches light/dark automatically. Don't hand-pick oklch values — derive from palette indices. Accent variants: L/C shifts on the same hue as the base palette entry, never adjacent indices.
- Palette generation: seed at target L/C/H → `qlab separate --adaptive --tolerance tight --gamut p3` → `qlab harmonize --adaptive --tolerance tight --gamut p3`. Both passes, in order. Read palette comments in `theme.ts` for current ΔE and seed values.
- OKLCH hue 0° is pink/magenta, not red. Warmer = higher hue toward orange. Read `logoPalette.ts` for the offset that places red at step 0.
- `mix-blend-mode` and `filter` on children of `preserve-3d` elements flattens 3D. Use alpha in background colors or `color-mix` instead.
- Blog posts are assembled dynamically from MDX files at build time by `utils/blog.server.ts`. No JSON index to maintain — just create the MDX file with `export const meta`.
- `PlatonicLogo.tsx` faces must NOT use `backface-visibility: hidden`. Per-face axis-angle interpolation during morph transitions can briefly flip a face normal and cull mid-animation. `transform-style: preserve-3d` z-sorts back faces naturally.
- `CelebrationEffect.tsx` particles are `React.memo`'d; `onAnimationEnd` is a stable `useCallback` that reads `fwId`/`particleId` from `data-*` attributes. Don't close over IDs in per-item arrow functions — it defeats memoization on the particle list.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ git clone https://github.com/styled-components/styled-components-website
# Enter the repo
cd styled-components-website
# Install the dependencies
yarn install
pnpm install
# Start local development
yarn dev
pnpm dev
```

> Note: This requires Node.js and yarn to be set up locally, see [nodejs.org](https://nodejs.org) for more information.
> Note: This requires Node.js and pnpm to be set up locally, see [nodejs.org](https://nodejs.org) for more information.

### Updating the visual diffs

If you add a new section or materially change the website, it probably will trigger the image comparison diff snapshot to fail. These can be updated via:

```sh
yarn test -u
pnpm test -- -u
```

### Folder structure
Expand Down
12 changes: 4 additions & 8 deletions app/api/proxy/[asset]/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import axios from 'axios';
import { NextRequest, NextResponse } from 'next/server';

const proxyMap: Record<string, string> = {
'npm-v.svg': 'https://img.shields.io/npm/v/styled-components.svg',
'size.svg': 'https://img.shields.io/badge/gzip%20size-12.4%20kB-brightgreen.svg',
'downloads.svg': 'https://img.shields.io/npm/dm/styled-components.svg?maxAge=3600',
'stars.svg':
'https://img.shields.io/github/stars/styled-components/styled-components.svg?style=social&label=Star&maxAge=3600',
Expand All @@ -23,11 +21,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}

try {
const { data, headers } = await axios.get(remoteUrl, {
responseType: 'arraybuffer',
});

const contentType = headers['content-type'];
const resp = await fetch(remoteUrl);
const data = await resp.arrayBuffer();
const contentType = resp.headers.get('content-type') || 'application/octet-stream';

return new NextResponse(data, {
status: 200,
Expand All @@ -36,7 +32,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
'cache-control': 's-maxage=3600,stale-while-revalidate',
},
});
} catch (error) {
} catch {
return NextResponse.json({ error: 'Error fetching remote asset' }, { status: 500 });
}
}
46 changes: 46 additions & 0 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { notFound } from 'next/navigation';
import BlogPostPage from '@/components/BlogPostPage';
import CelebrationEffect from '@/components/CelebrationEffect';
import { getPosts, getPostBySlug } from '@/utils/blog.server';

export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return { title: 'Post Not Found' };

return {
title: post.title,
description: post.description ?? `${post.title} by ${post.author}`,
};
}

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPostBySlug(slug);

if (!post) {
notFound();
}

let MdxContent: React.ComponentType;
try {
const mod = await import(`@/sections/blog/${post.date}-${post.slug}.mdx`);
MdxContent = mod.default;
} catch {
notFound();
}

return (
<>
{slug === 'celebrating-a-decade-of-styled-components' && <CelebrationEffect />}
<BlogPostPage post={post}>
<MdxContent />
</BlogPostPage>
</>
);
}
18 changes: 18 additions & 0 deletions app/blog/blog.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* Route-level overrides for blog pages.
Static CSS to avoid the createGlobalStyle SSR -> client hydration flash. */

[data-e2e-id='content'] img {
max-width: 100%;
height: auto;
border-radius: 4px;
}

/* Magazine-style section break: short warm rule, generous air. */
[data-e2e-id='content'] hr {
border: 0;
margin: 3.5rem auto;
width: 4rem;
height: 2px;
background: var(--sc-color-blogAccent);
opacity: 0.6;
}
5 changes: 5 additions & 0 deletions app/blog/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './blog.css';

export default function BlogLayout({ children }: { children: React.ReactNode }) {
return children;
}
13 changes: 13 additions & 0 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import BlogListPage from '@/components/BlogListPage';
import { getPosts } from '@/utils/blog.server';

export const metadata = {
title: 'Blog',
description: 'Articles and announcements from the styled-components team',
};

export default async function BlogPage() {
const posts = await getPosts();

return <BlogListPage posts={posts} />;
}
Loading
Loading