Skip to content

Channel Info

The channel detail panel that covers a channel on narrow screens.

The channel detail panel that covers a channel on narrow screens. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

import {
  ChannelInfo,
  ChannelInfoAction,
  ChannelInfoActions,
  ChannelInfoEmpty,
  ChannelInfoEmptyBody,
  ChannelInfoEmptyIcon,
  ChannelInfoEmptyTitle,
  ChannelInfoIcon,
  ChannelInfoIdentity,
  ChannelInfoName,
  ChannelInfoPanel,
  ChannelInfoToolbar,
  ChannelInfoType,
} from "@/registry/bases/newt/ui/channel-info"
import { Tabs, TabsContent, TabsTrigger } from "@/registry/bases/newt/ui/tabs"

export default function ChannelInfoDemo() {
  return (
    <ChannelInfo
      aria-label="Channel information for #general"
      className="h-[420px] rounded-md"
    >
      <ChannelInfoToolbar>
        <ChannelInfoAction aria-label="Back to channel">
          <svg viewBox="0 0 20 20" className="h-5 w-5" fill="currentColor">
            <path d="M8.7 3.3 2 10l6.7 6.7 1.4-1.4L5.8 11H18V9H5.8l4.3-4.3-1.4-1.4Z" />
          </svg>
        </ChannelInfoAction>
        <ChannelInfoActions>
          <ChannelInfoAction aria-label="Search">
            <svg
              viewBox="0 0 20 20"
              className="h-5 w-5"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
            >
              <circle cx="9" cy="9" r="5.5" />
              <path d="M13 13l4 4" />
            </svg>
          </ChannelInfoAction>
          <ChannelInfoAction aria-label="Notification settings">
            <svg viewBox="0 0 20 20" className="h-5 w-5" fill="currentColor">
              <path d="M10 2a5 5 0 0 0-5 5v3.5L3.5 13v1h13v-1L15 10.5V7a5 5 0 0 0-5-5Zm0 16a2.5 2.5 0 0 0 2.45-2h-4.9A2.5 2.5 0 0 0 10 18Z" />
            </svg>
          </ChannelInfoAction>
        </ChannelInfoActions>
      </ChannelInfoToolbar>

      <ChannelInfoIdentity>
        <ChannelInfoIcon />
        <div className="min-w-0">
          <ChannelInfoName>general</ChannelInfoName>
          <ChannelInfoType>Text Channel</ChannelInfoType>
        </div>
      </ChannelInfoIdentity>

      {/* `contents` lets the tab strip and the panel sit directly in the
          panel column, so the panel is not nested inside the tablist. */}
      <Tabs defaultValue="threads" role="presentation" className="contents">
        <div
          role="tablist"
          aria-label="Channel information sections"
          className="mx-2 flex gap-1 rounded-md bg-newt-bg-base p-1"
        >
          <TabsTrigger value="members">Members</TabsTrigger>
          <TabsTrigger value="pins">Pins</TabsTrigger>
          <TabsTrigger value="threads">Threads</TabsTrigger>
        </div>

        <ChannelInfoPanel>
          <TabsContent value="members">
            <ChannelInfoEmpty>
              <ChannelInfoEmptyTitle>No members to show.</ChannelInfoEmptyTitle>
            </ChannelInfoEmpty>
          </TabsContent>
          <TabsContent value="pins">
            <ChannelInfoEmpty>
              <ChannelInfoEmptyTitle>
                No pinned messages yet.
              </ChannelInfoEmptyTitle>
            </ChannelInfoEmpty>
          </TabsContent>
          <TabsContent value="threads">
            <ChannelInfoEmpty>
              <ChannelInfoEmptyIcon>
                <svg
                  viewBox="0 0 24 24"
                  className="h-8 w-8"
                  fill="currentColor"
                >
                  <path d="M4 4h16v10H8l-4 4V4Z" />
                </svg>
              </ChannelInfoEmptyIcon>
              <ChannelInfoEmptyTitle>
                There are no threads.
              </ChannelInfoEmptyTitle>
              <ChannelInfoEmptyBody>
                Stay focused on a conversation with a thread &mdash; a temporary
                text channel.
              </ChannelInfoEmptyBody>
            </ChannelInfoEmpty>
          </TabsContent>
        </ChannelInfoPanel>
      </Tabs>
    </ChannelInfo>
  )
}
<script setup lang="ts">
import {
  ChannelInfo,
  ChannelInfoAction,
  ChannelInfoActions,
  ChannelInfoEmpty,
  ChannelInfoEmptyBody,
  ChannelInfoEmptyIcon,
  ChannelInfoEmptyTitle,
  ChannelInfoIcon,
  ChannelInfoIdentity,
  ChannelInfoName,
  ChannelInfoPanel,
  ChannelInfoToolbar,
  ChannelInfoType,
} from "~~/registry/bases/newt/ui/channel-info"
import { Tabs, TabsContent, TabsTrigger } from "~~/registry/bases/newt/ui/tabs"
</script>

<template>
  <ChannelInfo
    aria-label="Channel information for #general"
    class="h-[420px] rounded-md"
  >
    <ChannelInfoToolbar>
      <ChannelInfoAction aria-label="Back to channel">
        <svg viewBox="0 0 20 20" class="h-5 w-5" fill="currentColor">
          <path
            d="M8.7 3.3 2 10l6.7 6.7 1.4-1.4L5.8 11H18V9H5.8l4.3-4.3-1.4-1.4Z"
          />
        </svg>
      </ChannelInfoAction>
      <ChannelInfoActions>
        <ChannelInfoAction aria-label="Search">
          <svg
            viewBox="0 0 20 20"
            class="h-5 w-5"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
          >
            <circle cx="9" cy="9" r="5.5" />
            <path d="M13 13l4 4" />
          </svg>
        </ChannelInfoAction>
        <ChannelInfoAction aria-label="Notification settings">
          <svg viewBox="0 0 20 20" class="h-5 w-5" fill="currentColor">
            <path
              d="M10 2a5 5 0 0 0-5 5v3.5L3.5 13v1h13v-1L15 10.5V7a5 5 0 0 0-5-5Zm0 16a2.5 2.5 0 0 0 2.45-2h-4.9A2.5 2.5 0 0 0 10 18Z"
            />
          </svg>
        </ChannelInfoAction>
      </ChannelInfoActions>
    </ChannelInfoToolbar>

    <ChannelInfoIdentity>
      <ChannelInfoIcon />
      <div class="min-w-0">
        <ChannelInfoName>general</ChannelInfoName>
        <ChannelInfoType>Text Channel</ChannelInfoType>
      </div>
    </ChannelInfoIdentity>

    <!--
      `contents` lets the tab strip and the panel sit directly in the panel
      column, so the panel is not nested inside the tablist.
    -->
    <Tabs default-value="threads" role="presentation" class="contents">
      <div
        role="tablist"
        aria-label="Channel information sections"
        class="mx-2 flex gap-1 rounded-md bg-newt-bg-base p-1"
      >
        <TabsTrigger value="members">Members</TabsTrigger>
        <TabsTrigger value="pins">Pins</TabsTrigger>
        <TabsTrigger value="threads">Threads</TabsTrigger>
      </div>

      <ChannelInfoPanel>
        <TabsContent value="members">
          <ChannelInfoEmpty>
            <ChannelInfoEmptyTitle>No members to show.</ChannelInfoEmptyTitle>
          </ChannelInfoEmpty>
        </TabsContent>
        <TabsContent value="pins">
          <ChannelInfoEmpty>
            <ChannelInfoEmptyTitle>
              No pinned messages yet.
            </ChannelInfoEmptyTitle>
          </ChannelInfoEmpty>
        </TabsContent>
        <TabsContent value="threads">
          <ChannelInfoEmpty>
            <ChannelInfoEmptyIcon>
              <svg viewBox="0 0 24 24" class="h-8 w-8" fill="currentColor">
                <path d="M4 4h16v10H8l-4 4V4Z" />
              </svg>
            </ChannelInfoEmptyIcon>
            <ChannelInfoEmptyTitle>
              There are no threads.
            </ChannelInfoEmptyTitle>
            <ChannelInfoEmptyBody>
              Stay focused on a conversation with a thread &mdash; a temporary
              text channel.
            </ChannelInfoEmptyBody>
          </ChannelInfoEmpty>
        </TabsContent>
      </ChannelInfoPanel>
    </Tabs>
  </ChannelInfo>
</template>

Installation

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

Add the components it builds on

Copy the source into your project

import * as React from "react"

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

/*
 * The panel that slides in over a channel on narrow screens, holding the
 * member list, pins, threads and the rest of the channel detail tabs.
 */
const ChannelInfo = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, role = "dialog", ...props }, ref) => (
  <div
    ref={ref}
    role={role}
    className={cn(
      "flex h-full w-full max-w-sm flex-col overflow-hidden bg-newt-bg-surface text-newt-text-primary",
      className
    )}
    {...props}
  />
))
ChannelInfo.displayName = "ChannelInfo"

const ChannelInfoToolbar = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex h-14 shrink-0 items-center justify-between gap-2 px-2",
      className
    )}
    {...props}
  />
))
ChannelInfoToolbar.displayName = "ChannelInfoToolbar"

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

const ChannelInfoAction = React.forwardRef<
  HTMLButtonElement,
  React.ComponentProps<"button">
>(({ className, type = "button", ...props }, ref) => (
  <button
    ref={ref}
    type={type}
    className={cn(
      "inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-newt-text-secondary transition-colors duration-fast ease-newt",
      "hover:bg-newt-bg-hover hover:text-newt-text-primary",
      "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
      className
    )}
    {...props}
  />
))
ChannelInfoAction.displayName = "ChannelInfoAction"

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

const ChannelInfoIcon = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, children = "#", ...props }, ref) => (
  <div
    ref={ref}
    aria-hidden="true"
    className={cn(
      "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-newt-bg-elevated text-lg font-bold leading-none text-newt-text-muted",
      className
    )}
    {...props}
  >
    {children}
  </div>
))
ChannelInfoIcon.displayName = "ChannelInfoIcon"

const ChannelInfoName = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("truncate font-bold text-newt-text-primary", className)}
    {...props}
  />
))
ChannelInfoName.displayName = "ChannelInfoName"

const ChannelInfoType = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-[13px] text-newt-text-muted", className)}
    {...props}
  />
))
ChannelInfoType.displayName = "ChannelInfoType"

/* Scrolling area under the tab strip. */
const ChannelInfoPanel = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("min-h-0 flex-1 overflow-y-auto px-2 pb-4", className)}
    {...props}
  />
))
ChannelInfoPanel.displayName = "ChannelInfoPanel"

const ChannelInfoEmpty = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex flex-col items-center gap-2 px-6 py-10 text-center",
      className
    )}
    {...props}
  />
))
ChannelInfoEmpty.displayName = "ChannelInfoEmpty"

const ChannelInfoEmptyIcon = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    aria-hidden="true"
    className={cn(
      "flex h-14 w-14 items-center justify-center rounded-full bg-newt-bg-elevated text-newt-text-muted",
      className
    )}
    {...props}
  />
))
ChannelInfoEmptyIcon.displayName = "ChannelInfoEmptyIcon"

const ChannelInfoEmptyTitle = React.forwardRef<
  HTMLParagraphElement,
  React.ComponentProps<"p">
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("font-semibold text-newt-text-primary", className)}
    {...props}
  />
))
ChannelInfoEmptyTitle.displayName = "ChannelInfoEmptyTitle"

const ChannelInfoEmptyBody = React.forwardRef<
  HTMLParagraphElement,
  React.ComponentProps<"p">
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("max-w-[36ch] text-sm text-newt-text-muted", className)}
    {...props}
  />
))
ChannelInfoEmptyBody.displayName = "ChannelInfoEmptyBody"

export {
  ChannelInfo,
  ChannelInfoToolbar,
  ChannelInfoActions,
  ChannelInfoAction,
  ChannelInfoIdentity,
  ChannelInfoIcon,
  ChannelInfoName,
  ChannelInfoType,
  ChannelInfoPanel,
  ChannelInfoEmpty,
  ChannelInfoEmptyIcon,
  ChannelInfoEmptyTitle,
  ChannelInfoEmptyBody,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <!--
    The panel that slides in over a channel on narrow screens, holding the
    member list, pins, threads and the rest of the channel detail tabs.
  -->
  <div
    role="dialog"
    :class="
      cn(
        'flex h-full w-full max-w-sm flex-col overflow-hidden bg-newt-bg-surface text-newt-text-primary',
        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>
  <div
    :class="
      cn(
        'flex h-14 shrink-0 items-center justify-between gap-2 px-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>
  <div :class="cn('flex items-center gap-1', 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>
  <button
    type="button"
    :class="
      cn(
        'inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-newt-text-secondary transition-colors duration-fast ease-newt',
        'hover:bg-newt-bg-hover hover: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 = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('flex min-w-0 items-center gap-3 px-4 pb-3', 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>
  <div
    aria-hidden="true"
    :class="
      cn(
        'flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-newt-bg-elevated text-lg font-bold leading-none text-newt-text-muted',
        props.class
      )
    "
  >
    <slot>#</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>
  <div :class="cn('truncate font-bold text-newt-text-primary', 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>
  <div :class="cn('text-[13px] text-newt-text-muted', 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>
  <!-- Scrolling area under the tab strip. -->
  <div :class="cn('min-h-0 flex-1 overflow-y-auto px-2 pb-4', 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>
  <div
    :class="
      cn('flex flex-col items-center gap-2 px-6 py-10 text-center', 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>
  <div
    aria-hidden="true"
    :class="
      cn(
        'flex h-14 w-14 items-center justify-center rounded-full bg-newt-bg-elevated text-newt-text-muted',
        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>
  <p :class="cn('font-semibold text-newt-text-primary', props.class)">
    <slot />
  </p>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <p :class="cn('max-w-[36ch] text-sm text-newt-text-muted', props.class)">
    <slot />
  </p>
</template>
export { default as ChannelInfo } from "./ChannelInfo.vue"
export { default as ChannelInfoToolbar } from "./ChannelInfoToolbar.vue"
export { default as ChannelInfoActions } from "./ChannelInfoActions.vue"
export { default as ChannelInfoAction } from "./ChannelInfoAction.vue"
export { default as ChannelInfoIdentity } from "./ChannelInfoIdentity.vue"
export { default as ChannelInfoIcon } from "./ChannelInfoIcon.vue"
export { default as ChannelInfoName } from "./ChannelInfoName.vue"
export { default as ChannelInfoType } from "./ChannelInfoType.vue"
export { default as ChannelInfoPanel } from "./ChannelInfoPanel.vue"
export { default as ChannelInfoEmpty } from "./ChannelInfoEmpty.vue"
export { default as ChannelInfoEmptyIcon } from "./ChannelInfoEmptyIcon.vue"
export { default as ChannelInfoEmptyTitle } from "./ChannelInfoEmptyTitle.vue"
export { default as ChannelInfoEmptyBody } from "./ChannelInfoEmptyBody.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 { ChannelInfo } from "@/components/ui/channel-info"

export function Example() {
  return <ChannelInfo />
}
<script setup lang="ts">
import { ChannelInfo, ChannelInfoToolbar, ChannelInfoActions, ChannelInfoAction, ChannelInfoIdentity, ChannelInfoIcon, ChannelInfoName, ChannelInfoType, ChannelInfoPanel, ChannelInfoEmpty, ChannelInfoEmptyIcon, ChannelInfoEmptyTitle, ChannelInfoEmptyBody } from "@/components/ui/channel-info"
</script>

<template>
  <ChannelInfo />
</template>

API reference

ChannelInfo

PropType
children / default slot?React.ReactNode / Vue slot

Content rendered inside the component.

TypeReact.ReactNode / Vue slot
className / class?string

Additional classes merged with the component defaults.

Typestring

The registry exports ChannelInfo, ChannelInfoToolbar, ChannelInfoActions, ChannelInfoAction, ChannelInfoIdentity, ChannelInfoIcon, ChannelInfoName, ChannelInfoType, ChannelInfoPanel, ChannelInfoEmpty, ChannelInfoEmptyIcon, ChannelInfoEmptyTitle, ChannelInfoEmptyBody. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.

Tokens

Channel Info 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.