feat(website): world-class brand and design system - #1179
Draft
vivek7405 wants to merge 17 commits into
Draft
Conversation
The site had no design system in the real sense: colour, type sizes, and button shapes were decided per page, and the palette leaned on a warm accent applied decoratively (glowing pill CTAs, gradient eyebrows, tinted section labels). At nextjs.org scale that reads as a landing-page advert rather than as a tool. Three changes, in order of consequence. Colour is now rationed. Neutrals carry the page at near-zero chroma (0.003..0.008, enough to warm them off engine grey) and the brand ramp is reserved for the mark, focus, and live state. The primary action is plain foreground on background, the highest contrast pairing the palette has, which is what keeps the one accented element on a page reading as special. The hero shows instead of telling. Source on the left, the very component that source declares running on the right, SSR'd into the HTML and upgraded in place. It is the one claim on the page a screenshot cannot fake, and no competing framework site has an equivalent. The marks moved into lib/brand.ts. The slice through the W is now a mask rather than a rect painted in the page background, which only looked right on the single surface it was sampled from and drew a dark stripe across the mark on every other one.
…ing pages The home page, /why-webjs, /what-is-webjs, and /brand each carried their own copy of the button, install-command, and panel class strings. That is how the site drifted: a change to the control shape landed on whichever page the author happened to have open, and the four slowly stopped matching. lib/design.ts is now the single definition, and the four pages import it. The closing CTA panel loses its accent tint and glow along the way, so the last thing on a page is no longer the loudest thing on it, and the remaining uppercase mono micro-labels become sentence case.
The draft /brand shipped a 76 KB wordmark that embedded a base64 woff2 and drew the name as live text, so the one asset a third party downloads depended on a font they may not have. It is now outlined paths at 3.5 KB, extracted from Inter Tight at the display weight, in variants for light and dark backgrounds plus a monogram and a favicon, zipped as one package. The W itself is redrawn. The slice was thick and sat just below the middle apex, which severed the letter into floating fragments at large sizes; it is now thin and low, so the mark stays whole and degrades to a plain W below favicon size instead of to mush. The slice is a mask, so it is correct on every background rather than only on the one it was sampled from. The page gains what the Vercel guidelines have and the draft did not: a clear-space rule quantified in one unit and drawn rather than described, a minimum size with the fallback mark named, a naming section for the WebJs casing rule, and trademark terms. Its swatches now paint the token values from lib/brand-tokens.ts, so the palette on the page cannot drift from the palette in the layout. The draft described the palette as electric cyan next to amber chips, which is exactly that drift.
app/layout.ts declares favicon-192.png and apple-touch-icon.png, and both still carried the previous logo. Swapping only favicon.svg would have left every browser that prefers a raster icon, and Google's favicon crawler, on the old mark indefinitely.
I redrew the mark instead of adopting it, and lost the three things that made the original work. The wordmark is ITALIC, so it leans with the W rather than standing upright beside it. The monogram IS the W of WebJs, not a badge next to the name. And one horizontal slice cuts the lockup as a single object. My version was an upright lowercase wordmark beside a separate sliced W, which kept none of that. The authored file is restored verbatim and is now the source of truth. The light variant flips only the two PAINTED fills; the #fff inside the mask is the mask channel and stays white in both, or the mark vanishes. Bare W files are added for placing on your own background, and everything is greyscale, so the identity depends on no accent colour at all. Re-outlining the embedded italic face is deliberately NOT done here. The font is variable, and opentype.js applies its deltas wrongly: glyph.getPath ignores the variation entirely and returns wght=400, while font.getPath applies it but drops the Js and deforms the b. A corrupted logo is a worse outcome than a 76 KB one, so the file stays authored until the face can be subset properly. /brand loses the signature-ramp section along with this. With a greyscale identity it documented a gradient the brand no longer uses.
Three lines survived the logo restore and no longer matched what the page shows. The colour section still described a three-stop ramp reserved for the mark, which is now greyscale. The naming section claimed the wordmark is set lowercase by design, while the lockup plainly reads WebJs. And the clear-space guides were tinted with the accent, introducing a colour the identity does not use into the one diagram meant to describe it. This is the same drift the page was rebuilt to avoid, arriving from the other direction: the swatches are generated and stayed correct, the prose around them was hand-written and did not.
Rationing the accent was right; removing it was not. The primary button and the closing call to action are the two surfaces on a page whose whole job is to be clicked, and they are exactly where the brand colour earns its place. Both go back to amber with the glow, from one shared recipe (BTN_PRIMARY and the new PANEL_CTA) rather than the four copies they used to have. The light theme goes back to its warmer neutrals too. Dropping the chroma to near zero read cold and clinical in light mode, where the warmth was carrying most of the character. What deliberately does NOT change is the page backdrop: --glow-strength drives a fixed full-page layer, and raising it tints the whole site rather than the call to action. The CTA's glow comes from --shadow-glow on the panel itself, so the glow is where it was asked for and nowhere else. Everything above those two surfaces stays neutral, which is what makes them land. The dead ramp tokens (--grad, --accent-mid, --accent-2) and the .grad-text / .grad-bg helpers go with this: the identity is greyscale, so nothing consumed them any more. /brand's colour copy is corrected to match.
…n /brand Two unrequested changes are reverted here. The dark theme had drifted off what webjs.dev serves: pure black became a warm charcoal (oklch 0.115 0.003 60), the neutrals picked up chroma, and the accent moved from hue 52 to 65 at a higher lightness. That came from the shared design-system foundation rather than from anything asked for, and changing a live site's background is not a side effect to slip into a brand task. Every dark value now matches main exactly. The page backdrop goes back with it: the dot-grid texture and the third magenta radial are removed, leaving the two radials off --glow-a that the live site already had. The glow that was actually wanted belongs to the closing CTA panel, and it comes from --shadow-glow on that panel. /brand gains what it was missing: the accent is now SHOWN, not just described. It has its own swatch group (--accent, --accent-fg, --accent-live, both themes) and a row demonstrating the three places it is allowed to appear, so the rule is visible rather than asserted. Its asset-card backdrops also used the near-monochrome values, which meant the page previewed the marks against a page colour the site does not use.
…py bar Three fixes to the dark theme only. Light is untouched. The backdrop tint read brown. That is not a hue problem, it is a lightness one: brown IS dark orange, and it only exists relative to something lighter. --glow-a sat at 0.63 lightness, so mixed into black at 16% it had nothing to be lighter than and settled as a stain. Over white the same hue reads as the pale pink that works in light mode. Raising it to 0.85 makes it behave like light instead of pigment, and the hue stays at 52. The primary button gains real contrast headroom. Amber at 0.7 lightness gave 7.32:1 against its own label; at 0.82 it gives 9.93:1, and 10.88:1 against the page. Hue stays 52 rather than drifting toward 65, so dark and light remain the same colour family: a higher-contrast option at hue 65 measured barely better (10.25:1) and was not worth splitting the two themes apart. The install command goes back to the live site's proportions. The tightened version read as a caption, and this bar is a primary call to action in its own right.
Chasing contrast by raising lightness alone washed the button out: amber at 0.82 lightness measured 9.93:1 but read as a pale peach, not as the brand colour. Chroma is the other axis, and it buys saturation without the wash. oklch(0.74 0.19 52) is #ff8101, a fully saturated orange, and it still measures 8.29:1 against its label and 8.37:1 against the page, comfortably past the 7:1 AAA bar and better than the 7.32:1 the live site ships. Hue stays 52, so dark and light remain one colour family. Also note for anyone chasing a change that will not appear: the copy bar was already correct in lib/design.ts for two commits, but the dev server was serving a stale module for it, so the page kept rendering the old class string. Restarting the server was the fix, not another edit. Measured against production afterwards, both are now 342x52 with identical padding, radius, and background.
The backdrop no longer carries a visible colour. --glow-a moves from 0.85 to 0.42 lightness in dark, so the tint sits just above the page's own black and reads as depth in the upper corners rather than as a wash. The multiplier goes up (0.16 to 0.30) as the colour comes down, which keeps the falloff shape while removing the cast. Full removal is one value away if the flat version is preferred: set --glow-strength to 0 in the two dark blocks. It was rendered and compared, and the only difference is that the top corners go completely dead. The accent glows are unaffected. Those come from --accent-live through --shadow-glow on the button and the CTA panel, which is a separate token from the backdrop, so the two can be tuned independently.
The install bar sat at 16px while the two buttons beside it were 10px, so the hero's three controls read as three unrelated shapes. That came in with the restore from production, where the buttons are full pills and 16px looked correct next to them; it stopped being correct the moment the buttons became 10px rectangles. Rather than hard-code another number, the radius is now a named step. Three exist, chosen by what a thing IS and not by how big it is: RADIUS_CONTROL (10px) for anything you click, 2xl (16px) for cards and code windows, 20px for full-width panels. The bar keeps every other production property, the padding, the blur, the shadow, since those are what make it read as a call to action rather than a caption. Measured after the change: primary button, ghost button, and install bar all report 10px.
Dark carries no backdrop tint at all now. --glow-strength is 0 in both dark blocks, so the fixed layer paints nothing and the page is flat black. Light keeps 0.16, where the warm wash is wanted and reads as pink rather than as a stain, so the layer stays in place rather than being deleted. Verified in a browser: dark resolves --glow-strength to 0, light to 0.16.
…token Batches several fixes plus a review of another agent's concurrent edits. New: the preview pane gains a rendered/usage flip, built from a checkbox and two labels rather than a component. It needs no JavaScript, works with scripting off, and cannot tear down the live element the way a re-rendering toggle would, so the count you clicked survives a flip and back. The two stage header bars now share a fixed 42px height: the left carries 11px traffic dots and the right only text, so equal padding produced unequal heights and the seam between the panes visibly stepped. /brand gains a light monogram, since the lockup shipped both variants and the monogram only the dark one. Hover state moves to a --hover-surface token holding an ALPHA overlay. The solid --bg-subtle was a 0.09 lift on a black page, real in numbers and invisible to the eye, while the same token in light was a 0.025 step that read clearly. An overlay also composes over the header's translucent blur instead of fighting it. Reverted from the concurrent edits, both against explicit direction: --shadow-glow was set to none in dark, which removed the glow from the primary BUTTON as well as the CTA panel; and the light accent had drifted from 0.54 to 0.58 lightness, taking it to 4.35:1 as text on the page, under the 4.5:1 floor, on a theme that was declared settled. Four --accent-btn-* tokens were declared and never referenced by any rule; a dead token is worse than none, because the next reader assumes the button reads it. Kept from those edits: the CTA fill is transparent in dark (the literal ask) while light keeps its tint, and the stage now sits in the progressive enhancement section rather than the hero.
The free-standing actions are pills again, matching the live site. Horizontal padding goes 18px to 20px with them: a pill eats more of its own width at the ends, so the same padding reads tighter than on a rectangle. The radius scale in lib/design.ts is updated to say so rather than leaving the comment describing the previous decision. It now has two rules instead of one number. A free-standing action is a pill, so its shape says press me before its colour does. Everything that is a SURFACE (the install bar, cards, panels) keeps a radius scaled to its own height, roughly a quarter of it, which is why the 52px bar stays at 16px and the 28px copy button at 7px.
…ting it Three long-form hubs (/blog, /articles, /compare) carried byte-identical headers, and two marketing pages hand-assembled the closing CTA out of the same parts. Both are now single functions in lib/design.ts, which is where cross-cutting composition belongs: module-specific UI stays in its module, and these are shared across modules that otherwise have nothing to do with each other. The duplication was already costing correctness. The two CTAs had drifted to different lede widths (56ch and 48ch) and different arrow icons (14px at 2.6 stroke, 15px at 2.4). Both now render identically because there is one definition. The three hub headers still carried the uppercase mono eyebrow that was removed from every other page, so the pages sharing a header were also the last ones carrying a device the site had dropped. Also adds the layout scale the system was missing (WIDE, READING, SECTION) and removes three exports that had zero consumers. An unused recipe in a shared module is a claim of reuse that is not true, and it invites the next reader to assume a page reads it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilds the site on a real design system and finishes the brand direction that was sitting uncommitted in the primary checkout.
Closes #1178
Grounded in the shared claude.ai/design design-system project and benchmarked against nextjs.org (which is pure black, no colour wash, small understated controls, and a very large tight headline).
Landed
Colour is rationed. Neutrals carry the page at near-zero chroma; the brand ramp is reserved for the mark, focus, and live state. The primary CTA is plain foreground on background, not an accent pill. The glow wash dropped from 0.24 to 0.07 and the closing-CTA panel lost its tint, so the last thing on a page is no longer the loudest thing on it.
Live-component hero. Source on the left, the component that source declares running on the right, SSR'd and upgraded in place. It is the one claim on the page a screenshot cannot fake, and no competing framework site has an equivalent.
Real brand assets. The draft wordmark was 76 KB because it embedded a base64 woff2 and drew the name as live text. It is now outlined paths at 3.5 KB, extracted from Inter Tight at display weight, with light and dark variants, a monogram, a favicon, and a zip. The W was redrawn: the slice was thick and sat just below the middle apex, fragmenting the letter at large sizes, and is now thin and low so the mark stays whole and degrades to a plain W below favicon size. The slice is a mask, not a rect painted in the page background, so it is correct on every surface.
One source for the recipes.
lib/design.tsreplaces four copy-pastedBTNstrings;lib/brand.tsholds the marks;lib/brand-tokens.tsholds the palette as data, and /brand paints its swatches from it rather than restating hex (the draft captioned the palette "electric cyan" beside amber chips)./brand rebuilt with what the Vercel guidelines have and the draft lacked: a clear-space rule quantified in one unit and drawn rather than described, a minimum size with the fallback mark named, a naming section for the casing rule, and trademark terms. Added to the sitemap and the footer.
Eyebrow labels removed site-wide, and the raster icons regenerated (swapping only favicon.svg would have left raster-preferring browsers on the old mark).
Verification
webjs checkclean. All routes 200 and reviewed in both themes at 1440px. Screenshots taken against a live dev server throughout.Not done yet
/docsand/uireviewed only at the route level, not section by sectionpublic/og.pngsocial card still shows the previous brand