Skip to content

Next.js Setup Guide

Airframe works seamlessly with Next.js in both App Router and Pages Router.

npm install @airframeui/core
# or
pnpm add @airframeui/core

Create or update app/globals.css:

@import "@airframeui/core/core.css";

Then import it in app/layout.tsx:

import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Create or update styles/globals.css:

@import "@airframeui/core/core.css";

Then import it in pages/_app.tsx:

import '../styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

Now you can use Airframe classes anywhere:

// app/page.tsx (App Router) or pages/index.tsx (Pages Router)
export default function Home() {
  return (
    <div className="af-container">
      <div className="af-stack af-gap-lg">
        <h1 className="af-title">Welcome</h1>
        <button className="af-btn">Get Started</button>
      </div>
    </div>
  )
}
// app/dashboard/page.tsx
export default function Dashboard() {
  return (
    <div className="af-container">
      <div className="af-stack af-gap-lg">
        <header className="af-inline af-justify-between">
          <h1 className="af-title">Dashboard</h1>
          <button className="af-btn">New Item</button>
        </header>
        <div className="af-grid">
          <div className="af-col-span-8@lg">
            <div className="af-card">
              <h2 className="af-card__title">Main Content</h2>
              <p className="af-card__body">Content goes here</p>
            </div>
          </div>
          <div className="af-col-span-4@lg">
            <div className="af-card">
              <h2 className="af-card__title">Sidebar</h2>
              <p className="af-card__body">Sidebar content</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}
// app/contact/page.tsx
export default function Contact() {
  return (
    <div className="af-container-sm">
      <form className="af-stack">
        <div className="af-field">
          <label htmlFor="name" className="af-field__label">
            Name
          </label>
          <input type="text" id="name" name="name" className="af-input" required />
        </div>
        <div className="af-field">
          <label htmlFor="email" className="af-field__label">
            Email
          </label>
          <input type="email" id="email" name="email" className="af-input" required />
        </div>
        <div className="af-field">
          <label htmlFor="message" className="af-field__label">
            Message
          </label>
          <textarea id="message" name="message" className="af-textarea" rows={5} required />
        </div>
        <button type="submit" className="af-btn">
          Send Message
        </button>
      </form>
    </div>
  )
}
// app/components/Counter.tsx
'use client'
import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div className="af-card">
      <h2 className="af-card__title">Counter</h2>
      <div className="af-card__body af-stack">
        <p className="af-title">{count}</p>
        <div className="af-inline af-gap-sm">
          <button className="af-btn" onClick={() => setCount(count - 1)}>
            Decrease
          </button>
          <button className="af-btn" onClick={() => setCount(count + 1)}>
            Increase
          </button>
        </div>
      </div>
    </div>
  )
}
// pages/index.tsx
export default function Home() {
  return (
    <div className="af-container">
      <div className="af-stack af-gap-lg">
        <h1 className="af-title">Welcome</h1>
        <button className="af-btn">Get Started</button>
      </div>
    </div>
  )
}
// pages/about.tsx
import Layout from '../components/Layout'

export default function About() {
  return (
    <Layout>
      <div className="af-container">
        <div className="af-stack af-gap-lg">
          <h1 className="af-title">About Us</h1>
          <div className="af-card">
            <p className="af-card__body">Content goes here</p>
          </div>
        </div>
      </div>
    </Layout>
  )
}

Override --af-* in globals.css. Force light or dark with data-theme on <html>. JSX uses className, not class. Full workflow: Theming.

TypeScript support is included. No additional types needed.

Airframe is CSS-only, so it works perfectly with Next.js Server Components:

// app/products/page.tsx
export default async function Products() {
  const products = await fetchProducts()
  return (
    <div className="af-container">
      <div className="af-grid">
        {products.map(product => (
          <div key={product.id} className="af-col-span-6@md af-col-span-4@lg">
            <div className="af-card">
              <h2 className="af-card__title">{product.name}</h2>
              <p className="af-card__body">{product.description}</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

Make sure you’re importing the CSS in the correct file:

  • App Router: app/globals.css imported in app/layout.tsx
  • Pages Router: styles/globals.css imported in pages/_app.tsx

Check that:

  1. CSS is imported correctly
  2. Classes use className (not class) in JSX
  3. No conflicting CSS is overriding styles

If you see build errors, ensure:

  • Your Node version meets the minimum required by your Next.js version
  • All dependencies are installed
  • TypeScript is configured correctly