Styled navigation landmark with current-page marking, built on the @foldkit/ui Nav helper.

Preview
Style
src/demo/views/nav.ts47 lines
import { Schema as S } from 'effect'
import { evo } from 'foldkit/struct'
import { defineMessageUnion } from 'foldkit/message'
import type { Html, HtmlBuilder } from 'foldkit/html'

import { nav } from '../../generated/registry/ui/nav'

import { DemoNav } from '../bundles'
import { defineSlice, type UpdateReturn } from '../slice'
import type { Model, Message as AppMessage } from '../assemble'

const Message = defineMessageUnion({
  SelectedNav: { value: DemoNav },
})

const NAV_ITEMS = ['Overview', 'Components', 'Settings', 'Docs'] as const

export const navView = (model: Model, h: HtmlBuilder<AppMessage>): Html =>
  nav<AppMessage, (typeof NAV_ITEMS)[number]>(
    {
      items: NAV_ITEMS,
      ariaLabel: 'Primary',
      toHref: () => '#',
      isItemCurrent: (value) => value === model.activeNav,
      onItemClick: (value) => Message.SelectedNav({ value }),
      toLabel: (value) => value,
    },
    h,
  )

const fields = { activeNav: DemoNav }

const stateSchema = S.Struct(fields)
type State = typeof stateSchema.Type

export const slice = defineSlice({
  fields,
  init: { activeNav: 'Components' },
  messages: [Message.SelectedNav],
  handlers: (model: State) => ({
    SelectedNav: ({ value }: typeof Message.SelectedNav.Type): UpdateReturn => ({
      model: evo(model, { activeNav: () => value }),
    }),
  }),
  samples: [Message.SelectedNav({ value: 'Docs' })],
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/nav

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/nav.ts68 lines
import { Nav as FoldkitNav } from '@foldkit/ui'
import type { Html, HtmlBuilder } from 'foldkit/html'

import { cn } from '@/lib/utils'

export const navClass = 'flex items-center rounded-lg border border-border bg-card p-1 shadow-sm'

export const navListClass = 'flex flex-1 list-none items-center justify-center gap-1'

export const navItemClass = 'relative'

export const navLinkClass =
  'relative inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-px hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[current]:bg-primary data-[current]:text-primary-foreground data-[current]:shadow-sm active:translate-y-0 active:scale-[.98] motion-reduce:transform-none motion-reduce:transition-none'

export type NavConfig<M, Value extends string = string> = Readonly<{
  items: ReadonlyArray<Value>
  toHref: (value: Value, index: number) => string
  isItemCurrent: (value: Value, index: number) => boolean
  toLabel: (value: Value, index: number) => Html | string
  ariaLabel: string
  onItemClick?: (value: Value, index: number) => M
  className?: string
  linkClass?: string
}>

/** Styled navigation landmark built on the @foldkit/ui Nav helper. The
 *  current destination is marked with `aria-current="page"` and styled via
 *  the `data-current` attribute. Items render inside a list (`ul`/`li`),
 *  mirroring the structure of the shadcn navigation-menu and breadcrumb
 *  primitives. */
export const nav = <M, Value extends string = string>(
  config: NavConfig<M, Value>,
  h: HtmlBuilder<M>,
): Html =>
  FoldkitNav.view<Value>({
    items: config.items,
    ariaLabel: config.ariaLabel,
    toHref: config.toHref,
    isItemCurrent: config.isItemCurrent,
    toView: ({ nav: navAttributes, items }) =>
      h.nav(
        [...navAttributes, h.DataAttribute('slot', 'nav'), h.Class(cn(navClass, config.className))],
        [
          h.ul(
            [h.DataAttribute('slot', 'nav-list'), h.Class(cn(navListClass))],
            items.map((item, index) =>
              h.li(
                [h.DataAttribute('slot', 'nav-item'), h.Class(cn(navItemClass))],
                [
                  h.a(
                    [
                      ...item.link,
                      ...(config.onItemClick === undefined
                        ? []
                        : [h.OnClick(config.onItemClick(item.value, index))]),
                      h.DataAttribute('slot', 'nav-link'),
                      h.Class(cn(navLinkClass, config.linkClass)),
                    ],
                    [config.toLabel(item.value, index)],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
  })