Understanding how the official p5.js reference website is architected, built, and deployed — covering everything from the framework choices to the iframe-based code runner, i18n system, and accessibility infrastructure.
The p5.js website (p5js.org) is a statically generated documentation and community platform built
with the following core technologies:
| Category | Technology |
|---|---|
| Framework | Astro v5 |
| UI Components | Preact v10 (with React compat layer) |
| Content | MDX + YAML via Astro content collections |
| Styling | Tailwind CSS v3 + SCSS modules |
| Code Editor |
CodeMirror 6 (@uiw/react-codemirror)
|
| Search | Fuse.js (client-side fuzzy search) |
| Testing | Vitest + Playwright (for a11y tests) |
| Deployment | Static build, service-worker-powered offline support |
The choice of Astro is strategically
important. Astro is designed for content-heavy sites and ships zero
JavaScript by default — components only hydrate on the client when
explicitly annotated with directives like
client:load
or
client:only. This means the reference documentation is fast and fully static, while
interactive UI elements (like the code editor or locale switcher) opt into
client-side rendering on-demand.
Preact (not React) is used as the
component runtime. Since Preact is a 3KB alternative to React with
identical APIs, this significantly reduces the bundle size. The
@astrojs/preact
integration is configured with
compat: true
to transparently support third-party React libraries (like
CodeMirror's React bindings) without shipping React itself.
At a high level, the repository is organized as:
├── .github/ # CI/CD workflows and issue templates
├── docs/ # Contributor documentation
├── public/ # Static assets (fonts, images, example media files)
├── src/
│ ├── api/ # External API integration (OpenProcessing)
│ ├── cached-data/ # Pre-fetched JSON from OpenProcessing
│ ├── components/ # Reusable UI components (Astro + Preact)
│ ├── content/ # Astro content collections (MDX + YAML)
│ │ ├── examples/ # Code examples (localized)
│ │ ├── reference/ # API reference entries
│ │ ├── tutorials/ # Tutorials (localized)
│ │ ├── events/ # Community events
│ │ ├── libraries/ # Third-party p5 libraries
│ │ ├── people/ # Contributors and team
│ │ ├── ui/ # UI translation strings (YAML per locale)
│ │ └── banner/ # Site-wide banner content
│ ├── globals/ # Shared runtime constants and state
│ ├── i18n/ # Locale utilities and constants
│ ├── layouts/ # Page layout wrappers
│ ├── pages/ # Astro file-based routing
│ └── scripts/ # Node.js build-time scripts
├── styles/ # Global SCSS and variables
├── test/ # Unit and accessibility tests
├── astro.config.mjs # Astro + integration config
└── package.json
The separation between
src/scripts/
(build-time Node.js code) and
src/components/
(browser code) is intentional and important — Astro enforces this boundary
at the framework level.
The
astro.config.mjs
file is the heart of the build configuration:
export default defineConfig({
site: 'https://p5js.org',
compressHTML: false,
integrations: [
mermaid({ autoTheme: true }),
preact({ compat: true }),
mdx(),
tailwind(),
fast(), // custom compression plugin
serviceWorker({ workbox: { ... } }),
],
prefetch: {
defaultStrategy: "viewport",
prefetchAll: true,
},
trailingSlash: "always",
build: { format: "directory", concurrency: 2 },
image: {
domains: ["openprocessing.org"],
service: passthroughImageService()
},
markdown: {
shikiConfig: { theme: 'github-light-high-contrast' },
},
});
Key design decisions:
compressHTML: false
— HTML compression is skipped in favor of a custom
fast
plugin (found in
src/scripts/fast-compress), giving the team more control over the output.
prefetch: { defaultStrategy: "viewport", prefetchAll: true }
— All links in the viewport are prefetched automatically, making
navigation feel near-instant. This is a significant UX enhancement for a
reference documentation site where users frequently jump between pages.
build.concurrency: 2
— Limits parallel page builds to avoid memory pressure during the large
reference section generation.
passthroughImageService()
— The built-in Astro image optimizer is bypassed. This is important
because the site references images from OpenProcessing's CDN (openprocessing.org) at build time, which a server-side image optimizer couldn't
process.
github-light-high-contrast
theme, outputting pre-rendered HTML with no client-side JavaScript
required.
The build also defines several custom npm scripts for generating specific sections of the site:
"build:reference": "tsx ./src/scripts/builders/reference.ts",
"build:contributor-docs": "tsx ./src/scripts/builders/contribute.ts",
"build:contributors": "tsx ./src/scripts/builders/people.ts",
"build:search": "tsx ./src/scripts/builders/search.ts",
"build:p5-version": "tsx ./src/scripts/p5-version.ts",
These builders run before or independently from the main Astro build. They
clone the upstream p5.js library repository (using
simple-git), parse its JSDoc/YUIDoc annotations, and generate
.mdx
and
.yaml
files that Astro then processes as normal content. This means the
reference documentation is always generated from the actual source code of
the library.
The site uses a dual-component model:
.astro
files for server-rendered, mostly-static components, and
.tsx/.jsx
(Preact) files for interactive, client-hydrated components.
These run entirely at build time and ship pure HTML:
Head/index.astro
— Full
<head>
with Open Graph, Twitter Card, and locale-mapped meta tags. The locale
map (en
→
en_US,
ko
→
ko_KR, etc.) ensures proper social media preview cards per locale.
Footer/index.astro
— Grid-based footer with localized section labels, external social
links, and the optional banner component. Pulls banner content from the
banner
content collection per locale.
Nav/index.astro
— Computes locale, detects homepage vs. inner page (to toggle logo
color), and passes everything as props to the hydrated
NavPanels
Preact component.
GridItem/*.astro
— A family of card components for rendering different content types
(Examples, Tutorials, Libraries, Sketches, Events, People,
ContributorDocs). Each takes a typed
CollectionEntry
and renders a consistent card layout.
These use
client:load
or
client:only
directives:
CodeEmbed/index.jsx
— The interactive p5.js code editor (detailed in the next section).
Nav/NavPanels.tsx
— Manages open/closed state for the main nav and jump-to panel, with
responsive behavior handled via a
ResizeObserver.
AccessibilitySettings/index.tsx
— Reads/writes accessibility preferences to
localStorage
and toggles CSS classes on
<html>.
LocaleSelect/index.tsx
— Locale switcher that constructs the new URL and redirects.
SearchProvider
— Client-only Fuse.js-powered search.
CopyCodeButton/index.tsx
— Copies code to clipboard using the Clipboard API, with an accessible
live region announcement.
Icon/index.tsx
— A comprehensive inline SVG icon system. Every icon is embedded
directly as SVG paths — no icon font, no external sprite. This avoids
FOUT and ensures icons are perfectly scaled at any size. The TypeScript
union type
IconKind
acts as a compile-time guarantee that only valid icon names can be
referenced, and an
assertNever
guard throws a runtime error if a new icon kind is added to the type but
not implemented.
Image/index.astro
— A wrapper around Astro's built-in
<Image>
component that adds two important features: a
visibleAltText
overlay (shows alt text visually as a badge over the image, toggled by
an accessibility setting), and support for
aspectRatio
variants (photo,
square,
none).
AnnotatedLine/index.astro
— A specialized component for displaying annotated code in tutorials. It
parses inline slot markers embedded in code strings (e.g.,
/*__SLOT_TOP_myLabel*/.../*__SLOT_END*/) and renders a table where each "slot" gets a colored
background, with annotation content placed above or below the
corresponding code segment. The coloring cycles through a palette using
alternating even/odd line schemes.
This is arguably the most technically interesting part of the codebase. The site needs to run live p5.js sketches in the browser, allow editing, and do so efficiently when multiple sketches appear on the same page.
The system has two distinct implementations for two contexts:
Used on example pages where code is editable. On mount, it:
<script id="p5ScriptTag" src={cdnLibraryUrl}>
tag into the document
<head>
once (idempotent — it checks if the tag already exists).
@uiw/react-codemirror
with JavaScript syntax highlighting and line wrapping.
createCanvas(w, h)
from the code string using a regex, and sets the preview iframe size
accordingly.
createButton,
createDiv, etc.) and adds extra height to accommodate them.
const canvasMatch =
/createCanvas\(\s*(\d+),\s*(\d+)\s*(?:,\s*(?:P2D|WEBGL)\s*)?\)/m.exec(
initialCode,
);
The editor buttons (Play, Stop, Reset, Copy) are rendered as
CircleButton
components with accessible
aria-label
attributes. Pressing "Run" calls
updateOrReRun(), which resets
previewCodeString
to force a re-render of the
CodeFrame
iframe.
The actual p5.js sketch runs inside a sandboxed
<iframe>
with
srcDoc
set to a self-contained HTML document. The document wrapping function is
notable:
const wrapInMarkup = (code: CodeBundle) => `<!DOCTYPE html>
<meta charset="utf8" />
<base href="${code.base || "/assets/"}" />
<style>html, body { margin: 0; padding: 0; } canvas { display: block; }</style>
<body>${code.htmlBody || ""}</body>
<script id="code" type="text/javascript">${wrapSketch(code.js) || ""}</script>
...`;
The
wrapSketch()
function automatically wraps bare p5.js code in a
setup()
function if neither
setup
nor a p5 constructor call is detected — this is a quality-of-life feature
for examples that show just a few drawing commands.
The p5.js Library Loading Problem
Loading p5.min.js inside each iframe naively would cause multiple full
downloads — one per sketch on a page, every time the sketch runs. To solve
this, the system uses
postMessage:
p5.min.js
once (via the
<script id="p5ScriptTag">
in
<head>).
CodeFrame
mounts and becomes visible, it
fetch()es the script's text content.
postMessage
to the iframe:
iframeRef.current.contentWindow.postMessage({ sender: cdnLibraryUrl,
message: p5ScriptText }, "*").
message
event listener receives the text, creates a
<script>
element, and injects it into
<head>.
This lets the browser cache the library in one place while every iframe reuses that cached content.
Intersection Observer for Performance
Both
CodeFrame
and
CodeFrameForServer
use
IntersectionObserver
to stop rendering sketches that scroll out of view:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
iframeRef.current.style.removeProperty("display");
} else {
iframeRef.current.style.display = "none";
}
});
},
{ rootMargin: "20px" },
);
Setting
display: none
on an iframe stops the browser from running the sketch's
draw()
loop every frame, which is critical on pages (like the reference) that
embed many simultaneous sketches.
iframe Sandbox
The iframe uses
sandbox="allow-scripts allow-popups allow-modals allow-forms
allow-same-origin". The
allow-same-origin
flag is required for the
postMessage
communication to work across the parent → iframe boundary in some browser
configurations.
A simpler version used on SSR/static pages where the code is not editable.
Instead of the postMessage trick, it includes p5.js directly via a CDN
<script src>
tag baked into the
srcDoc. The Intersection Observer is still used, but instead of toggling
display: none, it fully removes and restores the iframe's
srcDoc
content.
The site supports six locales:
en
(English — the default)
es
(Spanish)
hi
(Hindi)
ko
(Korean)
zh-Hans
(Simplified Chinese)
Internationalization is built from scratch, without using any external i18n library. It is split across two files:
export const defaultLocale = "en";
export const nonDefaultSupportedLocales = ["es", "hi", "ko", "zh-Hans"];
export const supportedLocales = [defaultLocale, ...nonDefaultSupportedLocales];
The key function is
getUiTranslator(), which:
src/content/ui/{locale}.yaml
file for the requested locale.
t(...keys)
function that does recursive key lookup with English fallback.
const t = (...args: string[]) => {
let val = findTranslation(args, currentLocaleDict);
if (val === undefined) val = findTranslation(args, defaultLocaleDict);
if (val === undefined) return args[args.length - 1]; // key as last-resort fallback
return val;
};
The variadic
...args
signature supports nested keys, e.g.,
t("referenceCategories", "modules", "color"), which maps to a nested YAML structure. This is used extensively for
categorized translations (tutorial categories, reference categories,
callout titles, attribution text, etc.).
Locale detection from the URL is done via
getCurrentLocale(pathname), which uses a regex to match the path against known locale prefixes. The
LocaleSelect
Preact component constructs the new locale's URL by replacing the
locale prefix in the current path.
When a page exists in a non-English locale but has fallen behind the
English source, the site shows a banner. The banner text itself is
hardcoded in the component (not in YAML translation files) to avoid a
chicken-and-egg problem — if the translation files are outdated, you
can't rely on them to display an accurate "this is outdated"
message. The
checkTranslationBanner
utility compares content file modification timestamps to determine
staleness.
Astro content collections are used for all structured content. The most
important utility is the locale fallback system in
src/pages/_utils.ts.
export const getCollectionInLocaleWithFallbacks = memoize(
async (collectionName, locale) => {
const localizedEntries = await getCollectionInLocale(
collectionName,
locale,
);
const defaultLocaleCollection =
await getCollectionInDefaultLocale(collectionName);
const filteredDefaultEntries = defaultLocaleCollection.filter(
(defaultEntry) => {
return !localizedEntries.some(
(localeEntry) =>
removeLocalePrefix(localeEntry.id) ===
removeLocalePrefix(defaultEntry.id),
);
},
);
return [...localizedEntries, ...filteredDefaultEntries];
},
(...args) => args.join("_"),
);
This function merges localized entries with English fallbacks. For any
content piece that doesn't exist in the requested locale, the English
version is shown. The result is memoized with Lodash's
memoize
using a string key of all arguments — this is a significant performance
optimization since this function is called many times during a build (once
per page, for multiple collections).
Examples historically used a different URL scheme than what Astro
generates from directory structure. The
exampleContentSlugToLegacyWebsiteSlug()
function handles backward compatibility:
export const exampleContentSlugToLegacyWebsiteSlug = (slug: string): string =>
slug
.replace(/^[\w-]+?\//, "") // strip locale prefix
.replace(/\d+_(.*?)\/\d+_(.*?)\/description$/, "$1-$2") // "123_topic/456_sub/description" → "topic-sub"
.replace(/_/g, "-"); // underscores → hyphens
This preserves all existing inbound links from external sites that linked to the old URL format.
This function powers the in-page "Jump To" navigation panel. It:
data.category
for tutorials, from slug structure for examples, from a static config
for reference).
JumpToLink
objects, marking the currently-viewed entry as
current: true.
The result is stored in a module-level
jumpToState
singleton during the build, then consumed by the
NavPanels
component.
The navigation is a two-panel system:
Both panels are managed by the
NavPanels
Preact component, which tracks open/closed state for each:
const [isOpen, setIsOpen] = useState({ main: false, jump: false });
Mobile behavior differs from desktop:
width < 768px): only one panel can be open at a time — opening one closes the other.
width >= 768px): both panels default to open.
This responsive behavior is handled with a
ResizeObserver
on
document.body
rather than CSS media queries, because the state changes need to trigger
JavaScript re-renders (not just style changes):
const documentObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (!isMobile && entry.contentRect.width < 768) {
setIsMobile(true);
setIsOpen({ main: false, jump: false });
} else if (isMobile && entry.contentRect.width >= 768) {
setIsMobile(false);
setIsOpen({ main: true, jump: true });
}
}
});
documentObserver.observe(document.body);
The jump-to state is set at build time
in each layout (e.g.,
ExampleLayout.astro
calls
setJumpToState(jumpToState)) and then read by the
NavPanels
component via the global
jumpToState
singleton from
src/globals/state.ts.
Accessibility is treated as a first-class concern with dedicated infrastructure:
Exposes four toggleable settings, stored in
localStorage:
| Setting | Effect |
|---|---|
dark-theme
|
Toggles dark color scheme via CSS class on
<html>
|
monochrome-theme
|
Replaces all colors with grayscale |
show-alt-text
|
Makes the visually-hidden alt text badge on images visible |
reduced-motion
|
Disables CSS animations and transitions |
The settings are loaded from
localStorage
and applied to
document.documentElement
on component mount. To avoid a flash of un-themed content, the
BaseLayout.astro
injects an inline
<script>
in
<head>
that reads and applies these settings before the
page renders:
<script is:inline>
const settings = ["dark-theme", "monochrome-theme", "show-alt-text", "reduced-motion"];
const storedSettings = settings.filter(s => localStorage.getItem(s) === "true");
if (storedSettings.length === 0) storedSettings.push("light-theme");
document.documentElement.className = `${storedSettings.join(" ")} ${document.documentElement.className}`;
</script>
A custom Preact hook that provides an
announce(message)
function for ARIA live region notifications:
const announce = (message: string, clearMessage = 1000) => {
const node = ref.current;
clearTimer();
node.textContent = message;
timerRef.current = setTimeout(() => {
if (node) node.textContent = "";
}, clearMessage);
};
This is used in
CopyCodeButton
(announces "Code copied to clipboard") and
CodeEmbed
(announces "Sketch is running" / "Sketch stopped" /
"Code reset"). Screen reader users receive audio feedback for
all interactive code embed actions.
Every page has a "Skip to main content" link at the very top of
the
<nav>
element:
<a href="#main-content" class="skip-to-main">Skip to main content</a>
This links to
<main id="main-content">
in
BaseLayout.astro.
The
Callout
component renders with proper
role="region"
and
aria-label, using the localized callout title as the accessible name:
<section role="region" aria-label={String(t('calloutTitles', props.title || "Try this!"))}>
The custom
Image
component renders alt text both as an HTML
alt
attribute (for screen readers) and as a visible overlay badge when
show-alt-text
is enabled. The badge is
aria-hidden
since it's redundant with the
alt
attribute — it's purely for sighted users who want to review alt text
quality.
The test suite includes
test/a11y/
— Playwright tests using
@axe-core/playwright
— which run automated accessibility audits against the rendered pages.
The "Sketches" section of the community area showcases creative
p5.js work from
OpenProcessing. Rather than making live API calls at runtime (which would fail during
static builds), all sketch data is pre-fetched and committed to the
repository as JSON files in
src/cached-data/.
Two curation JSON files exist:
openprocessing-curation-87649-sketches.json
— 2024 curation
openprocessing-curation-89576-sketches.json
— 2025 curation
Individual sketch detail files live in
src/cached-data/openprocessing-sketches/{id}.json, each containing metadata like
title,
description,
instructions,
width,
height,
mode, and
createdOn.
The API module provides typed access to this cached data:
export async function getCurationSketches(): Promise<OpenProcessingCurationResponse> {
const payload2024 = await readJson(CURATION_2024_FILE);
const payload2025 = await readJson(CURATION_2025_FILE);
// Priority sketches are featured first
const prioritySketches = payload2025
.filter((s) => priorityIds.includes(String(s.visualID)))
.sort(
(a, b) =>
priorityIds.indexOf(String(a.visualID)) -
priorityIds.indexOf(String(b.visualID)),
);
// Merge, deduplication handled by checking if individual sketch file exists
const allSketches = [...prioritySketches, ...payload2024];
const availableSketches = [];
for (const sketch of allSketches) {
if (await exists(SKETCH_FILE(sketch.visualID)))
availableSketches.push(sketch);
}
return availableSketches;
}
A handpicked
priorityIds
array ensures specific high-quality sketches from the 2025 curation always
appear at the top of the gallery.
Sketches are embedded in their own pages via the OpenProcessing embed URL:
export const makeSketchEmbedUrl = (id: number) =>
`https://openprocessing.org/sketch/${id}/embed/?plusEmbedFullscreen=true&plusEmbedInstructions=false`;
The
SketchLayout.astro
calculates an aspect ratio for the iframe based on the sketch's
width
and
height
metadata, adding 50px to account for OpenProcessing's own toolbar. A
ScalingIframe
component handles responsive scaling.
The site uses
astrojs-service-worker
(backed by
Workbox) to enable offline support and fast repeat visits.
serviceWorker({
workbox: {
globPatterns: ["**/*.{css,js,jpg,json,png,svg,ico,woff,woff2}"],
clientsClaim: true,
runtimeCaching: [
{
urlPattern: ({ url }) => url.pathname.endsWith(".html"),
handler: "CacheFirst",
options: {
cacheName: "html-pages-cache",
expiration: { maxEntries: 50, maxAgeSeconds: 24 * 60 * 60 * 7 }
},
},
],
},
}),
globPatterns.
CacheFirst
runtime strategy — served from cache immediately, network only as
fallback. Pages are cached for up to 7 days, with a maximum of 50 HTML
pages in the cache.
clientsClaim: true
ensures the service worker takes control of all open tabs immediately
upon activation, without requiring a page reload.
This is particularly important for p5.js's audience — creative coders who may work offline or in low-connectivity environments.
The
src/scripts/utils.ts
module provides the foundational utilities for all build scripts:
export const cloneLibraryRepo = async (localSavePath, repoUrl, branch) => {
const hasRecentRepo =
branch !== "main" && repoExists && (await fileModifiedSince(localSavePath));
if (!hasRecentRepo) {
await git.clone(repoUrl, localSavePath, [
"--depth",
"1",
"--filter=blob:none",
"--branch",
branch,
]);
}
};
The
--depth 1 --filter=blob:none
flags create a shallow, blobless clone — this is a Git optimization that
downloads only the tree structure without any file content initially, then
fetches only the files actually needed. This dramatically reduces clone
time for the large p5.js library repository.
The function includes a staleness check: if the local clone is less than
72 hours old and we're not on
main, it skips re-cloning. This makes incremental builds much faster.
The p5.js library's documentation preprocessor (docs/preprocessor.js) uses an absolute
process.cwd()
path to reference
parameterData.json. When cloned into an arbitrary directory, this breaks. The build script
automatically patches it to use
__dirname
instead:
preprocessorContent = preprocessorContent.replace(
"path.join(process.cwd(), 'docs', 'parameterData.json')",
`path.join(__dirname, 'parameterData.json')`,
);
getContentFilePaths()
recursively finds all
.mdx
and
.yaml
files in a directory, used by the reference and contributor-docs builders
to enumerate what needs to be generated.
rewriteRelativeMdLinks()
converts relative
.md
links (from the source library's GitHub docs) to the Astro URL
convention (no extension, trailing slash):
./access.md → ./access/
This ensures that imported markdown from external repos (like contributor-docs sourced from the p5.js library repo) has correct navigation links on the website.
The site has minimal client-side state. For the build-time inter-layout communication needed by the nav system, a module-level singleton is used:
// src/globals/state.ts
export let jumpToState: JumpToState | null = null;
export const setJumpToState = (newJumpToState: JumpToState | null) => {
jumpToState = newJumpToState;
};
This pattern works because during Astro's static build, all layouts
run in the same Node.js process, so module state is shared. Each layout
calls
setJumpToState()
with its page-specific links, overwriting any previous value. Since pages
are built sequentially (or with controlled concurrency), each page gets
its own jump-to state at render time.
The
NavPanels
component receives
jumpToState
as a prop (not imported directly from the module) to maintain clean
component boundaries.
Styling uses a combination of Tailwind CSS utility classes and SCSS modules:
Used for the majority of component-level styling. The
@astrojs/tailwind
integration generates a stylesheet from utility classes found across all
component files. Custom design tokens (colors, spacing) are defined in
tailwind.config.mjs
and referenced via CSS custom properties.
Used in a few places where Tailwind's constraint-based approach is
limiting — particularly for complex state-driven styles. The
Nav/styles.module.scss
is the primary example, handling the intricate open/closed/mobile/desktop
state matrix with nested selectors.
styles/global.scss
defines:
@font-face
declarations for the "National Park" font family (in multiple
weights, loaded as WOFF2).
.rendered-markdown
(which styles MDX-rendered content),
.content-grid, and
.skip-to-main.
styles/variables.scss
defines breakpoint constants ($breakpoint-tablet,
$breakpoint-desktop) used across SCSS modules to keep responsive breakpoints consistent.
Vitest is configured with two test projects:
test: {
projects: [
{ test: { name: "DOM", environment: "jsdom", include: ["test/**/*"] } },
{ test: { name: "node", include: ["test/pages/*"] } },
];
}
The
test/api/OpenProcessing.test.ts
file tests the curation API utilities in isolation with mock data.
The
test/a11y/
suite uses
@axe-core/playwright
to run automated WCAG compliance checks against rendered pages. These
tests are run with
npm run test:a11y
and are separate from the unit test suite.
The
.github/workflows/
directory contains:
deploy.yml
— Triggered on main branch pushes, builds and deploys to production.
beta_deploy.yml
— Deploys preview builds for PRs.
test.yml
— Runs Vitest and type checks on all PRs.
translation-sync.yml
— Runs the translation tracker to identify outdated translations.
auto-close-issues.yml
— Closes stale issues automatically.
The
.github/actions/translation-tracker/
is a custom GitHub Action written in Node.js. It compares the modification
dates of localized content files against their English counterparts and
generates a report of outdated translations, which is then posted as a PR
comment or issue update. This is a sophisticated piece of infrastructure
for maintaining multilingual documentation quality at scale.
The p5.js website is a well-engineered documentation platform with several standout technical decisions:
useLiveRegion, ARIA attributes) through to automated Playwright test coverage.
draw()
loops on pages with multiple sketches.
The codebase reflects a thoughtful balance between developer experience, end-user performance, accessibility, and the unique requirements of a creative-coding community platform.