Skip to content

App Launcher

The Apps popout, with search, a recents strip and the apps installed in a server.

The Apps popout, with search, a recents strip and the apps installed in a server. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

import {
  AppLauncher,
  AppLauncherBody,
  AppLauncherHandle,
  AppLauncherItem,
  AppLauncherItemIcon,
  AppLauncherList,
  AppLauncherRecent,
  AppLauncherRecents,
  AppLauncherSearch,
  AppLauncherSearchInput,
  AppLauncherSection,
  AppLauncherSectionHeader,
  AppLauncherSectionTitle,
  AppLauncherViewMore,
} from "@/registry/bases/newt/ui/app-launcher"

const ShieldIcon = () => (
  <svg viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2 4 5v7c0 5 3.4 8.4 8 10 4.6-1.6 8-5 8-10V5l-8-3Z" />
  </svg>
)

const BoardIcon = () => (
  <svg viewBox="0 0 24 24" fill="currentColor">
    <path d="M4 4h16v12H8l-4 4V4Z" />
  </svg>
)

export default function AppLauncherDemo() {
  return (
    <AppLauncher aria-label="Apps" className="h-[420px]">
      <AppLauncherHandle />

      <AppLauncherSearch>
        <AppLauncherSearchInput
          placeholder="Search Apps & Commands"
          aria-label="Search apps and commands"
        />
      </AppLauncherSearch>

      <AppLauncherBody>
        <AppLauncherSection aria-label="Recents">
          <AppLauncherSectionHeader>
            <AppLauncherSectionTitle>Recents</AppLauncherSectionTitle>
            <AppLauncherViewMore>View More</AppLauncherViewMore>
          </AppLauncherSectionHeader>
          <AppLauncherRecents>
            <AppLauncherRecent aria-label="Moderation">
              <ShieldIcon />
            </AppLauncherRecent>
            <AppLauncherRecent aria-label="Poll Night">
              <BoardIcon />
            </AppLauncherRecent>
          </AppLauncherRecents>
        </AppLauncherSection>

        <AppLauncherSection aria-label="Apps in this server">
          <AppLauncherSectionHeader>
            <AppLauncherSectionTitle>
              Apps in this Server
            </AppLauncherSectionTitle>
            <AppLauncherViewMore>View More</AppLauncherViewMore>
          </AppLauncherSectionHeader>
          <AppLauncherList>
            <AppLauncherItem
              name="Moderation"
              description="Bans, timeouts and audit logs."
              icon={
                <AppLauncherItemIcon round>
                  <ShieldIcon />
                </AppLauncherItemIcon>
              }
            />
            <AppLauncherItem
              promoted
              name="Poll Night"
              description="Run a quick poll in any channel."
              icon={
                <AppLauncherItemIcon>
                  <BoardIcon />
                </AppLauncherItemIcon>
              }
            />
          </AppLauncherList>
        </AppLauncherSection>
      </AppLauncherBody>
    </AppLauncher>
  )
}
<script setup lang="ts">
import {
  AppLauncher,
  AppLauncherBody,
  AppLauncherHandle,
  AppLauncherItem,
  AppLauncherItemIcon,
  AppLauncherList,
  AppLauncherRecent,
  AppLauncherRecents,
  AppLauncherSearch,
  AppLauncherSearchInput,
  AppLauncherSection,
  AppLauncherSectionHeader,
  AppLauncherSectionTitle,
  AppLauncherViewMore,
} from "~~/registry/bases/newt/ui/app-launcher"

const shieldPath = "M12 2 4 5v7c0 5 3.4 8.4 8 10 4.6-1.6 8-5 8-10V5l-8-3Z"
const boardPath = "M4 4h16v12H8l-4 4V4Z"
</script>

<template>
  <AppLauncher aria-label="Apps" class="h-[420px]">
    <AppLauncherHandle />

    <AppLauncherSearch>
      <AppLauncherSearchInput
        placeholder="Search Apps & Commands"
        aria-label="Search apps and commands"
      />
    </AppLauncherSearch>

    <AppLauncherBody>
      <AppLauncherSection aria-label="Recents">
        <AppLauncherSectionHeader>
          <AppLauncherSectionTitle>Recents</AppLauncherSectionTitle>
          <AppLauncherViewMore>View More</AppLauncherViewMore>
        </AppLauncherSectionHeader>
        <AppLauncherRecents>
          <AppLauncherRecent aria-label="Moderation">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path :d="shieldPath" />
            </svg>
          </AppLauncherRecent>
          <AppLauncherRecent aria-label="Poll Night">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path :d="boardPath" />
            </svg>
          </AppLauncherRecent>
        </AppLauncherRecents>
      </AppLauncherSection>

      <AppLauncherSection aria-label="Apps in this server">
        <AppLauncherSectionHeader>
          <AppLauncherSectionTitle>Apps in this Server</AppLauncherSectionTitle>
          <AppLauncherViewMore>View More</AppLauncherViewMore>
        </AppLauncherSectionHeader>
        <AppLauncherList>
          <AppLauncherItem
            name="Moderation"
            description="Bans, timeouts and audit logs."
          >
            <template #icon>
              <AppLauncherItemIcon round>
                <svg viewBox="0 0 24 24" fill="currentColor">
                  <path :d="shieldPath" />
                </svg>
              </AppLauncherItemIcon>
            </template>
          </AppLauncherItem>
          <AppLauncherItem
            promoted
            name="Poll Night"
            description="Run a quick poll in any channel."
          >
            <template #icon>
              <AppLauncherItemIcon>
                <svg viewBox="0 0 24 24" fill="currentColor">
                  <path :d="boardPath" />
                </svg>
              </AppLauncherItemIcon>
            </template>
          </AppLauncherItem>
        </AppLauncherList>
      </AppLauncherSection>
    </AppLauncherBody>
  </AppLauncher>
</template>

Installation

pnpm dlx newtui@latest add app-launcher
npx newtui@latest add app-launcher
yarn dlx newtui@latest add app-launcher
bunx newtui@latest add app-launcher

Copy the source into your project

import * as React from "react"

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

/* The Apps popout: search, recents, and the apps installed in a server. */
const AppLauncher = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, role = "dialog", ...props }, ref) => (
  <div
    ref={ref}
    role={role}
    className={cn(
      "flex h-[min(680px,calc(100dvh-2rem))] w-full max-w-[504px] flex-col overflow-hidden rounded-md border border-newt-border bg-newt-bg-surface text-newt-text-primary shadow-elevation-high",
      className
    )}
    {...props}
  />
))
AppLauncher.displayName = "AppLauncher"

/* Grip shown when the launcher is presented as a bottom sheet. */
const AppLauncherHandle = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    aria-hidden="true"
    className={cn(
      "mx-auto mt-2 h-1 w-9 shrink-0 rounded-full bg-newt-border",
      className
    )}
    {...props}
  />
))
AppLauncherHandle.displayName = "AppLauncherHandle"

/* Drill-in header: back control on the left, view title beside it. */
const AppLauncherHeader = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex h-14 shrink-0 items-center gap-2 px-2 text-base font-semibold",
      className
    )}
    {...props}
  />
))
AppLauncherHeader.displayName = "AppLauncherHeader"

const AppLauncherBack = React.forwardRef<
  HTMLButtonElement,
  React.ComponentProps<"button">
>(({ className, type = "button", ...props }, ref) => (
  <button
    ref={ref}
    type={type}
    className={cn(
      "inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-newt-bg-elevated 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}
  />
))
AppLauncherBack.displayName = "AppLauncherBack"

const SearchIcon = () => (
  <svg
    viewBox="0 0 20 20"
    fill="none"
    stroke="currentColor"
    strokeWidth="2"
    strokeLinecap="round"
    aria-hidden="true"
    className="pointer-events-none absolute start-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-newt-text-muted"
  >
    <circle cx="9" cy="9" r="5.5" />
    <path d="M13 13l4 4" />
  </svg>
)

/* Search field wrapper; place an `<AppLauncherSearchInput>` inside it. */
const AppLauncherSearch = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, children, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("shrink-0 px-4 pb-3 pt-4", className)}
    {...props}
  >
    <div className="relative flex items-center">
      <SearchIcon />
      {children}
    </div>
  </div>
))
AppLauncherSearch.displayName = "AppLauncherSearch"

const AppLauncherSearchInput = React.forwardRef<
  HTMLInputElement,
  React.ComponentProps<"input">
>(({ className, type = "search", ...props }, ref) => (
  <input
    ref={ref}
    type={type}
    autoComplete="off"
    spellCheck={false}
    className={cn(
      "h-11 w-full appearance-none rounded-md border-2 border-transparent bg-newt-bg-input py-0 ps-11 pe-3 text-base text-newt-text-primary outline-none",
      "placeholder:text-newt-text-muted focus:border-newt-brand",
      className
    )}
    {...props}
  />
))
AppLauncherSearchInput.displayName = "AppLauncherSearchInput"

const AppLauncherBody = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto px-4 pb-4",
      className
    )}
    {...props}
  />
))
AppLauncherBody.displayName = "AppLauncherBody"

const AppLauncherSection = React.forwardRef<
  HTMLElement,
  React.ComponentProps<"section">
>(({ className, ...props }, ref) => (
  <section
    ref={ref}
    className={cn("flex flex-col gap-2", className)}
    {...props}
  />
))
AppLauncherSection.displayName = "AppLauncherSection"

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

const AppLauncherSectionTitle = React.forwardRef<
  HTMLHeadingElement,
  React.ComponentProps<"h2">
>(({ className, ...props }, ref) => (
  <h2
    ref={ref}
    className={cn(
      "m-0 inline-flex items-center gap-1 text-base font-medium text-newt-text-primary",
      className
    )}
    {...props}
  />
))
AppLauncherSectionTitle.displayName = "AppLauncherSectionTitle"

const AppLauncherViewMore = React.forwardRef<
  HTMLButtonElement,
  React.ComponentProps<"button">
>(({ className, type = "button", ...props }, ref) => (
  <button
    ref={ref}
    type={type}
    className={cn(
      "cursor-pointer rounded-sm border-0 bg-transparent p-0 text-base font-semibold text-newt-text-link no-underline hover:underline",
      "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
      className
    )}
    {...props}
  />
))
AppLauncherViewMore.displayName = "AppLauncherViewMore"

/* Card that stacks `<AppLauncherItem>` rows behind one rounded surface. */
const AppLauncherList = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex flex-col overflow-hidden rounded-md bg-newt-bg-elevated",
      "[&>*+*]:border-t [&>*+*]:border-newt-border",
      className
    )}
    {...props}
  />
))
AppLauncherList.displayName = "AppLauncherList"

export interface AppLauncherItemProps extends Omit<
  React.ComponentProps<"button">,
  "children"
> {
  name: string
  description?: string
  /** Icon for the app; pair with `<AppLauncherItemIcon>`. */
  icon?: React.ReactNode
  /** Marks a paid placement, as the client does for promoted apps. */
  promoted?: boolean
}

const AppLauncherItem = React.forwardRef<
  HTMLButtonElement,
  AppLauncherItemProps
>(
  (
    {
      className,
      name,
      description,
      icon,
      promoted = false,
      type = "button",
      ...props
    },
    ref
  ) => (
    <button
      ref={ref}
      type={type}
      className={cn(
        "flex w-full cursor-pointer items-center gap-3 border-0 bg-transparent px-3 py-3 text-start transition-colors duration-fast ease-newt",
        "hover:bg-newt-bg-hover",
        "focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-newt-text-link",
        className
      )}
      {...props}
    >
      {icon}
      <span className="flex min-w-0 flex-1 flex-col gap-0.5">
        <span className="flex min-w-0 items-center gap-1.5">
          <span className="truncate text-base font-medium leading-tight text-newt-text-primary">
            {name}
          </span>
          {promoted ? (
            <span className="shrink-0 rounded-full bg-newt-text-primary px-1.5 py-0.5 text-[10px] font-bold uppercase leading-none tracking-wide text-newt-bg-surface">
              Promoted
            </span>
          ) : null}
        </span>
        {description ? (
          <span className="truncate text-sm leading-snug text-newt-text-muted">
            {description}
          </span>
        ) : null}
      </span>
    </button>
  )
)
AppLauncherItem.displayName = "AppLauncherItem"

export interface AppLauncherItemIconProps extends React.ComponentProps<"span"> {
  /** Bots use circular avatars; games and activities keep the squircle. */
  round?: boolean
}

const AppLauncherItemIcon = React.forwardRef<
  HTMLSpanElement,
  AppLauncherItemIconProps
>(({ className, round = false, ...props }, ref) => (
  <span
    ref={ref}
    aria-hidden="true"
    className={cn(
      "inline-flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden bg-newt-bg-base text-newt-text-secondary",
      round ? "rounded-full" : "rounded-[10px]",
      "[&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:h-5 [&>svg]:w-5",
      className
    )}
    {...props}
  />
))
AppLauncherItemIcon.displayName = "AppLauncherItemIcon"

/* Horizontal strip of recently used apps. */
const AppLauncherRecents = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex gap-3 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
      className
    )}
    {...props}
  />
))
AppLauncherRecents.displayName = "AppLauncherRecents"

const AppLauncherRecent = React.forwardRef<
  HTMLButtonElement,
  React.ComponentProps<"button">
>(({ className, type = "button", ...props }, ref) => (
  <button
    ref={ref}
    type={type}
    className={cn(
      "inline-flex h-12 w-12 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-[14px] border-0 bg-newt-bg-base p-0 text-newt-text-secondary transition-[filter] duration-fast ease-newt",
      "hover:brightness-110",
      "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
      "[&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:h-6 [&>svg]:w-6",
      className
    )}
    {...props}
  />
))
AppLauncherRecent.displayName = "AppLauncherRecent"

export {
  AppLauncher,
  AppLauncherHandle,
  AppLauncherHeader,
  AppLauncherBack,
  AppLauncherSearch,
  AppLauncherSearchInput,
  AppLauncherBody,
  AppLauncherSection,
  AppLauncherSectionHeader,
  AppLauncherSectionTitle,
  AppLauncherViewMore,
  AppLauncherList,
  AppLauncherItem,
  AppLauncherItemIcon,
  AppLauncherRecents,
  AppLauncherRecent,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <!-- The Apps popout: search, recents, and the apps installed in a server. -->
  <div
    role="dialog"
    :class="
      cn(
        'flex h-[min(680px,calc(100dvh-2rem))] w-full max-w-[504px] flex-col overflow-hidden rounded-md border border-newt-border bg-newt-bg-surface text-newt-text-primary 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>
  <!-- Grip shown when the launcher is presented as a bottom sheet. -->
  <div
    aria-hidden="true"
    :class="
      cn(
        'mx-auto mt-2 h-1 w-9 shrink-0 rounded-full bg-newt-border',
        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(
        'flex h-14 shrink-0 items-center gap-2 px-2 text-base font-semibold',
        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-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-newt-bg-elevated 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>
  <!-- Search field wrapper; place an `<AppLauncherSearchInput>` inside it. -->
  <div :class="cn('shrink-0 px-4 pb-3 pt-4', props.class)">
    <div class="relative flex items-center">
      <svg
        viewBox="0 0 20 20"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        aria-hidden="true"
        class="pointer-events-none absolute start-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-newt-text-muted"
      >
        <circle cx="9" cy="9" r="5.5" />
        <path d="M13 13l4 4" />
      </svg>
      <slot />
    </div>
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <input
    type="search"
    autocomplete="off"
    spellcheck="false"
    :class="
      cn(
        'h-11 w-full appearance-none rounded-md border-2 border-transparent bg-newt-bg-input py-0 ps-11 pe-3 text-base text-newt-text-primary outline-none',
        'placeholder:text-newt-text-muted focus:border-newt-brand',
        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(
        'flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto 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>
  <section :class="cn('flex flex-col gap-2', 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>
  <div
    :class="cn('flex items-center justify-between gap-2 px-0.5', 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>
  <h2
    :class="
      cn(
        'm-0 inline-flex items-center gap-1 text-base font-medium text-newt-text-primary',
        props.class
      )
    "
  >
    <slot />
  </h2>
</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(
        'cursor-pointer rounded-sm border-0 bg-transparent p-0 text-base font-semibold text-newt-text-link no-underline hover:underline',
        '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>
  <!-- Card that stacks `<AppLauncherItem>` rows behind one rounded surface. -->
  <div
    :class="
      cn(
        'flex flex-col overflow-hidden rounded-md bg-newt-bg-elevated',
        '[&>*+*]:border-t [&>*+*]:border-newt-border',
        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"]
    name: string
    description?: string
    /** Marks a paid placement, as the client does for promoted apps. */
    promoted?: boolean
  }>(),
  { promoted: false }
)
</script>

<template>
  <button
    type="button"
    :class="
      cn(
        'flex w-full cursor-pointer items-center gap-3 border-0 bg-transparent px-3 py-3 text-start transition-colors duration-fast ease-newt',
        'hover:bg-newt-bg-hover',
        'focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-newt-text-link',
        props.class
      )
    "
  >
    <slot name="icon" />
    <span class="flex min-w-0 flex-1 flex-col gap-0.5">
      <span class="flex min-w-0 items-center gap-1.5">
        <span
          class="truncate text-base font-medium leading-tight text-newt-text-primary"
        >
          {{ props.name }}
        </span>
        <span
          v-if="props.promoted"
          class="shrink-0 rounded-full bg-newt-text-primary px-1.5 py-0.5 text-[10px] font-bold uppercase leading-none tracking-wide text-newt-bg-surface"
        >
          Promoted
        </span>
      </span>
      <span
        v-if="props.description"
        class="truncate text-sm leading-snug text-newt-text-muted"
      >
        {{ props.description }}
      </span>
    </span>
  </button>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"]
    /** Bots use circular avatars; games and activities keep the squircle. */
    round?: boolean
  }>(),
  { round: false }
)
</script>

<template>
  <span
    aria-hidden="true"
    :class="
      cn(
        'inline-flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden bg-newt-bg-base text-newt-text-secondary',
        props.round ? 'rounded-full' : 'rounded-[10px]',
        '[&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:h-5 [&>svg]:w-5',
        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>
  <!-- Horizontal strip of recently used apps. -->
  <div
    :class="
      cn(
        'flex gap-3 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
        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-12 w-12 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-[14px] border-0 bg-newt-bg-base p-0 text-newt-text-secondary transition-[filter] duration-fast ease-newt',
        'hover:brightness-110',
        'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link',
        '[&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:h-6 [&>svg]:w-6',
        props.class
      )
    "
  >
    <slot />
  </button>
</template>
export { default as AppLauncher } from "./AppLauncher.vue"
export { default as AppLauncherHandle } from "./AppLauncherHandle.vue"
export { default as AppLauncherHeader } from "./AppLauncherHeader.vue"
export { default as AppLauncherBack } from "./AppLauncherBack.vue"
export { default as AppLauncherSearch } from "./AppLauncherSearch.vue"
export { default as AppLauncherSearchInput } from "./AppLauncherSearchInput.vue"
export { default as AppLauncherBody } from "./AppLauncherBody.vue"
export { default as AppLauncherSection } from "./AppLauncherSection.vue"
export { default as AppLauncherSectionHeader } from "./AppLauncherSectionHeader.vue"
export { default as AppLauncherSectionTitle } from "./AppLauncherSectionTitle.vue"
export { default as AppLauncherViewMore } from "./AppLauncherViewMore.vue"
export { default as AppLauncherList } from "./AppLauncherList.vue"
export { default as AppLauncherItem } from "./AppLauncherItem.vue"
export { default as AppLauncherItemIcon } from "./AppLauncherItemIcon.vue"
export { default as AppLauncherRecents } from "./AppLauncherRecents.vue"
export { default as AppLauncherRecent } from "./AppLauncherRecent.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 { AppLauncher } from "@/components/ui/app-launcher"

export function Example() {
  return <AppLauncher />
}
<script setup lang="ts">
import { AppLauncher, AppLauncherHandle, AppLauncherHeader, AppLauncherBack, AppLauncherSearch, AppLauncherSearchInput, AppLauncherBody, AppLauncherSection, AppLauncherSectionHeader, AppLauncherSectionTitle, AppLauncherViewMore, AppLauncherList, AppLauncherItem, AppLauncherItemIcon, AppLauncherRecents, AppLauncherRecent } from "@/components/ui/app-launcher"
</script>

<template>
  <AppLauncher />
</template>

API reference

AppLauncherItem

PropType
name?string

Required name value.

Typestring
description?string | undefined

Optional description value.

Typestring | undefined
icon?React.ReactNode | undefined

Icon for the app; pair with `<AppLauncherItemIcon>`.

TypeReact.ReactNode | undefined
promoted?boolean | undefined

Marks a paid placement, as the client does for promoted apps.

Typeboolean | undefined

AppLauncherItemIcon

PropType
round?boolean | undefined

Bots use circular avatars; games and activities keep the squircle.

Typeboolean | undefined

The registry exports AppLauncher, AppLauncherHandle, AppLauncherHeader, AppLauncherBack, AppLauncherSearch, AppLauncherSearchInput, AppLauncherBody, AppLauncherSection, AppLauncherSectionHeader, AppLauncherSectionTitle, AppLauncherViewMore, AppLauncherList, AppLauncherItem, AppLauncherItemIcon, AppLauncherRecents, AppLauncherRecent. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Give the launcher an accessible heading, keep search labelled, and expose the active item while keyboard focus moves through the results.

Tokens

App Launcher 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.