Dropdown

Dropdown menu component.

template 2 required5 optional2 slots2 a11y rules

Preview

Usage

views.py
python
from djust import LiveView
from djust.decorators import event_handler


class MyView(LiveView):
    template_name = "my_template.html"

    def mount(self, request, **kwargs):
        self.id = 'gallery-dropdown'

    @event_handler()
    def toggle_dropdown(self, value=None, **kwargs):
        ...  # write to self.component; the re-render carries it
my_template.html
django
{% load theme_components %}
{% theme_dropdown id=id label='Actions' align='left' %}

Events this component sends to the view: toggle_dropdown.

Props

NameTypeRequiredDefault
idstrrequired
labelstrrequired
alignstrleft
css_prefixstr
attrsdict
slot_triggerstr
slot_menustr

Accessibility

RequirementElementAttributeValue
Trigger must have aria-haspopup=truebuttonaria-haspopuptrue
Trigger must have aria-expandedbuttonaria-expanded(present)

Slots

slot_trigger slot_menu
Source & styles
FilePathNotes
templatedjust_theming/components/dropdown.htmlCopy it to the same path in your project, or per theme under djust_theming/themes/<theme>/components/.
cssdjust_theming/components.cssDefines 6 of this component's classes at line 471, 476, 496, 504, 518, 526. Override those rules, or the custom properties they read, in a stylesheet loaded after it.
cssdjust_components/components.cssDefines 4 of this component's classes at line 87, 88, 90, 93. Override those rules, or the custom properties they read, in a stylesheet loaded after it.
CSS variables
--accent --accent-foreground --background --border --card --card-foreground --foreground --radius
Template source — dropdown.html
django
{% comment %}
Server-driven dropdown. The trigger dispatches `toggle_dropdown`; openness is
the host LiveView's `is_open`, and every open/closed artefact is rendered from
it — the menu's `display`, `aria-expanded`, and the `data-open` attribute the
chevron rotation is keyed on (`theming/css/components.css`), which
`51-keyboard-nav.js` also reads to decide whether the menu is open.

`data-theme-dropdown` below is the client fallback for plain pages, and
`components.js` stands down on any page with a djust mount root so a LiveView
page uses only the server path. Both hooks coexist deliberately: the theming
gallery cannot be a LiveView (its 25 `theme_*` tags are Django-engine only), so
dropping the fallback would leave it with a dead dropdown.

`data-open="true"` rather than a bare attribute: `theming/css/components.css`
matches on presence and would accept either, but the app-level
`djust_components/components.css` matches `[data-open="true"]` and
`51-keyboard-nav.js` handles both — emitting the valued form satisfies all three.

Known gap, unchanged here: the `slot_trigger` branch renders the caller's markup
verbatim, so it carries no `dj-click` and therefore no server event — and no
`aria-haspopup` / `aria-expanded`, which `DROPDOWN_CONTRACT` requires. A
slot-provided trigger is inert. That predates this change; fixing it means
deciding whether the contract should be enforced on caller-supplied markup.
{% endcomment %}
<div class="{{ css_prefix }}dropdown {% if attrs.class %}{{ attrs.class }}{% endif %}"
     data-theme-dropdown="{{ id }}"
     {% if is_open %}data-open="true"{% endif %}
     {% if attrs.id %}id="{{ attrs.id }}"{% endif %}>
    {% if slot_trigger %}
    {{ slot_trigger|safe }}
    {% else %}
    <button class="{{ css_prefix }}dropdown-trigger"
            aria-haspopup="true"
            aria-expanded="{% if is_open %}true{% else %}false{% endif %}"
            aria-controls="{{ id }}-menu"
            dj-click="toggle_dropdown"
            data-value="{{ id }}"
            {% if component_id %}data-component-id="{{ component_id }}"{% endif %}>
        {{ label }}
        <svg class="{{ css_prefix }}dropdown-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="m6 9 6 6 6-6"/>
        </svg>
    </button>
    {% endif %}
    <div class="{{ css_prefix }}dropdown-menu {{ css_prefix }}dropdown-{{ align }}"
         id="{{ id }}-menu"
         role="menu"
         {% if not is_open %}style="display:none;"{% endif %}>
        {% if slot_menu %}{{ slot_menu|safe }}{% endif %}
    </div>
</div>