BtnGroupDropDown

Keep the primary action in reach and its alternatives close by. A native disclosure with a button-styled trigger, an open content slot, and independent menu styling.

BtnGroupDropDown

View source · API reference

Use it beside a primary action inside BtnGroup, or on its own for a compact set of links and actions. The trigger is a native summary inside details; menu items retain their own button or link semantics.

Playground

Explore all thirteen props. Trigger appearance and menu appearance are independent: try a strong accent trigger with a neutral surface menu. Open the preview after changing a setting to see the result. Its links navigate to real component guides.

An empty accessible-name field lets the visible label name the trigger. In icon-only mode, the component falls back to the label. The playground starts with a neutral menu; omitting menuTone instead inherits the surrounding tone.

Split publish action

A primary action with nearby publishing choices, adapted from the framework's Button Group Samples. Publish immediately, mark the draft ready, or schedule the preview for tomorrow. Each choice updates the visible state. Published previews disable publishing and scheduling until you return to draft or reset.

This example models the workflow locally; it does not publish content or schedule a background job. The action handler closes the disclosure and returns focus to its trigger.

<RegorApp id="publish-actions" src="./publish-actions.ts"/>
import {
  defineBadgeComponents,
  defineBtnGroupComponents,
  defineButtonComponents,
  defineFlexComponents,
  defineIconComponents,
  definePanelComponents,
} from '@purestack/ts-components'
import type { SemanticTone } from '@purestack/ts-style'
import {
  iconoir_calendar,
  iconoir_check,
  iconoir_send,
  lucide_chevron_down,
} from '@purestack/ts-svg-icons'
import {
  type ComputedRef,
  computed,
  createApp,
  defineComponent,
  html,
  type Ref,
  ref,
} from 'regor'

export type ReleaseState = 'Draft' | 'Ready' | 'Scheduled' | 'Published'

export interface PublishActions {
  releaseState: Ref<ReleaseState>
  releaseTone: ComputedRef<SemanticTone>
  isPublished: ComputedRef<boolean>
  feedback: ComputedRef<string>
  publish: () => void
  choose: (state: 'Draft' | 'Ready' | 'Scheduled', event: Event) => void
  reset: () => void
}

const publishActionsTemplate = html`<Panel tone="neutral" variant="surfaceAlt" class="p-4 overflow-visible">
  <Flex direction="column" align="start" class="gap-3">
    <div>
      <div class="fs-h2 fw-700">Release notes</div>
      <p class="tone-text-muted mb-0">Keep the main action close to its publishing choices.</p>
    </div>
    <Flex wrap="true" align="center" role="status">
      <Badge :tone="releaseTone">{{ releaseState }}</Badge>
      <span>{{ feedback }}</span>
    </Flex>
    <BtnGroup :wrap="true" role="group" aria-label="Publishing actions">
      <Btn tone="accent" icon="iconoir:send" :disabled="isPublished" @click="publish">Publish now</Btn>
      <BtnGroupDropDown label="More" tone="accent" menuTone="neutral" align="start">
        <Btn variant="subtle" icon="iconoir:calendar" :disabled="isPublished" @click="choose('Scheduled', $event)">Schedule for tomorrow</Btn>
        <Btn variant="subtle" icon="iconoir:check" :disabled="isPublished" @click="choose('Ready', $event)">Mark ready</Btn>
        <Btn variant="subtle" @click="choose('Draft', $event)">Return to draft</Btn>
      </BtnGroupDropDown>
    </BtnGroup>
    <Btn variant="link" @click="reset">Reset release</Btn>
  </Flex>
</Panel>`

function createPublishActions(): PublishActions {
  const releaseState = ref<ReleaseState>('Draft')
  const isPublished = computed(() => releaseState() === 'Published')
  const releaseTone = computed<SemanticTone>(() =>
    releaseState() === 'Draft'
      ? 'neutral'
      : releaseState() === 'Scheduled'
        ? 'info'
        : 'success',
  )
  const feedback = computed<string>(
    () =>
      ({
        Draft: 'Changes stay in this example.',
        Ready: 'Reviewed and ready to publish.',
        Scheduled: 'Preview scheduled for tomorrow at 09:00.',
        Published: 'Preview release published.',
      })[releaseState()],
  )
  return {
    releaseState,
    releaseTone,
    isPublished,
    feedback,
    publish: () => releaseState('Published'),
    choose: (state, event) => {
      releaseState(state)
      const menu = (event.currentTarget as HTMLElement).closest('details')
      if (menu) {
        menu.open = false
        menu.querySelector('summary')?.focus()
      }
    },
    reset: () => releaseState('Draft'),
  }
}

const icons: Record<string, string> = {
  'lucide:chevron-down': lucide_chevron_down,
  'iconoir:send': iconoir_send,
  'iconoir:calendar': iconoir_calendar,
  'iconoir:check': iconoir_check,
}
const component = defineComponent<PublishActions>(publishActionsTemplate, {
  context: createPublishActions,
})

createApp(
  {
    components: {
      PublishActions: component,
      ...defineBadgeComponents(),
      ...defineBtnGroupComponents(),
      ...defineButtonComponents(),
      ...defineFlexComponents(),
      ...definePanelComponents(),
      ...defineIconComponents((name) => {
        if (!icons[name]) throw new Error(`Icon is not registered: ${name}`)
        return icons[name]
      }),
    },
  },
  { selector: 'app#publish-actions', template: html`<PublishActions/>` },
)

The default slot accepts links, buttons, and ordinary markup. Add a short heading or description to explain the choices. Navigation remains native, so readers can copy a destination or open it in another tab.

Compact triggers

Use an icon-only trigger when space is tight and its context is clear. Give it a specific accessible name. Set size on each trigger or button independently; a group does not forward its children's appearance props.

For a working icon-only action menu with selection state, try the selection toolbar.

Placement and wrapping

align anchors the menu to the trigger's start or end edge. It does not align the whole button group. PureStack's menu runtime adjusts placement to the viewport, can place a menu above the trigger, and limits its height when space is tight.

The row below wraps at narrow widths. Its surrounding Panel uses overflow-visible so it cannot clip the open menu. Use this utility on any preview container whose normal styles hide overflow.

Behavior and accessibility

  • Tab reaches the summary. Enter or Space toggles the native disclosure. Tab then moves through its links and buttons in document order.
  • PureStack's built-in menu runtime closes open menus on Escape or an outside click. Opening a menu closes the other open menus.
  • Selecting an item inside the panel does not automatically close the menu. The publish example closes it explicitly and restores focus after completing an action.
  • Use a specific ariaLabel for icon-only triggers, such as “More message actions”. Visible triggers normally use their label as their accessible name.
  • This is a disclosure containing ordinary controls. It does not add ARIA menu roles, arrow-key selection, or a focus trap. Keep native child semantics unless you implement a complete alternative keyboard model.
  • An icon-only trigger falls back to label, then “More actions”, for its accessible name. A name that describes the actual choices is more useful.
  • There is no dropdown disabled prop. Disable individual actions with Btn, or omit the disclosure when none of its content is useful.

API reference

<BtnGroupDropDown label="More">

A native <details> with a <summary> trigger, thirteen public props, and one default slot. Trigger and panel variants have separate defaults.

All props accept values or Regor refs. Register defineBtnGroupComponents() and defineIconComponents() in browser applications, along with the families used in the slot. Even the default trigger uses an icon: register lucide:chevron-down unless you supply another one.

Trigger content

Trigger appearance

Slots, attributes, and events

Default slot

The menu contents. Direct Btn and BtnLink children fill the menu width and align their labels to the start. Plain markup can provide headings, descriptions, or metadata. The trigger text comes from label, not the slot.

Native attributes

id, class, and other unconsumed attributes reach the root details element. Its native open state determines visibility.

Events and closing

The root exposes native details events such as toggle. Each child owns its click or navigation behavior. There is no custom selection event or automatic close-on-select behavior.

Runtime

ts-ssg includes its standard menu runtime for outside-click dismissal, Escape dismissal, and viewport placement. Native details still toggles without JavaScript. A browser app mounted with RegorApp uses that same page runtime.

For a complete action handler that updates state, closes the disclosure, and restores focus, open publish-actions.ts in the split publish example's source tabs.