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 — 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 — a temporary
text channel.
</ChannelInfoEmptyBody>
</ChannelInfoEmpty>
</TabsContent>
</ChannelInfoPanel>
</Tabs>
</ChannelInfo>
</template>Installation
pnpm dlx newtui@latest add channel-infonpx newtui@latest add channel-infoyarn dlx newtui@latest add channel-infobunx newtui@latest add channel-infoAdd 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
children / default slot?React.ReactNode / Vue slot
Content rendered inside the component.
React.ReactNode / Vue slotclassName / class?string
Additional classes merged with the component defaults.
stringThe 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.