Skip to content

User Profile Popout

Floating profile card with a banner, overlapping avatar, bio section and role tags.

Floating profile card with a banner, overlapping avatar, bio section and role tags. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

Root class newt-profile
import { Avatar } from "@/registry/bases/newt/ui/avatar"
import { Button } from "@/registry/bases/newt/ui/button"
import { RoleTag } from "@/registry/bases/newt/ui/role-tag"
import {
  StatusDot,
  StatusIndicator,
} from "@/registry/bases/newt/ui/status-indicator"
import {
  UserProfile,
  UserProfileActions,
  UserProfileAvatar,
  UserProfileBanner,
  UserProfileBio,
  UserProfileBody,
  UserProfileDivider,
  UserProfileHandle,
  UserProfileHead,
  UserProfileName,
  UserProfileRoles,
  UserProfileSectionLabel,
} from "@/registry/bases/newt/ui/user-profile"

export default function UserProfileDemo() {
  return (
    <UserProfile>
      <UserProfileBanner />
      <UserProfileHead>
        <UserProfileActions>
          <Button variant="secondary" size="icon" aria-label="Send message">
            ✉
          </Button>
        </UserProfileActions>
        <UserProfileAvatar>
          <StatusIndicator>
            <Avatar size="lg">U</Avatar>
            <StatusDot status="online" />
          </StatusIndicator>
        </UserProfileAvatar>
        <UserProfileName>username</UserProfileName>
        <UserProfileHandle>username#0000</UserProfileHandle>
        <UserProfileDivider />
      </UserProfileHead>
      <UserProfileBody>
        <UserProfileSectionLabel>About me</UserProfileSectionLabel>
        <UserProfileBio>Bio text goes here.</UserProfileBio>
        <UserProfileDivider />
        <UserProfileSectionLabel>Roles</UserProfileSectionLabel>
        <UserProfileRoles>
          <RoleTag>Role</RoleTag>
        </UserProfileRoles>
      </UserProfileBody>
    </UserProfile>
  )
}
<script setup lang="ts">
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import { Button } from "~~/registry/bases/newt/ui/button"
import { RoleTag } from "~~/registry/bases/newt/ui/role-tag"
import { StatusIndicator } from "~~/registry/bases/newt/ui/status-indicator"
import {
  UserProfile,
  UserProfileActions,
  UserProfileAvatar,
  UserProfileBanner,
  UserProfileBio,
  UserProfileBody,
  UserProfileDivider,
  UserProfileHandle,
  UserProfileHead,
  UserProfileName,
  UserProfileRoles,
  UserProfileSectionLabel,
} from "~~/registry/bases/newt/ui/user-profile"
</script>

<template>
  <UserProfile>
    <UserProfileBanner />
    <UserProfileHead>
      <UserProfileActions>
        <Button variant="secondary" size="icon" aria-label="Send message"
          >✉</Button
        >
      </UserProfileActions>
      <UserProfileAvatar>
        <StatusIndicator status="online">
          <Avatar size="lg">U</Avatar>
        </StatusIndicator>
      </UserProfileAvatar>
      <UserProfileName>username</UserProfileName>
      <UserProfileHandle>username#0000</UserProfileHandle>
      <UserProfileDivider />
    </UserProfileHead>
    <UserProfileBody>
      <UserProfileSectionLabel>About me</UserProfileSectionLabel>
      <UserProfileBio>Bio text goes here.</UserProfileBio>
      <UserProfileDivider />
      <UserProfileSectionLabel>Roles</UserProfileSectionLabel>
      <UserProfileRoles>
        <RoleTag>Role</RoleTag>
      </UserProfileRoles>
    </UserProfileBody>
  </UserProfile>
</template>

Installation

pnpm dlx newtui@latest add user-profile
npx newtui@latest add user-profile
yarn dlx newtui@latest add user-profile
bunx newtui@latest add user-profile

Add the components it builds on

Copy the source into your project

import * as React from "react"

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

const UserProfile = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "w-[280px] overflow-hidden rounded-lg border border-newt-border bg-newt-bg-base shadow-elevation-high",
      className
    )}
    {...props}
  />
))
UserProfile.displayName = "UserProfile"

const UserProfileBanner = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("h-[60px] bg-newt-brand", className)}
    {...props}
  />
))
UserProfileBanner.displayName = "UserProfileBanner"

const UserProfileHead = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("relative px-4 pb-3", className)} {...props} />
))
UserProfileHead.displayName = "UserProfileHead"

const UserProfileActions = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex justify-end gap-2 pb-9 pt-2", className)}
    {...props}
  />
))
UserProfileActions.displayName = "UserProfileActions"

const UserProfileAvatar = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "absolute -top-6 start-4 rounded-full border-4 border-newt-bg-base",
      className
    )}
    {...props}
  />
))
UserProfileAvatar.displayName = "UserProfileAvatar"

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

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

const UserProfileDivider = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    role="separator"
    className={cn("my-[10px] h-px bg-newt-border", className)}
    {...props}
  />
))
UserProfileDivider.displayName = "UserProfileDivider"

const UserProfileBody = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("px-4 pb-4", className)} {...props} />
))
UserProfileBody.displayName = "UserProfileBody"

const UserProfileSectionLabel = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "mb-[6px] text-[11px] font-bold uppercase tracking-[0.06em] text-newt-text-muted",
      className
    )}
    {...props}
  />
))
UserProfileSectionLabel.displayName = "UserProfileSectionLabel"

const UserProfileBio = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "text-[13px] leading-[1.5] text-newt-text-secondary",
      className
    )}
    {...props}
  />
))
UserProfileBio.displayName = "UserProfileBio"

const UserProfileRoles = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("flex flex-wrap gap-1", className)} {...props} />
))
UserProfileRoles.displayName = "UserProfileRoles"

export {
  UserProfile,
  UserProfileBanner,
  UserProfileHead,
  UserProfileActions,
  UserProfileAvatar,
  UserProfileName,
  UserProfileHandle,
  UserProfileDivider,
  UserProfileBody,
  UserProfileSectionLabel,
  UserProfileBio,
  UserProfileRoles,
}
<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(
        'w-[280px] overflow-hidden rounded-lg border border-newt-border bg-newt-bg-base shadow-elevation-high',
        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('h-[60px] bg-newt-brand', 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('relative 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 :class="cn('flex justify-end gap-2 pb-9 pt-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(
        'absolute -top-6 start-4 rounded-full border-4 border-newt-bg-base',
        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-base 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('mb-[10px] 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>
  <div
    role="separator"
    :class="cn('my-[10px] h-px bg-newt-border', 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('px-4 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(
        'mb-[6px] text-[11px] font-bold uppercase tracking-[0.06em] 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>
  <div
    :class="
      cn('text-[13px] leading-[1.5] text-newt-text-secondary', 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-wrap gap-1', props.class)">
    <slot />
  </div>
</template>
export { default as UserProfile } from "./UserProfile.vue"
export { default as UserProfileBanner } from "./UserProfileBanner.vue"
export { default as UserProfileHead } from "./UserProfileHead.vue"
export { default as UserProfileActions } from "./UserProfileActions.vue"
export { default as UserProfileAvatar } from "./UserProfileAvatar.vue"
export { default as UserProfileName } from "./UserProfileName.vue"
export { default as UserProfileHandle } from "./UserProfileHandle.vue"
export { default as UserProfileDivider } from "./UserProfileDivider.vue"
export { default as UserProfileBody } from "./UserProfileBody.vue"
export { default as UserProfileSectionLabel } from "./UserProfileSectionLabel.vue"
export { default as UserProfileBio } from "./UserProfileBio.vue"
export { default as UserProfileRoles } from "./UserProfileRoles.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 { UserProfile } from "@/components/ui/user-profile"

export function Example() {
  return <UserProfile />
}
<script setup lang="ts">
import { UserProfile, UserProfileBanner, UserProfileHead, UserProfileActions, UserProfileAvatar, UserProfileName, UserProfileHandle, UserProfileDivider, UserProfileBody, UserProfileSectionLabel, UserProfileBio, UserProfileRoles } from "@/components/ui/user-profile"
</script>

<template>
  <UserProfile />
</template>

API reference

UserProfile

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 UserProfile, UserProfileBanner, UserProfileHead, UserProfileActions, UserProfileAvatar, UserProfileName, UserProfileHandle, UserProfileDivider, UserProfileBody, UserProfileSectionLabel, UserProfileBio, UserProfileRoles. 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

User Profile Popout 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-profile.