Skip to content

Channel Header

The bar above the message list, with the channel name, topic and channel actions.

The bar above the message list, with the channel name, topic and channel actions. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

import {
  ChannelHeader,
  ChannelHeaderAction,
  ChannelHeaderDivider,
  ChannelHeaderIcon,
  ChannelHeaderInfo,
  ChannelHeaderName,
  ChannelHeaderSearch,
  ChannelHeaderToolbar,
  ChannelHeaderTopic,
} from "@/registry/bases/newt/ui/channel-header"

export default function ChannelHeaderDemo() {
  return (
    <ChannelHeader className="w-full">
      <ChannelHeaderInfo>
        <ChannelHeaderIcon />
        <ChannelHeaderName>general</ChannelHeaderName>
        <ChannelHeaderDivider />
        <ChannelHeaderTopic>
          Say hello and tell us what you are building.
        </ChannelHeaderTopic>
      </ChannelHeaderInfo>

      <ChannelHeaderToolbar>
        <ChannelHeaderAction aria-label="Pinned messages">
          <svg viewBox="0 0 24 24" className="h-6 w-6" fill="currentColor">
            <path d="M14 3v6l3 3v2h-4v6l-1 1-1-1v-6H7v-2l3-3V3h4Z" />
          </svg>
        </ChannelHeaderAction>
        <ChannelHeaderAction active aria-label="Hide member list" aria-pressed>
          <svg viewBox="0 0 24 24" className="h-6 w-6" fill="currentColor">
            <path d="M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm7 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM2 19a7 7 0 0 1 14 0v1H2v-1Zm15.5-6a5.5 5.5 0 0 1 4.5 5.4V20h-4v-1a8.9 8.9 0 0 0-1.6-5.1c.35-.06.72-.09 1.1-.09Z" />
          </svg>
        </ChannelHeaderAction>
        <ChannelHeaderSearch className="hidden sm:flex" />
      </ChannelHeaderToolbar>
    </ChannelHeader>
  )
}
<script setup lang="ts">
import {
  ChannelHeader,
  ChannelHeaderAction,
  ChannelHeaderDivider,
  ChannelHeaderIcon,
  ChannelHeaderInfo,
  ChannelHeaderName,
  ChannelHeaderSearch,
  ChannelHeaderToolbar,
  ChannelHeaderTopic,
} from "~~/registry/bases/newt/ui/channel-header"
</script>

<template>
  <ChannelHeader class="w-full">
    <ChannelHeaderInfo>
      <ChannelHeaderIcon />
      <ChannelHeaderName>general</ChannelHeaderName>
      <ChannelHeaderDivider />
      <ChannelHeaderTopic>
        Say hello and tell us what you are building.
      </ChannelHeaderTopic>
    </ChannelHeaderInfo>

    <ChannelHeaderToolbar>
      <ChannelHeaderAction aria-label="Pinned messages">
        <svg viewBox="0 0 24 24" class="h-6 w-6" fill="currentColor">
          <path d="M14 3v6l3 3v2h-4v6l-1 1-1-1v-6H7v-2l3-3V3h4Z" />
        </svg>
      </ChannelHeaderAction>
      <ChannelHeaderAction
        active
        aria-label="Hide member list"
        aria-pressed="true"
      >
        <svg viewBox="0 0 24 24" class="h-6 w-6" fill="currentColor">
          <path
            d="M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm7 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM2 19a7 7 0 0 1 14 0v1H2v-1Zm15.5-6a5.5 5.5 0 0 1 4.5 5.4V20h-4v-1a8.9 8.9 0 0 0-1.6-5.1c.35-.06.72-.09 1.1-.09Z"
          />
        </svg>
      </ChannelHeaderAction>
      <ChannelHeaderSearch class="hidden sm:flex" />
    </ChannelHeaderToolbar>
  </ChannelHeader>
</template>

Installation

pnpm dlx newtui@latest add channel-header
npx newtui@latest add channel-header
yarn dlx newtui@latest add channel-header
bunx newtui@latest add channel-header

Copy the source into your project

import * as React from "react"

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

/* The bar above the message list: channel name, topic, and channel actions. */
const ChannelHeader = React.forwardRef<
  HTMLElement,
  React.ComponentProps<"header">
>(({ className, ...props }, ref) => (
  <header
    ref={ref}
    className={cn(
      "flex h-12 items-center justify-between gap-3 border-b border-newt-border bg-newt-bg-surface ps-4 pe-3 text-newt-text-primary",
      className
    )}
    {...props}
  />
))
ChannelHeader.displayName = "ChannelHeader"

const ChannelHeaderInfo = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex min-w-0 items-center gap-2", className)}
    {...props}
  />
))
ChannelHeaderInfo.displayName = "ChannelHeaderInfo"

const ChannelHeaderIcon = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, children = "#", ...props }, ref) => (
  <span
    ref={ref}
    aria-hidden="true"
    className={cn(
      "shrink-0 text-xl font-bold leading-none text-newt-text-muted",
      className
    )}
    {...props}
  >
    {children}
  </span>
))
ChannelHeaderIcon.displayName = "ChannelHeaderIcon"

const ChannelHeaderName = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn(
      "shrink-0 truncate text-base font-semibold text-newt-text-primary",
      className
    )}
    {...props}
  />
))
ChannelHeaderName.displayName = "ChannelHeaderName"

/* Round dot that sets the topic apart from the channel name. */
const ChannelHeaderDivider = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    aria-hidden="true"
    className={cn(
      "h-1 w-1 shrink-0 rounded-full bg-newt-text-muted opacity-70",
      className
    )}
    {...props}
  />
))
ChannelHeaderDivider.displayName = "ChannelHeaderDivider"

const ChannelHeaderTopic = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn(
      "min-w-0 truncate text-[13px] font-medium text-newt-text-muted",
      className
    )}
    {...props}
  />
))
ChannelHeaderTopic.displayName = "ChannelHeaderTopic"

const ChannelHeaderToolbar = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex shrink-0 items-center gap-4", className)}
    {...props}
  />
))
ChannelHeaderToolbar.displayName = "ChannelHeaderToolbar"

export interface ChannelHeaderActionProps extends React.ComponentProps<"button"> {
  /** Keeps a toggled action (such as the member list) lit up. */
  active?: boolean
}

const ChannelHeaderAction = React.forwardRef<
  HTMLButtonElement,
  ChannelHeaderActionProps
>(({ className, active = false, type = "button", ...props }, ref) => (
  <button
    ref={ref}
    type={type}
    data-state={active ? "active" : "inactive"}
    className={cn(
      "inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-newt-text-muted transition-colors duration-fast ease-newt",
      "hover:text-newt-text-primary data-[state=active]:text-newt-text-primary",
      "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
      className
    )}
    {...props}
  />
))
ChannelHeaderAction.displayName = "ChannelHeaderAction"

const SearchIcon = () => (
  <svg
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.75"
    strokeLinecap="round"
    aria-hidden="true"
    className="h-4 w-4 shrink-0"
  >
    <circle cx="7" cy="7" r="4.5" />
    <path d="M10.5 10.5 14 14" />
  </svg>
)

export interface ChannelHeaderSearchProps extends React.ComponentProps<"button"> {
  placeholder?: string
}

const ChannelHeaderSearch = React.forwardRef<
  HTMLButtonElement,
  ChannelHeaderSearchProps
>(
  (
    {
      className,
      placeholder = "Search",
      type = "button",
      children,
      "aria-label": ariaLabel = placeholder,
      ...props
    },
    ref
  ) => (
    <button
      ref={ref}
      type={type}
      aria-label={ariaLabel}
      className={cn(
        "flex h-7 w-60 shrink-0 cursor-pointer items-center justify-between gap-3 rounded-sm border border-newt-border bg-newt-bg-input px-2 text-newt-text-muted transition-colors duration-fast ease-newt",
        "hover:border-newt-text-muted",
        "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
        className
      )}
      {...props}
    >
      <span className="min-w-0 flex-1 truncate text-start text-sm font-medium leading-none">
        {children ?? placeholder}
      </span>
      <SearchIcon />
    </button>
  )
)
ChannelHeaderSearch.displayName = "ChannelHeaderSearch"

/* Compact "N Online" readout used by the narrow layout of the header. */
const ChannelHeaderOnline = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, children, ...props }, ref) => (
  <span
    ref={ref}
    className={cn(
      "inline-flex items-center gap-1.5 text-xs text-newt-text-muted",
      className
    )}
    {...props}
  >
    <span
      aria-hidden="true"
      className="h-2 w-2 shrink-0 rounded-full bg-newt-online"
    />
    {children}
  </span>
))
ChannelHeaderOnline.displayName = "ChannelHeaderOnline"

export {
  ChannelHeader,
  ChannelHeaderInfo,
  ChannelHeaderIcon,
  ChannelHeaderName,
  ChannelHeaderDivider,
  ChannelHeaderTopic,
  ChannelHeaderToolbar,
  ChannelHeaderAction,
  ChannelHeaderSearch,
  ChannelHeaderOnline,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <header
    :class="
      cn(
        'flex h-12 items-center justify-between gap-3 border-b border-newt-border bg-newt-bg-surface ps-4 pe-3 text-newt-text-primary',
        props.class
      )
    "
  >
    <slot />
  </header>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('flex min-w-0 items-center gap-2', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span
    aria-hidden="true"
    :class="
      cn(
        'shrink-0 text-xl font-bold leading-none text-newt-text-muted',
        props.class
      )
    "
  >
    <slot>#</slot>
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span
    :class="
      cn(
        'shrink-0 truncate text-base font-semibold text-newt-text-primary',
        props.class
      )
    "
  >
    <slot />
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <!-- Round dot that sets the topic apart from the channel name. -->
  <span
    aria-hidden="true"
    :class="
      cn(
        'h-1 w-1 shrink-0 rounded-full bg-newt-text-muted opacity-70',
        props.class
      )
    "
  />
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span
    :class="
      cn(
        'min-w-0 truncate text-[13px] font-medium text-newt-text-muted',
        props.class
      )
    "
  >
    <slot />
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('flex shrink-0 items-center gap-4', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"]
    /** Keeps a toggled action (such as the member list) lit up. */
    active?: boolean
  }>(),
  { active: false }
)
</script>

<template>
  <button
    type="button"
    :data-state="props.active ? 'active' : 'inactive'"
    :class="
      cn(
        'inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-newt-text-muted transition-colors duration-fast ease-newt',
        'hover:text-newt-text-primary data-[state=active]:text-newt-text-primary',
        'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link',
        props.class
      )
    "
  >
    <slot />
  </button>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = withDefaults(
  defineProps<{ class?: HTMLAttributes["class"]; placeholder?: string }>(),
  { placeholder: "Search" }
)
</script>

<template>
  <button
    type="button"
    :aria-label="props.placeholder"
    :class="
      cn(
        'flex h-7 w-60 shrink-0 cursor-pointer items-center justify-between gap-3 rounded-sm border border-newt-border bg-newt-bg-input px-2 text-newt-text-muted transition-colors duration-fast ease-newt',
        'hover:border-newt-text-muted',
        'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link',
        props.class
      )
    "
  >
    <span
      class="min-w-0 flex-1 truncate text-start text-sm font-medium leading-none"
    >
      <slot>{{ props.placeholder }}</slot>
    </span>
    <svg
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      stroke-width="1.75"
      stroke-linecap="round"
      aria-hidden="true"
      class="h-4 w-4 shrink-0"
    >
      <circle cx="7" cy="7" r="4.5" />
      <path d="M10.5 10.5 14 14" />
    </svg>
  </button>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <!-- Compact "N Online" readout used by the narrow layout of the header. -->
  <span
    :class="
      cn(
        'inline-flex items-center gap-1.5 text-xs text-newt-text-muted',
        props.class
      )
    "
  >
    <span
      aria-hidden="true"
      class="h-2 w-2 shrink-0 rounded-full bg-newt-online"
    />
    <slot />
  </span>
</template>
export { default as ChannelHeader } from "./ChannelHeader.vue"
export { default as ChannelHeaderInfo } from "./ChannelHeaderInfo.vue"
export { default as ChannelHeaderIcon } from "./ChannelHeaderIcon.vue"
export { default as ChannelHeaderName } from "./ChannelHeaderName.vue"
export { default as ChannelHeaderDivider } from "./ChannelHeaderDivider.vue"
export { default as ChannelHeaderTopic } from "./ChannelHeaderTopic.vue"
export { default as ChannelHeaderToolbar } from "./ChannelHeaderToolbar.vue"
export { default as ChannelHeaderAction } from "./ChannelHeaderAction.vue"
export { default as ChannelHeaderSearch } from "./ChannelHeaderSearch.vue"
export { default as ChannelHeaderOnline } from "./ChannelHeaderOnline.vue"

Update the import paths

The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.

Usage

import { ChannelHeader } from "@/components/ui/channel-header"

export function Example() {
  return <ChannelHeader />
}
<script setup lang="ts">
import { ChannelHeader, ChannelHeaderInfo, ChannelHeaderIcon, ChannelHeaderName, ChannelHeaderDivider, ChannelHeaderTopic, ChannelHeaderToolbar, ChannelHeaderAction, ChannelHeaderSearch, ChannelHeaderOnline } from "@/components/ui/channel-header"
</script>

<template>
  <ChannelHeader />
</template>

API reference

ChannelHeaderAction

PropType
active?boolean | undefined

Keeps a toggled action (such as the member list) lit up.

Typeboolean | undefined

ChannelHeaderSearch

PropType
placeholder?string | undefined

Optional placeholder value.

Typestring | undefined

The registry exports ChannelHeader, ChannelHeaderInfo, ChannelHeaderIcon, ChannelHeaderName, ChannelHeaderDivider, ChannelHeaderTopic, ChannelHeaderToolbar, ChannelHeaderAction, ChannelHeaderSearch, ChannelHeaderOnline. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Every icon-only toolbar action needs an aria-label. Preserve the visible focus ring and keep the channel name in the accessible heading hierarchy.

Tokens

Channel Header reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer.