Skip to main content
1

Install dependencies

Server-side rendering requires the component and html-template packages:
npm i remix
All necessary packages are included in the main remix package.
2

Create a render utility

Set up a helper function to render components to HTML streams:
app/utils/render.ts
import type { RemixNode } from 'remix/component'
import { renderToStream } from 'remix/component/server'

export function render(node: RemixNode, init?: ResponseInit) {
  let stream = renderToStream(node, {
    onError(error) {
      console.error('Render error:', error)
    },
  })

  let headers = new Headers(init?.headers)
  if (!headers.has('Content-Type')) {
    headers.set('Content-Type', 'text/html; charset=UTF-8')
  }

  return new Response(stream, { ...init, headers })
}
The renderToStream function converts your components into a streaming HTML response, enabling faster time-to-first-byte.
3

Create a document layout

Build a base HTML document component:
app/layout.tsx
import type { RemixNode } from 'remix/component'
import { css } from 'remix/component'

interface DocumentProps {
  title?: string
  children: RemixNode
}

export function Document({ title = 'My App', children }: DocumentProps) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{title}</title>
        <link rel="stylesheet" href="/styles.css" />
      </head>
      <body>
        <nav mix={[css({ padding: '1rem', background: '#f0f0f0' })]}>
          <a href="/">Home</a>
          {' | '}
          <a href="/about">About</a>
          {' | '}
          <a href="/contact">Contact</a>
        </nav>
        <main mix={[css({ padding: '2rem', maxWidth: '1200px', margin: '0 auto' })]}>
          {children}
        </main>
      </body>
    </html>
  )
}
The css function allows you to write inline styles with full TypeScript support.
4

Create page components

Build reusable page components:
app/pages/home.tsx
import { css } from 'remix/component'
import { Document } from '../layout.tsx'

interface HomePageProps {
  userName?: string
  posts: Array<{ id: number; title: string; excerpt: string }>
}

export function HomePage({ userName, posts }: HomePageProps) {
  return (
    <Document title="Home">
      <h1>Welcome {userName ? userName : 'Guest'}!</h1>

      <section mix={[css({ marginTop: '2rem' })]}>
        <h2>Recent Posts</h2>
        <div mix={[css({ display: 'grid', gap: '1rem', marginTop: '1rem' })]}>
          {posts.map(post => (
            <article
              key={post.id}
              mix={[
                css({
                  padding: '1.5rem',
                  border: '1px solid #ddd',
                  borderRadius: '8px',
                }),
              ]}
            >
              <h3 mix={[css({ marginBottom: '0.5rem' })]}>                    
                <a href={`/posts/${post.id}`}>{post.title}</a>
              </h3>
              <p mix={[css({ color: '#666' })]}>                    
                {post.excerpt}
              </p>
            </article>
          ))}
        </div>
      </section>
    </Document>
  )
}
5

Create route handlers

Connect your components to routes:
app/router.ts
import { createRouter } from 'remix/fetch-router'
import { routes } from 'remix/fetch-router/routes'
import { render } from './utils/render.ts'
import { HomePage } from './pages/home.tsx'
import { Document } from './layout.tsx'

export let appRoutes = routes({
  home: 'GET /',
  about: 'GET /about',
  posts: {
    show: 'GET /posts/:id',
  },
})

export let router = createRouter()

// Home page with data fetching
router.get(appRoutes.home, async () => {
  let posts = [
    {
      id: 1,
      title: 'Getting Started with Remix',
      excerpt: 'Learn the basics of building web apps with Remix.',
    },
    {
      id: 2,
      title: 'Advanced Routing Patterns',
      excerpt: 'Explore nested routes and dynamic segments.',
    },
  ]

  return render(
    <HomePage userName="Alice" posts={posts} />
  )
})

// About page
router.get(appRoutes.about, () => {
  return render(
    <Document title="About">
      <h1>About Us</h1>
      <p>We build amazing web applications with Remix.</p>
    </Document>
  )
})

// Post detail page
router.get(appRoutes.posts.show, ({ params }) => {
  let post = {
    id: Number(params.id),
    title: 'Getting Started with Remix',
    content: 'This is a detailed post about Remix...',
  }

  return render(
    <Document title={post.title}>
      <article>
        <h1>{post.title}</h1>
        <p>{post.content}</p>
        <a href="/">Back to home</a>
      </article>
    </Document>
  )
})
6

Add CSS styling

Use the css helper for scoped, type-safe styles:
import { css } from 'remix/component'

function Button({ children, primary }: { children: string; primary?: boolean }) {
  return (
    <button
      mix={[
        css({
          padding: '0.5rem 1rem',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          fontSize: '1rem',
        }),
        primary && css({
          background: '#0070f3',
          color: 'white',
        }),
        !primary && css({
          background: '#f0f0f0',
          color: '#333',
        }),
      ]}
    >
      {children}
    </button>
  )
}
The mix prop combines multiple style objects. Falsy values are ignored, making conditional styles easy.
7

Add interactive features with Frames

Use Frames to embed dynamic, auto-updating content:
app/pages/dashboard.tsx
import { Frame } from 'remix/component'
import { Document } from '../layout.tsx'

export function DashboardPage() {
  return (
    <Document title="Dashboard">
      <h1>Dashboard</h1>

      <div>
        <h2>Live Stats</h2>
        {/* This frame auto-refreshes every 5 seconds */}
        <Frame src="/api/stats" />
      </div>

      <div>
        <h2>Recent Activity</h2>
        <Frame src="/api/activity" />
      </div>
    </Document>
  )
}
Create the frame endpoints:
app/router.ts
import { css } from 'remix/component'

router.get('/api/stats', async () => {
  let stats = {
    visitors: Math.floor(Math.random() * 1000),
    sales: Math.floor(Math.random() * 100),
  }

  return render(
    <div mix={[css({ padding: '1rem', background: '#f9f9f9', borderRadius: '8px' })]}>
      <p>Visitors: {stats.visitors}</p>
      <p>Sales: {stats.sales}</p>
    </div>
  )
})
8

Optimize with streaming

The renderToStream function automatically streams HTML to the browser as it’s generated. For even faster initial page loads, stream expensive data:
router.get(appRoutes.home, async () => {
  // Start rendering immediately
  let postsPromise = fetchPosts() // Don't await

  return render(
    <Document>
      <h1>Welcome!</h1>
      {/* This will be streamed when ready */}
      <Suspense fallback={<p>Loading posts...</p>}>
        {postsPromise.then(posts => (
          <PostList posts={posts} />
        ))}
      </Suspense>
    </Document>
  )
})

Component Best Practices

Extract reusable components

app/components/card.tsx
import type { RemixNode } from 'remix/component'
import { css } from 'remix/component'

interface CardProps {
  title: string
  children: RemixNode
}

export function Card({ title, children }: CardProps) {
  return (
    <div
      mix={[
        css({
          padding: '1.5rem',
          border: '1px solid #ddd',
          borderRadius: '8px',
          boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
        }),
      ]}
    >
      <h3 mix={[css({ marginBottom: '1rem' })]}>        
        {title}
      </h3>
      {children}
    </div>
  )
}

Type your props

Always use TypeScript interfaces for component props to get autocomplete and type checking.

Use semantic HTML

Use proper HTML elements (<article>, <section>, <nav>) for better accessibility and SEO.

Build docs developers (and LLMs) love