Skip to content

Dropdown Menu

A floating menu panel with icon, label and description items that highlight in brand color.

A floating menu panel with icon, label and description items that highlight in brand color. 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-dropdown
import {
  Dropdown,
  DropdownContent,
  DropdownDescription,
  DropdownDivider,
  DropdownIcon,
  DropdownItem,
  DropdownLabel,
} from "@/registry/bases/newt/ui/dropdown"

export default function DropdownDemo() {
  return (
    <Dropdown>
      <DropdownItem active>
        <DropdownIcon>{"⚡"}</DropdownIcon>
        <DropdownContent>
          <DropdownLabel>Option label</DropdownLabel>
          <DropdownDescription>Option description</DropdownDescription>
        </DropdownContent>
      </DropdownItem>
      <DropdownDivider />
      <DropdownItem>
        <DropdownIcon>{"⚙"}</DropdownIcon>
        <DropdownContent>
          <DropdownLabel>Another option</DropdownLabel>
          <DropdownDescription>Description</DropdownDescription>
        </DropdownContent>
      </DropdownItem>
    </Dropdown>
  )
}
<script setup lang="ts">
import {
  Dropdown,
  DropdownContent,
  DropdownDescription,
  DropdownDivider,
  DropdownIcon,
  DropdownItem,
  DropdownLabel,
} from "~~/registry/bases/newt/ui/dropdown"
</script>

<template>
  <Dropdown>
    <DropdownItem active>
      <DropdownIcon>&#x26A1;</DropdownIcon>
      <DropdownContent>
        <DropdownLabel>Option label</DropdownLabel>
        <DropdownDescription>Option description</DropdownDescription>
      </DropdownContent>
    </DropdownItem>
    <DropdownDivider />
    <DropdownItem>
      <DropdownIcon>&#x2699;</DropdownIcon>
      <DropdownContent>
        <DropdownLabel>Another option</DropdownLabel>
        <DropdownDescription>Description</DropdownDescription>
      </DropdownContent>
    </DropdownItem>
  </Dropdown>
</template>

Installation

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

Copy the source into your project

import * as React from "react"

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

const Dropdown = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    role="menu"
    className={cn(
      "w-60 rounded-md border border-newt-border bg-[var(--newt-bg-floating)] p-1.5 text-newt-text-primary shadow-elevation-high",
      className
    )}
    {...props}
  />
))
Dropdown.displayName = "Dropdown"

export interface DropdownItemProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Highlights the item as if hovered (e.g. keyboard focus / selected). */
  active?: boolean
}

const DropdownItem = React.forwardRef<HTMLDivElement, DropdownItemProps>(
  ({ className, active = false, ...props }, ref) => (
    <div
      ref={ref}
      role="menuitem"
      tabIndex={-1}
      data-active={active ? "" : undefined}
      className={cn(
        "group flex cursor-pointer items-center gap-2.5 rounded-sm px-2.5 py-2 transition-colors duration-fast ease-newt hover:bg-newt-brand data-[active]:bg-newt-brand",
        className
      )}
      {...props}
    />
  )
)
DropdownItem.displayName = "DropdownItem"

const DropdownIcon = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    aria-hidden="true"
    className={cn(
      "w-5 shrink-0 text-center text-newt-text-muted group-hover:text-white group-data-[active]:text-white",
      className
    )}
    {...props}
  />
))
DropdownIcon.displayName = "DropdownIcon"

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

const DropdownLabel = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("text-sm font-medium", className)} {...props} />
))
DropdownLabel.displayName = "DropdownLabel"

const DropdownDescription = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "text-xs text-newt-text-muted group-hover:text-white/75 group-data-[active]:text-white/75",
      className
    )}
    {...props}
  />
))
DropdownDescription.displayName = "DropdownDescription"

const DropdownDivider = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    role="separator"
    className={cn("mx-1.5 my-1 h-px bg-newt-border", className)}
    {...props}
  />
))
DropdownDivider.displayName = "DropdownDivider"

export {
  Dropdown,
  DropdownItem,
  DropdownIcon,
  DropdownContent,
  DropdownLabel,
  DropdownDescription,
  DropdownDivider,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div
    role="menu"
    :class="
      cn(
        'w-60 rounded-md border border-newt-border bg-[var(--newt-bg-floating)] p-1.5 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"]
  /** Highlights the item as if hovered (e.g. keyboard focus / selected). */
  active?: boolean
}>()
</script>

<template>
  <div
    role="menuitem"
    tabindex="-1"
    :data-active="props.active ? '' : undefined"
    :class="
      cn(
        'group flex cursor-pointer items-center gap-2.5 rounded-sm px-2.5 py-2 transition-colors duration-fast ease-newt hover:bg-newt-brand data-[active]: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>
  <span
    aria-hidden="true"
    :class="
      cn(
        'w-5 shrink-0 text-center text-newt-text-muted group-hover:text-white group-data-[active]:text-white',
        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('min-w-0', 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-sm font-medium', 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-xs text-newt-text-muted group-hover:text-white/75 group-data-[active]:text-white/75',
        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('mx-1.5 my-1 h-px bg-newt-border', props.class)"
  />
</template>
export { default as Dropdown } from "./Dropdown.vue"
export { default as DropdownItem } from "./DropdownItem.vue"
export { default as DropdownIcon } from "./DropdownIcon.vue"
export { default as DropdownContent } from "./DropdownContent.vue"
export { default as DropdownLabel } from "./DropdownLabel.vue"
export { default as DropdownDescription } from "./DropdownDescription.vue"
export { default as DropdownDivider } from "./DropdownDivider.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 { Dropdown } from "@/components/ui/dropdown"

export function Example() {
  return <Dropdown />
}
<script setup lang="ts">
import { Dropdown, DropdownItem, DropdownIcon, DropdownContent, DropdownLabel, DropdownDescription, DropdownDivider } from "@/components/ui/dropdown"
</script>

<template>
  <Dropdown />
</template>

API reference

DropdownItem

PropType
active?boolean | undefined

Highlights the item as if hovered (e.g. keyboard focus / selected).

Typeboolean | undefined

The registry exports Dropdown, DropdownItem, DropdownIcon, DropdownContent, DropdownLabel, DropdownDescription, DropdownDivider. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Connect the trigger and menu with aria-controls and aria-expanded, support arrow keys and Escape, and return focus to the trigger on close.

Tokens

Dropdown Menu 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-dropdown.