Nasaq

Project setup

Both install routes end in the same steps: Tailwind CSS v4 with the Nasaq tokens, a provider around the app, and fonts.

Both install routes end in the same steps: Tailwind CSS v4 with the Nasaq tokens, a provider around the app, and fonts.

Requirements

  • React 19 or newer.
  • Tailwind CSS v4 (CSS-first configuration; there is no tailwind.config.js).
  • Node 22 or newer for the tooling.

Tokens and Tailwind

With the shadcn route, the nasaq preset writes the tokens into your global CSS. With the npm route, import them:

@import "tailwindcss";
@import "@fadymondy/nasaq/tokens.css";
@import "@fadymondy/nasaq/theme.css";
@import "@fadymondy/nasaq/web/styles.css";
@source "../node_modules/@fadymondy/nasaq/dist/web";

tokens.css defines the --nq-* custom properties. theme.css exposes them to Tailwind so utilities such as bg-nq-surface, text-nq-fg and border-nq-line exist. The shadcn bridge variables (--background, --foreground, --primary, --border, --ring) are defined from the same tokens, so bg-background and bg-primary also follow the theme and the brand.

The provider

Wrap the app once, near the root. It sets data-brand, data-theme, data-density, data-expression, lang and dir on <html>, and gives every component the Base UI direction context.

import { NasaqProvider } from "@/components/nasaq/nasaq-provider"; // npm: "@fadymondy/nasaq/web"

export function Providers({ children }: { children: React.ReactNode }) {
  return <NasaqProvider brand="nasaq">{children}</NasaqProvider>;
}

To avoid a flash of the wrong theme on first paint, put the theme script in <head>. It reads the saved choice (localStorage["nasaq-theme"]) or the system setting and applies the theme before the page renders. In Next.js:

import { nasaqThemeScript } from "@/components/nasaq/theme-script"; // npm: "@fadymondy/nasaq/web"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: nasaqThemeScript() }} />
      </head>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

suppressHydrationWarning is needed because the script changes <html> before React hydrates.

Fonts

The type tokens name font families but never load them, so you choose how:

TokenStackUsed for
--nq-font-sansInter, Alexandria, system-uiLatin UI text
--nq-font-arabicLusail, Inter, Alexandria, IBM Plex Sans Arabic, system-uiUI text when lang is ar
--nq-font-monoJetBrains Mono, Lusail, ui-monospaceCode, tabular figures

Inter, JetBrains Mono and Alexandria are open-licensed and available from Fontsource. This site loads them like this:

pnpm add @fontsource/inter @fontsource/jetbrains-mono @fontsource/alexandria
import "@fontsource/inter/400.css";
import "@fontsource/inter/500.css";
import "@fontsource/inter/600.css";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/alexandria/400.css";
import "@fontsource/alexandria/500.css";

Lusail is the Arabic face of the author's own products. It is licensed to those products and is not distributed with Nasaq or bundled into this site. When it is not loaded, the stack falls through to Alexandria, so nothing breaks. If you have a licence for a different Arabic face, put it first in --nq-font-arabic.

Dark mode class

shadcn's components expect a .dark class. The provider and the theme script set it together with data-theme, so both kinds of selector work. If you use next-themes or your own switcher, keep both in step, or let NasaqProvider own the theme. See Dark mode.

Check it works

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

export default function Page() {
  return (
    <main className="p-6">
      <Badge variant="outline">0.1.0</Badge> <Button>It works</Button>
    </main>
  );
}

The button takes the brand's action colour (green for the default nasaq brand), and the page background is warm ivory in light and deep navy in dark.

On this page