npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nextjs-slides

v0.8.3

Published

Composable slide deck primitives for Next.js — powered by React 19 ViewTransitions, Tailwind CSS, and highlight.js syntax highlighting.

Downloads

2,320

Readme

nextjs-slides

npm version

Composable slide deck primitives for Next.js — powered by React 19 ViewTransitions, Tailwind CSS v4, and highlight.js syntax highlighting.

Build full presentations from React components with URL-based routing, keyboard navigation, progress indicators, and smooth slide transitions — all declarative.

Note: I'm not experienced with publishing libraries — I built this for my own presentations. That said, feel free to try it out!

Install

npm install nextjs-slides

Demo

Live demo →

A minimal demo app lives in examples/demo. From the repo root:

npm run build && cd examples/demo && npm install && npm run dev

Open http://localhost:3000 — choose "Geist deck" or "Alternate deck" (Playfair + Dracula theme).

Peer dependencies: next >=15, react >=19, tailwindcss >=4.

Quick Start

1. Import the stylesheet

In your root layout or global CSS:

@import 'tailwindcss';
@import 'nextjs-slides/styles.css';

The stylesheet includes an @source directive that tells Tailwind v4 to scan the library's component files for utility classes — no extra configuration needed.

2. Define your slides

// app/slides/slides.tsx
import {
  Slide,
  SlideTitle,
  SlideSubtitle,
  SlideBadge,
  SlideCode,
} from 'nextjs-slides';

export const slides = [
  <Slide key="intro">
    <SlideBadge>Welcome</SlideBadge>
    <SlideTitle>My Presentation</SlideTitle>
    <SlideSubtitle>Built with nextjs-slides</SlideSubtitle>
  </Slide>,

  <Slide key="code" align="left">
    <SlideTitle>Code Example</SlideTitle>
    <SlideCode title="hello.ts">{`const greeting = "Hello, world!";
console.log(greeting);`}</SlideCode>
  </Slide>,
];

3. Create the layout (provider)

// app/slides/layout.tsx
import { SlideDeck } from 'nextjs-slides';
import { slides } from './slides';

export default function SlidesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <SlideDeck slides={slides}>{children}</SlideDeck>;
}

SlideDeck is a client component, so your layout can stay a server component — no "use client" needed.

4. Add the routes

// app/slides/page.tsx
import { redirect } from 'next/navigation';

export default function SlidesPage() {
  redirect('/slides/1');
}
// app/slides/[page]/page.tsx
import { getSlide, generateSlideParams } from 'nextjs-slides';
import { slides } from '../slides';

export const generateStaticParams = () => generateSlideParams(slides);

export default async function SlidePage({
  params,
}: {
  params: Promise<{ page: string }>;
}) {
  return getSlide(await params, slides);
}

That's it. Navigate to /slides and you have a full slide deck.

<SlideDeck> Props

| Prop | Type | Default | Description | | -------------- | --------------------------------- | ------------ | ------------------------------------------------------ | | slides | ReactNode[] | required | Your slides array | | speakerNotes | (string \| ReactNode \| null)[] | — | Notes per slide (same index). See Speaker Notes below. | | syncEndpoint | string | — | API route for presenter ↔ phone sync. | | basePath | string | "/slides" | URL prefix for slide routes | | exitUrl | string | — | URL for exit button (×). Shows in top-right when set. | | showProgress | boolean | true | Show dot progress indicator | | showCounter | boolean | true | Show "3 / 10" counter | | className | string | — | Additional class for the deck container | | children | React.ReactNode | required | Route content (from Next.js) |

Primitives

Layout

  • <Slide> — Full-screen slide container with decorative border. Props: align ("center" | "left"), className.
  • <SlideColumns> — Inline two-column grid for use inside <Slide> when you need a spanning title above two columns. Props: left, right, className.
  • <SlideSplitLayout> — Full-viewport two-column layout with vertical divider — a top-level alternative to <Slide> (do not nest inside <Slide>). Props: left, right, className.

Typography

  • <SlideTitle> — Large bold heading (responsive h1).
  • <SlideSubtitle> — Muted secondary text.
  • <SlideBadge> — Inverted pill badge.
  • <SlideHeaderBadge> — Italic accent label.
  • <SlideNote> — Small muted footnote.

Content

  • <SlideCode> — Syntax-highlighted code block (highlight.js). Props: title, className. Pass code as children string. Language is inferred from the file extension in title (e.g. example.tsx). Supported languages: JavaScript (.js, .jsx), TypeScript (.ts, .tsx), HTML/XML (.html, .xml). Unrecognized extensions fall back to TypeScript highlighting.
  • <SlideList> / <SlideListItem> — Bullet list.
  • <SlideDemo> — Interactive component container. Keyboard navigation is disabled inside so you can use inputs and buttons. Props: label, className.

Structured

  • <SlideStatementList> / <SlideStatement> — Title + description pairs with border separators.
  • <SlideSpeaker> — Avatar + name + title row. Props: name, title, avatar (optional image URL).
  • <SlideSpeakerGrid> — 2-column speaker grid.
  • <SlideSpeakerList> — Vertical speaker stack.

Navigation

  • <SlideLink> — Styled link for navigation. Props: href, variant ("primary" | "ghost"), className.

Keyboard Navigation

| Key | Action | | ------------ | -------------- | | or Space | Next slide | | | Previous slide |

Keyboard events are ignored inside <SlideDemo>, inputs, and textareas so you can interact without advancing slides.

Speaker Notes

Write notes in a markdown file — one section per slide, separated by --- on its own line. Sections are matched to slides by position: the first section = slide 1, the second = slide 2, and so on. Empty sections mean no notes for that slide.

Welcome everyone. This is the opening slide.

---

Talk about the base container here.

---

---

Slide 4 notes. Slide 3 had none.

Keep the number of sections in sync with your slides — if you have 12 slides, you need 12 sections (empty is fine). Don't start the file with ---; the first block of text (before any ---) is for slide 1.

Leading document title: If the file starts with # My Title (a single heading line), use stripLeadingTitle: true so that block isn't treated as slide 1:

parseSpeakerNotes(markdown, { stripLeadingTitle: true });

Parse the file and pass it to SlideDeck. Include syncEndpoint so the phone can follow along:

// app/slides/layout.tsx
import fs from 'fs';
import path from 'path';
import { SlideDeck, parseSpeakerNotes } from 'nextjs-slides';
import { slides } from './slides';

const notes = parseSpeakerNotes(
  fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8')
);

export default function SlidesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <SlideDeck
      slides={slides}
      speakerNotes={notes}
      syncEndpoint="/api/nxs-sync"
    >
      {children}
    </SlideDeck>
  );
}

Without syncEndpoint, the deck won't broadcast slide changes and the phone will stay on the first note.

Phone sync (presenter notes on your phone)

Open /notes on your phone while presenting on your laptop. The phone shows the current slide's notes and follows along as you navigate with the keyboard.

How sync works: When you navigate with arrow keys or spacebar, the deck POSTs { slide, total } to the sync endpoint. The notes page polls that endpoint every 500ms and displays notes[slide - 1] — so the notes array index must match slide order. Use the same notes.md file in both the layout and the notes page.

1. Create the sync API route:

// app/api/nxs-sync/route.ts
export { GET, POST } from 'nextjs-slides/sync';

2. Create the notes page (same notes.md, same parseSpeakerNotes — indices must match slides):

// app/notes/page.tsx
import fs from 'fs';
import path from 'path';
import { parseSpeakerNotes, SlideNotesView } from 'nextjs-slides';

const notes = parseSpeakerNotes(
  fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8')
);

export default function NotesPage() {
  return <SlideNotesView notes={notes} syncEndpoint="/api/nxs-sync" />;
}

Open your phone on http://<your-ip>:3000/notes (same network).

Demo notes (extra sections after the slides)

Add more --- sections after the last slide's notes — these become demo notes you can step through on your phone after the presentation ends:

...last slide notes

---

Open the counter demo. Show how useState drives the count.

---

Switch to the editor. Walk through adding a new slide.

The notes view auto-follows the deck during slides. Once you tap "Next" past the last slide, you enter demo notes territory (the header switches to "Demo 1 / 2") and the phone stops auto-syncing so you control it manually.

Note: The sync state lives in server memory — designed for next dev or single-server deployments. It won't persist across serverless function invocations.

Custom Base Path & Multiple Decks

Use basePath for a different URL, exitUrl for an exit button (×), and className for scoped font/syntax overrides:

<SlideDeck
  slides={slides}
  basePath="/slides-alt"
  exitUrl="/"
  className="slides-alt-deck"
>
  {children}
</SlideDeck>

Route at /slides-alt/[page]/page.tsx. Keep SlideDeck as the direct layout child (no wrapper div) so the exit animation works.

Breakout Pages

Pages inside the slides folder but outside the [page] route render without deck navigation — useful for live demos:

app/slides/
  layout.tsx          ← SlideDeck provider
  [page]/page.tsx     ← Slide routes
  demo/page.tsx       ← Breakout page (no deck chrome)

Styling & CSS

The library inherits your app's theme. Primitives use Tailwind utilities that resolve to CSS variables: --foreground, --background, --muted-foreground, --primary, --primary-foreground, --border, --muted. Compatible with shadcn/ui and any Tailwind v4 setup that defines these.

nextjs-slides/styles.css adds the Vercel-inspired code theme (--nxs-code-*, --sh-*) and slide transition animations. No scoping — slides inherit your global styles.

Customize code block: Override --nxs-code-bg, --nxs-code-border, --nxs-code-text for block styling. Override --sh-* for syntax highlighting: --sh-keyword, --sh-string, --sh-property, --sh-entity, --sh-class, --sh-tag (JSX/HTML tags), --sh-identifier, --sh-literal, --sh-comment, --sh-sign.

Geist fonts (optional)

Install geist, wire the fonts in your layout, and add the theme variables:

// app/layout.tsx
import { GeistSans } from 'geist/font/sans';
import { GeistMono } from 'geist/font/mono';
import { GeistPixelSquare } from 'geist/font/pixel';

export default function RootLayout({ children }) {
  return (
    <html
      lang="en"
      className={`${GeistSans.variable} ${GeistMono.variable} ${GeistPixelSquare.variable}`}
    >
      <body className={GeistSans.className}>{children}</body>
    </html>
  );
}
/* globals.css @theme inline */
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
--font-pixel:
  var(--font-geist-pixel-square), var(--font-geist-sans), ui-sans-serif,
  system-ui, sans-serif;

Use className="font-pixel" on primitives where you want the pixel display font.

Animations

Slide transitions use the React 19 <ViewTransition> component with addTransitionType(). The CSS in nextjs-slides/styles.css defines the ::view-transition-* animations. Override them in your own CSS to customize.

Troubleshooting

SlideCode syntax highlighting looks broken or colorless — Ensure you import nextjs-slides/styles.css in your root layout or global CSS (see Quick Start). The --sh-* variables must be in scope for highlight.js tokens to display correctly.

Split layout not stacking on small screens — Import nextjs-slides/styles.css without a layer: @import "nextjs-slides/styles.css" (not layer(base)). Layered imports can be overridden by Tailwind utilities. Also ensure the library CSS loads after Tailwind.

Slide utility classes not applying — The library's stylesheet includes @source "./*.js" so Tailwind v4 automatically scans the library's component files. If styles still don't apply, make sure nextjs-slides/styles.css is imported after tailwindcss in your CSS. As a fallback, you can manually add @source "../node_modules/nextjs-slides/dist" (path relative to your CSS file) in your global CSS.

SlideCode error "Could not find the language '…'" — Only JavaScript, TypeScript, and HTML/XML are registered. Unrecognized file extensions in the title prop (e.g. .terminal, .sh, .py) will fall back to TypeScript highlighting. If you previously saw this error, update the package — the fix gracefully handles unknown languages instead of throwing.

SlideSplitLayout nested inside Slide breaks layoutSlideSplitLayout is a full-viewport component (h-dvh w-dvw) that replaces Slide, not a child of it. Nesting it inside Slide creates a viewport-sized container inside another viewport-sized container with padding, which overflows. If you need a title above two columns, use <SlideColumns> inside <Slide> instead.

Exit animation (deck-unveil) not running — Ensure SlideDeck is the direct child of the layout. Wrapping it in a <div> can prevent the ViewTransition exit from firing. Use the className prop for scoped styling instead.

Notes out of sync — Ensure syncEndpoint is set and both layout and notes page use the same notes.md. On serverless (Vercel), in-memory sync can hit different instances; use a shared store for production.

Notes show a document title instead of slide 1 — If the file starts with # My Title before the first ---, use parseSpeakerNotes(markdown, { stripLeadingTitle: true }).

For maintainers

See DEPLOYMENT.md for Vercel deployment and release workflow.

License

MIT