Skip to main content
UI/UXDesignAccessibility

Design Systems: Balancing Aesthetics & Accessibility

elango
elango

Full Stack Engineer

Updated
7 min read
Design Systems: Balancing Aesthetics & Accessibility

Accessibility is a design system constraint, not a audit checkbox

When I designed elangodev.com, I wanted premium gradients, glass panels, and bold typography — the same visual language as blog and handbook pages. I also needed WCAG-conscious contrast, keyboard paths, and honest cookie consent before Google Analytics and AdSense load. Those requirements shaped components I reuse everywhere: CookieConsent, ThemeContext, blog prose classes, and PublicNav.

A design system on a solo portfolio is not Storybook with fifty variants. It is a handful of enforced patterns — semantic HTML first, ARIA when native elements are insufficient, dark mode tokens that preserve readability, and consent gates that respect privacy law and user trust.

CookieConsent: accept, reject, and real side effects

CookieConsent renders a fixed bottom dialog with role="dialog", aria-label, and aria-live="polite" so screen readers announce the banner when it appears. Two buttons — Essential Only and Accept All — map to CONSENT_ESSENTIAL and CONSENT_ACCEPTED in lib/consent.js. setConsent writes localStorage and dispatches cookie-consent-change so AnalyticsTracker and AdSenseScript react without reload.

export function hasAnalyticsConsent() {
  return getStoredConsent() === CONSENT_ACCEPTED;
}

export function setConsent(value) {
  localStorage.setItem(CONSENT_KEY, value);
  window.dispatchEvent(
    new CustomEvent("cookie-consent-change", { detail: value })
  );
}

Essential Only is not a dark pattern hidden in gray text — it is a peer button with equal visual weight to Accept All, placed first on mobile stacks for thumb reach. Privacy Policy links to /privacy with underline and sufficient color contrast in both light and dark themes. I wrote copy that names Google Analytics and AdSense explicitly because vague "we use cookies" language fails users and regulators.

ThemeContext and dark mode without flash

ThemeProvider in context/ThemeContext.jsx loads saved theme from localStorage on mount, toggles the dark class on document.documentElement for Tailwind dark: variants, and wraps MUI CssBaseline for dashboard widgets that still use Material components. Until mounted, it renders children without MUI theme to avoid hydration mismatch — a compromise I document so future refactors know why the tree briefly lacks MuiThemeProvider.

const changeTheme = useCallback((newTheme) => {
  setTheme(newTheme);
  localStorage.setItem("theme", newTheme);
  if (newTheme === "dark") {
    document.documentElement.classList.add("dark");
  } else {
    document.documentElement.classList.remove("dark");
  }
}, []);

layout.jsx sets suppressHydrationWarning on html because theme class may differ between server HTML and client storage. Blog prose uses dark:prose-invert so long articles remain readable at night — slate body text, white headings, blue links with hover underline. I test contrast on prose-p and prose-headings pairs, not just hero sections.

Blog typography as a reusable prose token

PostDetailContent.jsx wraps article HTML in a long Tailwind prose class string — prose-lg, prose-slate, custom heading weights, relaxed paragraph leading, rounded images. Generated blog content from my editorial pipeline outputs semantic h2 sections; the prose layer standardizes spacing and font rhythm without inline styles in the database.

That separation lets me restyle every published post by editing one className. Headings use font-black tracking-tighter for brand alignment with the homepage hero. Links stay blue with underline on hover — never low-contrast gray on purple gradients inside article bodies.

PublicNav: keyboard, focus, and mobile drawer

PublicNav spans elangodev.com marketing routes — blog, services, handbook, contact. Desktop links are real anchor elements via Next Link, not div click handlers. On the homepage, section jumps use buttons only when already on / with explicit scrollIntoView behavior; elsewhere, hash links route home first.

Mobile menu toggles with AnimatePresence; focus should trap inside the drawer when open — an improvement still on my backlog — but icon buttons include aria labels on scroll-to-top and menu close controls elsewhere on the site. I avoid outline-none without replacement focus rings on interactive elements in new code.

Forms and auth accessibility

Login inputs pair visible labels with focus ring styles — ring-indigo-500/50 on keyboard focus. Error messages from Supabase auth render in text with AlertCircle icon and readable red on dark backgrounds. Submit buttons disable during isLoading to prevent double POST, with state communicated visually and via button opacity.

Dashboard modals use Drawer components with titled sections. I prefer native button type="button" for non-submit actions so Enter in text fields does not accidentally close dialogs.

Ads and analytics within consent boundaries

AdSenseScript returns null until consent — screen readers never encounter hidden ad iframes prematurely. When ads load, I place AdUnit components inside article flow with labels, avoiding deceptive placement near navigation that looks like menu items. AnalyticsTracker skips localhost and waits for idle time — reducing surprise network activity during keyboard navigation audits.

Process: how I keep aesthetics and a11y aligned

I run Lighthouse accessibility on /, /blog, and /login before major releases. I manually tab through CookieConsent and PublicNav. When I add blog and handbook pages, I check color contrast on generated CSS output — especially yellow-on-white in gradient generators.

Design tokens live in Tailwind config and component class strings I copy intentionally, not accidentally. Dark mode is not an inverted screenshot — I pick slate palettes that preserve separation between card, border, and text.

What inclusive premium design means to me

Balancing aesthetics and accessibility on elangodev.com means users who accept cookies, reject tracking, prefer dark mode, or navigate by keyboard all get a coherent experience. CookieConsent and ThemeContext are as much my design system as rounded-4xl nav pills and prose typography on blog posts.

I would rather ship one accessible pattern reused ten times than ten bespoke hero sections with unique contrast failures. That discipline is how my portfolio demonstrates engineering maturity — not only motion and cloud architecture, but interfaces that welcome every visitor who lands on elangodev.com.

Accessibility on editorial pages

Headings on blog posts follow a single h1 with structured h2/h3 sections. Buttons and links are distinguishable. Cookie banner actions are keyboard reachable. These are not optional if you want a professional publication — and they correlate with the trust signals AdSense reviewers notice when skimming.

I test with keyboard only after major nav changes. Image alt text on covers describes the topic, not “image1”. Prefer reduced-motion for non-essential animation.

Design tokens without a bloated system

This site uses Tailwind utility patterns with a restrained palette. A full design-system monorepo would be overkill for one author’s publication. Accessibility and consistency beat component count.

Field notes unique to design systems accessibility

I keep this article in the elangodev.com corpus because it captures decisions I made while shipping features on this domain or in client systems I can discuss publicly. If a section ever drifts into generic textbook language, I rewrite it with a concrete file path, metric, or failure story. That editorial rule is how we avoid another round of low-value AdSense feedback.

Readers should leave with a default recommendation, a “do not do this” warning, and a verification step. For example: change one config, deploy to preview, measure one metric, then promote. Tutorials that never tell you how to know it worked are incomplete — I try to close every guide with a verification habit I actually use.

Finally, I cross-link related handbook chapters and sibling posts sparingly. Internal links should help humans navigate a topic cluster, not manufacture a crawl graph of thin pages. After the unpublish pass, only keepers that clear our depth bar remain published and sitemap-listed.

I will continue expanding this guide as production behavior on elangodev.com changes — new failure modes, new metrics, new code paths. Treat the article as a living engineering note, not a static SEO page. That maintenance posture is part of how a real publication compounds value for readers and for crawlers evaluating originality over time.

elango

Written by

elango

Full Stack Engineer & Software Architect specializing in React, Next.js, and high-performance web applications.

Share on:

Discover More

View blog