DEV Community

Apex
Apex

Posted on Originally published at apexnexus.site

I Rebuilt My AI News Site to Feel Like an Awwwards Winner (3D, Glass, and a Chromium Quirk)

I Rebuilt My AI News Site to Feel Like an Awwwards Winner (3D, Glass, and a Chromium Quirk) My site had 94 members, 15 people online, and the visual energy of a tax form. So I spent a weekend turning apexnexus.site into the kind of page that wins Awwwards: a Three.js 3D scene in the hero, glassmorphism cards, film grain, a custom cursor, Lenis smooth scroll, an aurora gradient that spins behind the headline. This is the story of what I added, what broke, and the single weirdest CSS bug I have ever debugged. It cost me $0. It took a lot of Playwright screenshots. ## The 3D scene that took 40 lines The hero now runs a real WebGL scene: a wireframe icosahedron with a glowing core, two orbit rings in cyan and violet, and a shell of 150 particles. It rotates on its own and leans toward your mouse. No Three.js framework wrapper, no OrbitControls, no asset pipeline. Just:

var mesh = new THREE.Mesh( new THREE.IcosahedronGeometry(1.35, 1), new THREE.MeshBasicMaterial({ color: 0x34d399, wireframe: true, transparent: true, opacity: 0.5 })
);
scene.add(mesh);
Enter fullscreen mode Exit fullscreen mode


The mouse reaction is just lerping two floats toward the cursor position every frame. That is the whole trick:

mx += (tx - mx) * 0.05;
mesh.rotation.x += 0.0025 + my * 0.008;
Enter fullscreen mode Exit fullscreen mode


Four rules made it sane on mobile: 1. Cap pixel ratio at 2 (Math.min(devicePixelRatio, 2)) or iPhones melt.

  1. Cut particle count from 150 to 60 on touch devices.
  2. Respect prefers-reduced-motion: render one static frame, stop the loop.
  3. Keep a pure-CSS atom fallback that shows if WebGL is missing. An iPhone renders 60 particles and one canvas. A desktop gets the full scene. Nobody notices the difference except the battery. ## The glass card trick Cards got backdrop-filter: blur(14px) with a gradient background and a 1px highlight line on the top edge: css.card::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.14), transparent); } That one pixel line is doing more work than the blur. It reads as "light hitting the top of glass" and instantly separates the card from the background. ## The custom cursor nobody asked for A 6px emerald dot that snaps to the mouse, plus a 34px ring that lerps behind it. The ring grows to 52px and glows when you hover anything interactive. It is the cheapest "this site is alive" signal there is: css #cursor-ring { transition: width.25s, height.25s, border-color.25s; } body.cursor-hover #cursor-ring { width: 52px; height: 52px; } Hidden entirely on pointer: coarse. Touch users never see it, desktop users never stop noticing it. ## The bug that made me question flexbox I added a marquee ticker under the nav: FREE LEARNING HUB, SECURITY PROTOCOLS, BUILD IN PUBLIC, NO PAYWALLS, DAILY AI. The classic pattern. Track is two copies of the text, animate translateX(-50%), seamless loop. On desktop: flawless. On iPhone and Android emulation: the words rendered on top of each other. The second copy of the group was drawn directly over the first. I tried everything. flex-shrink: 0 on groups. width: max-content on the track. inline-flex shrink-wrap. Every fix passed on desktop and failed on mobile. The track kept collapsing to the viewport width no matter what I set. Here is the smoking gun. I set the track width to 9999px inline in DevTools: js track.style.width = '9999px'; track.style.minWidth = '9999px'; And got: inline width 9999px, computed width 9999px, bounding rect 9999px. Remove the minWidth line and the computed width snaps back to 390px. The element was ignoring a plain width on a flex container, in Chromium, only when the content was wider than the viewport. The fix is a two-line JS size function that measures the real rendered group width and sets both width and minWidth to 2x, re-running on font load, window load, and resize: js function size() { var w = group.getBoundingClientRect().width * 2; track.style.width = w + 'px'; track.style.minWidth = w + 'px'; // the only thing Chromium honors } I still do not fully understand why width alone was ignored. The lesson is simpler: when a marquee misbehaves, do not trust max-content. Measure the rendered box and force both properties. ## The iOS overflow that broke everything Same release, different trap: the background parallax layer used position: fixed; inset: -30px so the parallax translate would never show edges. On iPhone that inflated the document width by 100+ pixels, producing a horizontal scroll on every page. The fix was inset: 0 plus a scale(1.06) inside the parallax transform. Same visual result, zero layout overflow. Verify with scrollWidth === clientWidth on an iPhone emulation before you ship anything with fixed elements. ## What actually moved the needle The redesign is nice. The numbers are the point: 94 members, 15 online, all still free, still $0/month to run, rebuilt with no paid assets and no paid libraries. If you are building a content site in 2026, skip the template. Do these five things and you are 80% of the way to feeling expensive: 1. One WebGL scene in the hero, mouse-reactive, mobile-capped.
  4. Glass cards with a top highlight pixel.
  5. Film grain overlay at 5% opacity.
  6. Custom cursor for fine pointers only.
  7. A marquee that you measured, not one you guessed. And if your marquee overlaps on mobile: measure the group, set width and minWidth, re-run on document.fonts.ready. Trust me on that one. --- 🌐 Live: apexnexus.site - the free AI Nexus learning hub (guides, cheat sheets, daily AI news) 💬 Free AI automation community: Discord

Top comments (0)