Release 2026.07.13
Serving all workspaces from fra1 and iad1.
Native popover menus with typed triggers, entries, placement, and keyboard state.
app/components/nitro_kit/dropdown.rbNitroKit::Dropdown.new(id:) { |menu| menu.trigger; menu.item }Nitro JavaScript positions menus from their triggers; the native no-JavaScript baseline remains safely bounded.
test/dummy/app/components/gallery/components/dropdown_page.rbNitroKit::Dropdown::PLACEMENTS.each do |placement|
sample(placement.to_s.humanize, slug: placement.to_s) do
render_menu("gallery-dropdown-#{placement}", placement:) do |menu|
menu.item("Open profile", href: "/gallery/profile")
menu.item("Copy account ID")
end
end
endTitles, links, buttons, separators, disabled items, and destructive intent remain native.
test/dummy/app/components/gallery/components/dropdown_page.rbrender NitroKit::Dropdown.new(id: "gallery-dropdown-account") do |menu|
menu.trigger("Account actions", variant: :primary)
menu.title("Workspace")
menu.item("Workspace settings", href: "/gallery/settings")
menu.item("Duplicate workspace")
menu.item("Export audit log", disabled: true)
menu.separator
menu.title("Danger zone")
menu.item("Delete workspace", variant: :destructive)
endThe compact overflow menu is an icon-only Button trigger with icon-led items.
A disabled trigger and long labels exercise non-interactive and constrained states.
test/dummy/app/components/gallery/components/dropdown_page.rbsample("Disabled trigger", slug: "disabled-trigger") do
render NitroKit::Dropdown.new(id: "gallery-dropdown-disabled") do |menu|
menu.trigger("Actions unavailable", disabled: true)
menu.item("Unavailable action", disabled: true)
end
end
sample("Long content", slug: "long-content") do
render NitroKit::Dropdown.new(id: "gallery-dropdown-long") do |menu|
menu.trigger("Deployment actions")
menu.item("Promote the currently verified release to production")
menu.item("Restore the previously healthy production release", variant: :destructive)
end
endCard, Badge, and Dropdown compose into a realistic deployment record.
Serving all workspaces from fra1 and iad1.
test/dummy/app/components/gallery/components/dropdown_page.rbrender NitroKit::Card.new(id: "gallery-dropdown-deployment-card") do |card|
card.title("Release 2026.07.13", level: 3)
card.body do
render NitroKit::Badge.new(
"Healthy",
id: "gallery-dropdown-deployment-status",
color: :success,
size: :sm
)
p { "Serving all workspaces from fra1 and iad1." }
end
card.footer do
render NitroKit::Dropdown.new(id: "gallery-dropdown-deployment") do |menu|
menu.trigger("Release actions", variant: :ghost)
menu.item("View deployment", href: "/gallery/deployments/2026-07-13")
menu.item("Copy release identifier")
menu.separator
menu.item("Roll back release", variant: :destructive)
end
end
endConstructor options, rendered root, closed vocabularies, and compound boundary for this component, exactly as shipped.
docs/component_contracts.md · NitroKit::Dropdown
id: nil, placement: :bottom_startdiv[data-nk=dropdown]; placements bottom_start bottom_end top_start top_end; item variants default destructive; item types button submit resettrigger, and at least one item. id: defaults to a generated nk-dropdown-* identifier and otherwise follows the shared fragment-safe identifier contract. trigger(text = nil, variant:, size:, icon:, icon_end:, label:, disabled:) forwards to Button, so an icon-only trigger keeps square icon-only geometry; variant: and size: are validated against Button vocabularies when the trigger is declared. item(text = nil, href:, icon:, variant:, type:, disabled:) renders an optional dropdown-item-icon slot and emits the owned data-variant on the item slot, matching the Toast::Item precedent; caller data-variant stays reserved. The native popover=auto owns open state and implicit invoker relationships. Optional visual titles are presentational to the menu accessibility tree; separators remain native. A small controller adds menu focus navigation, native outside-pointer dismissal fallback for capable Popover engines, and focus restoration without mirroring state. The fallback ignores composed-path interactions inside the trigger or menu and releases its listener on every close and disconnect.Instructions for coding agents, repeated on every component page so one fetched page has enough context. Humans can usually skip this section.
test/dummy/app/components/gallery/agent_rules.rb
render NitroKit::Button.new("Save", variant: :primary).ArgumentError; this component's accepted options are in the contract above.html: for HTML, aria: for ARIA, and data: for application data. class and style are forbidden, including inside html:.NitroKit::Component::RESERVED_DATA_ATTRIBUTES: data-nk, data-slot, data-variant, data-size, data-nk-escape, data-enhanced, data-state, data-disabled, data-required, data-orientation, data-presentation, data-placement, data-layout, data-side, data-field-type, data-dir, data-gap, data-align, data-justify, data-wrap, data-cols, data-mode, data-key. Do not pass them through data:. data-action, data-controller are additive and compose with Nitro behavior.desperately_need_a_class:. It requires a non-blank String and marks the exception with data-nk-escape="class".data-nk; owned parts emit component-qualified data-slot values such as field-control or card-title. Select on those attributes, never on classes.--nk-* custom properties in an application stylesheet. Variables beginning with --_nk-* are private component mechanics.NitroKit::Flex and NitroKit::Grid for layout. Parents own external placement and available width; components own their intrinsic geometry.data-state when styling or behavior also needs it.