Skip to content

Invite

The server invite card a link unfurls into inside a message.

The server invite card a link unfurls into inside a message. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

import { Button } from "@/registry/bases/newt/ui/button"
import {
  Invite,
  InviteBody,
  InviteCount,
  InviteCounts,
  InviteEyebrow,
  InviteIcon,
  InviteInfo,
  InviteName,
} from "@/registry/bases/newt/ui/invite"

/* Inline so the demo never reaches out to the network for a placeholder. */
const SERVER_ICON =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' rx='12' fill='%235865f2'/%3E%3C/svg%3E"

export default function InviteDemo() {
  return (
    <Invite>
      <InviteEyebrow>You&apos;ve been invited to join a server</InviteEyebrow>
      <InviteBody>
        <InviteIcon src={SERVER_ICON} />
        <InviteInfo>
          <InviteName href="#">Newt Community</InviteName>
          <InviteCounts>
            <InviteCount online>1,024 Online</InviteCount>
            <InviteCount>8,192 Members</InviteCount>
          </InviteCounts>
        </InviteInfo>
        <Button variant="success">Join</Button>
      </InviteBody>
    </Invite>
  )
}
<script setup lang="ts">
import { Button } from "~~/registry/bases/newt/ui/button"
import {
  Invite,
  InviteBody,
  InviteCount,
  InviteCounts,
  InviteEyebrow,
  InviteIcon,
  InviteInfo,
  InviteName,
} from "~~/registry/bases/newt/ui/invite"

/* Inline so the demo never reaches out to the network for a placeholder. */
const SERVER_ICON =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' rx='12' fill='%235865f2'/%3E%3C/svg%3E"
</script>

<template>
  <Invite>
    <InviteEyebrow>You've been invited to join a server</InviteEyebrow>
    <InviteBody>
      <InviteIcon :src="SERVER_ICON" />
      <InviteInfo>
        <InviteName href="#">Newt Community</InviteName>
        <InviteCounts>
          <InviteCount online>1,024 Online</InviteCount>
          <InviteCount>8,192 Members</InviteCount>
        </InviteCounts>
      </InviteInfo>
      <Button variant="success">Join</Button>
    </InviteBody>
  </Invite>
</template>

Installation

pnpm dlx newtui@latest add invite
npx newtui@latest add invite
yarn dlx newtui@latest add invite
bunx newtui@latest add invite

Add the components it builds on

Copy the source into your project

import * as React from "react"

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

/* The server-invite card a link unfurls into inside a message. */
const Invite = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "max-w-[432px] rounded-md bg-newt-bg-surface px-4 pb-4 pt-2 text-newt-text-primary",
      className
    )}
    {...props}
  />
))
Invite.displayName = "Invite"

const InviteEyebrow = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn(
      "text-[12px] font-extrabold uppercase tracking-[0.02em] text-newt-text-muted",
      className
    )}
    {...props}
  />
))
InviteEyebrow.displayName = "InviteEyebrow"

const InviteBody = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("mt-2 flex items-center gap-4", className)}
    {...props}
  />
))
InviteBody.displayName = "InviteBody"

const InviteIcon = React.forwardRef<
  HTMLImageElement,
  React.ComponentProps<"img">
>(({ className, alt = "", ...props }, ref) => (
  <img
    ref={ref}
    alt={alt}
    aria-hidden={alt === "" ? true : undefined}
    className={cn("h-12 w-12 shrink-0 rounded-md object-cover", className)}
    {...props}
  />
))
InviteIcon.displayName = "InviteIcon"

const InviteInfo = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("min-w-0 flex-1", className)} {...props} />
))
InviteInfo.displayName = "InviteInfo"

const InviteName = React.forwardRef<
  HTMLAnchorElement,
  React.ComponentProps<"a">
>(({ className, ...props }, ref) => (
  <a
    ref={ref}
    className={cn(
      "block truncate font-bold text-newt-text-primary no-underline hover:underline",
      className
    )}
    {...props}
  />
))
InviteName.displayName = "InviteName"

const InviteCounts = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "mt-0.5 flex items-center gap-3 text-sm text-newt-text-muted",
      className
    )}
    {...props}
  />
))
InviteCounts.displayName = "InviteCounts"

export interface InviteCountProps extends React.HTMLAttributes<HTMLSpanElement> {
  /** Green dot for the online count, muted for the member total. */
  online?: boolean
}

const InviteCount = React.forwardRef<HTMLSpanElement, InviteCountProps>(
  ({ className, online = false, children, ...props }, ref) => (
    <span
      ref={ref}
      className={cn("inline-flex items-center gap-1.5", className)}
      {...props}
    >
      <span
        aria-hidden="true"
        className={cn(
          "h-2 w-2 shrink-0 rounded-full",
          online ? "bg-newt-online" : "bg-newt-offline"
        )}
      />
      {children}
    </span>
  )
)
InviteCount.displayName = "InviteCount"

export {
  Invite,
  InviteEyebrow,
  InviteBody,
  InviteIcon,
  InviteInfo,
  InviteName,
  InviteCounts,
  InviteCount,
}
<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(
        'max-w-[432px] rounded-md bg-newt-bg-surface px-4 pb-4 pt-2 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>
  <span
    :class="
      cn(
        'text-[12px] font-extrabold uppercase tracking-[0.02em] 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('mt-2 flex 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"]; src: string; alt?: string }>(),
  { alt: "" }
)
</script>

<template>
  <img
    :src="props.src"
    :alt="props.alt"
    :aria-hidden="props.alt === '' ? true : undefined"
    :class="cn('h-12 w-12 shrink-0 rounded-md object-cover', 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>
  <div :class="cn('min-w-0 flex-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>
  <a
    :class="
      cn(
        'block truncate font-bold text-newt-text-primary no-underline hover:underline',
        props.class
      )
    "
  >
    <slot />
  </a>
</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(
        'mt-0.5 flex items-center gap-3 text-sm 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 = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"]
    /** Green dot for the online count, muted for the member total. */
    online?: boolean
  }>(),
  { online: false }
)
</script>

<template>
  <span :class="cn('inline-flex items-center gap-1.5', props.class)">
    <span
      aria-hidden="true"
      :class="
        cn(
          'h-2 w-2 shrink-0 rounded-full',
          props.online ? 'bg-newt-online' : 'bg-newt-offline'
        )
      "
    />
    <slot />
  </span>
</template>
export { default as Invite } from "./Invite.vue"
export { default as InviteEyebrow } from "./InviteEyebrow.vue"
export { default as InviteBody } from "./InviteBody.vue"
export { default as InviteIcon } from "./InviteIcon.vue"
export { default as InviteInfo } from "./InviteInfo.vue"
export { default as InviteName } from "./InviteName.vue"
export { default as InviteCounts } from "./InviteCounts.vue"
export { default as InviteCount } from "./InviteCount.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 { Invite } from "@/components/ui/invite"

export function Example() {
  return <Invite />
}
<script setup lang="ts">
import { Invite, InviteEyebrow, InviteBody, InviteIcon, InviteInfo, InviteName, InviteCounts, InviteCount } from "@/components/ui/invite"
</script>

<template>
  <Invite />
</template>

API reference

InviteCount

PropType
online?boolean | undefined

Green dot for the online count, muted for the member total.

Typeboolean | undefined

The registry exports Invite, InviteEyebrow, InviteBody, InviteIcon, InviteInfo, InviteName, InviteCounts, InviteCount. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Use descriptive alternative text for a meaningful server image and an empty alt for a decorative one. The join action must remain a real button.

Tokens

Invite 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.