Member List
A server member sidebar: role sections, presence, coloured names and APP tags.
A server member sidebar: role sections, presence, coloured names and APP tags. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
newt-memberimport { Avatar } from "@/registry/bases/newt/ui/avatar"
import {
MemberList,
MemberListAppTag,
MemberListHeading,
MemberListInfo,
MemberListItem,
MemberListName,
MemberListNameRow,
MemberListRole,
MemberListSection,
} from "@/registry/bases/newt/ui/member-list"
import { RoleTag } from "@/registry/bases/newt/ui/role-tag"
import {
StatusDot,
StatusIndicator,
} from "@/registry/bases/newt/ui/status-indicator"
export default function MemberListDemo() {
return (
<MemberList className="w-60">
<MemberListSection>
<MemberListHeading>Moderators — 1</MemberListHeading>
<MemberListItem>
<StatusIndicator>
<Avatar size="sm">A</Avatar>
<StatusDot status="online" />
</StatusIndicator>
<MemberListInfo>
<MemberListNameRow>
<MemberListName color="#f0b232">username</MemberListName>
<MemberListAppTag verified />
</MemberListNameRow>
<MemberListRole>Role</MemberListRole>
</MemberListInfo>
<RoleTag className="ml-auto">TAG</RoleTag>
</MemberListItem>
</MemberListSection>
<MemberListSection offline>
<MemberListHeading>Offline — 1</MemberListHeading>
<MemberListItem>
<StatusIndicator>
<Avatar size="sm">B</Avatar>
<StatusDot status="offline" />
</StatusIndicator>
<MemberListInfo>
<MemberListNameRow>
<MemberListName>someone</MemberListName>
</MemberListNameRow>
</MemberListInfo>
</MemberListItem>
</MemberListSection>
</MemberList>
)
}<script setup lang="ts">
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import {
MemberList,
MemberListAppTag,
MemberListHeading,
MemberListInfo,
MemberListItem,
MemberListName,
MemberListNameRow,
MemberListRole,
MemberListSection,
} from "~~/registry/bases/newt/ui/member-list"
import { RoleTag } from "~~/registry/bases/newt/ui/role-tag"
import {
StatusDot,
StatusIndicator,
} from "~~/registry/bases/newt/ui/status-indicator"
</script>
<template>
<MemberList class="w-60">
<MemberListSection>
<MemberListHeading>Moderators — 1</MemberListHeading>
<MemberListItem>
<StatusIndicator>
<Avatar size="sm">A</Avatar>
<StatusDot status="online" />
</StatusIndicator>
<MemberListInfo>
<MemberListNameRow>
<MemberListName color="#f0b232">username</MemberListName>
<MemberListAppTag verified />
</MemberListNameRow>
<MemberListRole>Role</MemberListRole>
</MemberListInfo>
<RoleTag class="ml-auto">TAG</RoleTag>
</MemberListItem>
</MemberListSection>
<MemberListSection offline>
<MemberListHeading>Offline — 1</MemberListHeading>
<MemberListItem>
<StatusIndicator>
<Avatar size="sm">B</Avatar>
<StatusDot status="offline" />
</StatusIndicator>
<MemberListInfo>
<MemberListNameRow>
<MemberListName>someone</MemberListName>
</MemberListNameRow>
</MemberListInfo>
</MemberListItem>
</MemberListSection>
</MemberList>
</template>Installation
pnpm dlx newtui@latest add member-listnpx newtui@latest add member-listyarn dlx newtui@latest add member-listbunx newtui@latest add member-listAdd the components it builds on
Copy the source into your project
import * as React from "react"
import { cn } from "@/lib/utils"
const MemberList = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
role="list"
className={cn("flex flex-col", className)}
{...props}
/>
))
MemberList.displayName = "MemberList"
/*
* Discord groups members under each hoisted role, then Online, then Offline.
* `offline` dims the whole group rather than every row on its own.
*/
export interface MemberListSectionProps extends React.HTMLAttributes<HTMLElement> {
offline?: boolean
}
const MemberListSection = React.forwardRef<HTMLElement, MemberListSectionProps>(
({ className, offline = false, ...props }, ref) => (
<section
ref={ref}
data-offline={offline || undefined}
className={cn(
"[&:not(:first-child)]:mt-3",
offline &&
"[&_[role=listitem]]:opacity-45 [&_[data-slot=member-name]]:text-newt-text-muted",
className
)}
{...props}
/>
)
)
MemberListSection.displayName = "MemberListSection"
const MemberListHeading = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"m-0 px-2 pb-0.5 text-[12px] font-semibold uppercase leading-4 tracking-[0.02em] text-newt-text-muted",
className
)}
{...props}
/>
))
MemberListHeading.displayName = "MemberListHeading"
const MemberListItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
role="listitem"
className={cn(
"flex min-w-0 cursor-pointer items-center gap-2.5 rounded-sm px-2 py-1.5 transition-colors duration-fast ease-newt hover:bg-newt-bg-hover",
className
)}
{...props}
/>
))
MemberListItem.displayName = "MemberListItem"
const MemberListNameRow = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex min-w-0 items-center gap-1", className)}
{...props}
/>
))
MemberListNameRow.displayName = "MemberListNameRow"
const MemberListInfo = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex min-w-0 flex-col leading-[1.2]", className)}
{...props}
/>
))
MemberListInfo.displayName = "MemberListInfo"
export interface MemberListNameProps extends Omit<
React.HTMLAttributes<HTMLSpanElement>,
"color"
> {
/** Render the name in the brand colour (e.g. highlighted role). */
colored?: boolean
/** Role colour. Any CSS colour; wins over `colored`. */
color?: string
}
const MemberListName = React.forwardRef<HTMLSpanElement, MemberListNameProps>(
({ className, colored = false, color, style, ...props }, ref) => (
<span
ref={ref}
data-slot="member-name"
/*
* SAFETY: `CSSProperties` has no index signature for custom properties,
* but React forwards unknown `--*` keys to the style attribute verbatim,
* so the extra key is written out exactly as spelled here.
*/
style={
color
? ({
...style,
"--newt-member-name-color": color,
} as React.CSSProperties)
: style
}
className={cn(
"truncate text-sm font-medium",
color
? "text-[var(--newt-member-name-color)]"
: colored
? "text-newt-brand"
: "text-newt-text-primary",
className
)}
{...props}
/>
)
)
MemberListName.displayName = "MemberListName"
/* The APP tag bots carry next to their name; `verified` adds Discord's check. */
export interface MemberListAppTagProps extends React.HTMLAttributes<HTMLSpanElement> {
verified?: boolean
}
const MemberListAppTag = React.forwardRef<
HTMLSpanElement,
MemberListAppTagProps
>(({ className, verified = false, children = "APP", ...props }, ref) => (
<span
ref={ref}
role="img"
aria-label={verified ? "Verified application" : "Application"}
className={cn(
"inline-flex shrink-0 items-center gap-0.5 rounded-sm bg-newt-brand px-1 py-px text-[9px] font-bold leading-none text-white",
className
)}
{...props}
>
{verified ? (
<svg
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
className="h-2.5 w-2.5"
>
<path
fill="currentColor"
d="M9.55 17.6 4.4 12.45l1.4-1.4 3.75 3.75 8.65-8.65 1.4 1.4z"
/>
</svg>
) : null}
{children}
</span>
))
MemberListAppTag.displayName = "MemberListAppTag"
const MemberListRole = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
<span
ref={ref}
className={cn("truncate text-xs text-newt-text-muted", className)}
{...props}
/>
))
MemberListRole.displayName = "MemberListRole"
export {
MemberList,
MemberListSection,
MemberListHeading,
MemberListItem,
MemberListInfo,
MemberListNameRow,
MemberListName,
MemberListAppTag,
MemberListRole,
}<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div role="list" :class="cn('flex flex-col', props.class)">
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
/*
* Discord groups members under each hoisted role, then Online, then Offline.
* `offline` dims the whole group rather than every row on its own.
*/
const props = defineProps<{
class?: HTMLAttributes["class"]
offline?: boolean
}>()
</script>
<template>
<section
:data-offline="props.offline || undefined"
:class="
cn(
'[&:not(:first-child)]:mt-3',
props.offline &&
'[&_[role=listitem]]:opacity-45 [&_[data-slot=member-name]]:text-newt-text-muted',
props.class
)
"
>
<slot />
</section>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<h3
:class="
cn(
'm-0 px-2 pb-0.5 text-[12px] font-semibold uppercase leading-4 tracking-[0.02em] text-newt-text-muted',
props.class
)
"
>
<slot />
</h3>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
role="listitem"
:class="
cn(
'flex min-w-0 cursor-pointer items-center gap-2.5 rounded-sm px-2 py-1.5 transition-colors duration-fast ease-newt hover:bg-newt-bg-hover',
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 min-w-0 flex-col leading-[1.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 min-w-0 items-center gap-1', props.class)">
<slot />
</div>
</template><script setup lang="ts">
import type { CSSProperties, HTMLAttributes } from "vue"
import { computed } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
/** Render the name in the brand colour. */
colored?: boolean
/** Role colour. Any CSS colour; wins over `colored`. */
color?: string
}>()
/*
* SAFETY: `CSSProperties` has no index signature for custom properties, but
* Vue writes unknown `--*` keys to the style attribute verbatim, so the extra
* key lands exactly as spelled here.
*/
const style = computed<CSSProperties | undefined>(() =>
props.color
? ({ "--newt-member-name-color": props.color } as CSSProperties)
: undefined
)
const colorClass = computed(() => {
if (props.color) return "text-[var(--newt-member-name-color)]"
return props.colored ? "text-newt-brand" : "text-newt-text-primary"
})
</script>
<template>
<span
data-slot="member-name"
:style="style"
:class="cn('truncate text-sm font-medium', colorClass, props.class)"
>
<slot />
</span>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
/* The APP tag bots carry next to their name; `verified` adds Discord's check. */
const props = defineProps<{
class?: HTMLAttributes["class"]
verified?: boolean
}>()
</script>
<template>
<span
role="img"
:aria-label="props.verified ? 'Verified application' : 'Application'"
:class="
cn(
'inline-flex shrink-0 items-center gap-0.5 rounded-sm bg-newt-brand px-1 py-px text-[9px] font-bold leading-none text-white',
props.class
)
"
>
<svg
v-if="props.verified"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
class="h-2.5 w-2.5"
>
<path
fill="currentColor"
d="M9.55 17.6 4.4 12.45l1.4-1.4 3.75 3.75 8.65-8.65 1.4 1.4z"
/>
</svg>
<slot>APP</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('truncate text-xs text-newt-text-muted', props.class)">
<slot />
</span>
</template>export { default as MemberList } from "./MemberList.vue"
export { default as MemberListSection } from "./MemberListSection.vue"
export { default as MemberListHeading } from "./MemberListHeading.vue"
export { default as MemberListItem } from "./MemberListItem.vue"
export { default as MemberListInfo } from "./MemberListInfo.vue"
export { default as MemberListNameRow } from "./MemberListNameRow.vue"
export { default as MemberListName } from "./MemberListName.vue"
export { default as MemberListAppTag } from "./MemberListAppTag.vue"
export { default as MemberListRole } from "./MemberListRole.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 { MemberList } from "@/components/ui/member-list"
export function Example() {
return <MemberList />
}<script setup lang="ts">
import { MemberList, MemberListSection, MemberListHeading, MemberListItem, MemberListInfo, MemberListNameRow, MemberListName, MemberListAppTag, MemberListRole } from "@/components/ui/member-list"
</script>
<template>
<MemberList />
</template>API reference
MemberListSection
offline?boolean | undefined
Optional offline value.
boolean | undefinedMemberListName
colored?boolean | undefined
Render the name in the brand colour (e.g. highlighted role).
boolean | undefinedcolor?string | undefined
Role colour. Any CSS colour; wins over `colored`.
string | undefinedMemberListAppTag
verified?boolean | undefined
Optional verified value.
boolean | undefinedThe registry exports MemberList, MemberListSection, MemberListHeading, MemberListItem, MemberListInfo, MemberListNameRow, MemberListName, MemberListAppTag, MemberListRole. 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
Member List 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. In the plain HTML and CSS distribution the same component is newt-member.