View as Markdown

Native Select

A styled native HTML select element with consistent design system integration.

100%
Loading...
<twig:NativeSelect>
    <twig:NativeSelect:Option value="">Select status</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="todo">Todo</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="in-progress">In Progress</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="done">Done</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="cancelled">Cancelled</twig:NativeSelect:Option>
</twig:NativeSelect>

Installation

Note

Available since UX Toolkit 3.5.

php bin/console ux:install native-select --kit shadcn

Install the following Composer dependencies:

composer require twig/html-extra:^3.24.0 symfony/ux-icons symfony/ux-twig-component:^3.5 tales-from-a-dev/twig-tailwind-extra:^1.3.0

Install the following front-end dependencies:

npm install shadcn tw-animate-css
# ...or with Symfony AssetMapper
php bin/console importmap:require tw-animate-css/dist/tw-animate.css shadcn/dist/tailwind.css

Copy the following file(s) into your app:

templates/components/NativeSelect.html.twig
{%- props
    ## 'default'|'sm' The select size.
    size = 'default'
-%}
<div
    data-slot="native-select-wrapper"
    data-size="{{ size }}"
    class="{{ ('group/native-select relative w-fit has-[select:disabled]:opacity-50 ' ~ attributes.render('class'))|tailwind_merge }}"
>
    <select
        data-slot="native-select"
        data-size="{{ size }}"
        class="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 ps-2.5 pe-8 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:[color-scheme:dark]"
        {{ attributes.without('class') }}
    >
        {##- The select options, typically `NativeSelect:Option` and `NativeSelect:OptGroup` components. -#}
        {%- block content %}{% endblock -%}
    </select>
    <twig:ux:icon name="lucide:chevron-down" class="pointer-events-none absolute top-1/2 end-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none" aria-hidden="true" data-slot="native-select-icon" />
</div>
templates/components/NativeSelect/OptGroup.html.twig
<optgroup data-slot="native-select-optgroup" {{ attributes.defaults({class: 'bg-[Canvas] text-[CanvasText]'|tailwind_classes}) }}>
    {##- The `NativeSelect:Option` components in this group. -#}
    {%- block content %}{% endblock -%}
</optgroup>
templates/components/NativeSelect/Option.html.twig
<option data-slot="native-select-option" {{ attributes.defaults({class: 'bg-[Canvas] text-[CanvasText]'|tailwind_classes}) }}>
    {##- The option label. -#}
    {%- block content %}{% endblock -%}
</option>

Usage

<twig:NativeSelect>
    <twig:NativeSelect:Option value="">Select a fruit</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="apple">Apple</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="banana">Banana</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="blueberry">Blueberry</twig:NativeSelect:Option>
</twig:NativeSelect>

Examples

Groups

Use NativeSelect:OptGroup to organize options into labeled groups.

100%
Loading...
<twig:NativeSelect>
    <twig:NativeSelect:Option value="">Select department</twig:NativeSelect:Option>
    <twig:NativeSelect:OptGroup label="Engineering">
        <twig:NativeSelect:Option value="frontend">Frontend</twig:NativeSelect:Option>
        <twig:NativeSelect:Option value="backend">Backend</twig:NativeSelect:Option>
        <twig:NativeSelect:Option value="devops">DevOps</twig:NativeSelect:Option>
    </twig:NativeSelect:OptGroup>
    <twig:NativeSelect:OptGroup label="Sales">
        <twig:NativeSelect:Option value="sales-rep">Sales Rep</twig:NativeSelect:Option>
        <twig:NativeSelect:Option value="account-manager">Account Manager</twig:NativeSelect:Option>
        <twig:NativeSelect:Option value="sales-director">Sales Director</twig:NativeSelect:Option>
    </twig:NativeSelect:OptGroup>
    <twig:NativeSelect:OptGroup label="Operations">
        <twig:NativeSelect:Option value="support">Customer Support</twig:NativeSelect:Option>
        <twig:NativeSelect:Option value="product-manager">Product Manager</twig:NativeSelect:Option>
        <twig:NativeSelect:Option value="ops-manager">Operations Manager</twig:NativeSelect:Option>
    </twig:NativeSelect:OptGroup>
</twig:NativeSelect>

Disabled

100%
Loading...
<twig:NativeSelect disabled>
    <twig:NativeSelect:Option value="">Disabled</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="apple">Apple</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="banana">Banana</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="blueberry">Blueberry</twig:NativeSelect:Option>
</twig:NativeSelect>

Invalid

Set aria-invalid="true" to display the error state.

100%
Loading...
<twig:NativeSelect aria-invalid="true">
    <twig:NativeSelect:Option value="">Error state</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="apple">Apple</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="banana">Banana</twig:NativeSelect:Option>
    <twig:NativeSelect:Option value="blueberry">Blueberry</twig:NativeSelect:Option>
</twig:NativeSelect>

RTL

To enable RTL support, set the dir="rtl" attribute on the root element.

100%
Loading...
<div class="flex flex-col items-center gap-4">
    {# Arabic #}
    <div dir="rtl">
        <twig:NativeSelect>
            <twig:NativeSelect:Option value="">اختر الحالة</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="todo">مهام</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="in-progress">قيد التنفيذ</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="done">منجز</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="cancelled">ملغي</twig:NativeSelect:Option>
        </twig:NativeSelect>
    </div>

    {# Hebrew #}
    <div dir="rtl">
        <twig:NativeSelect>
            <twig:NativeSelect:Option value="">בחר סטטוס</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="todo">לעשות</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="in-progress">בתהליך</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="done">הושלם</twig:NativeSelect:Option>
            <twig:NativeSelect:Option value="cancelled">בוטל</twig:NativeSelect:Option>
        </twig:NativeSelect>
    </div>
</div>

Accessibility

  • NativeSelect renders a real <select>, so it keeps the platform's own picker, keyboard handling and type-ahead. This is the most accessible way to present a list of options.
  • Give it a visible Label bound with for and id. The first option is not a label, and a placeholder option should be disabled so it cannot be chosen.
  • The chevron is drawn with <twig:ux:icon>, which renders aria-hidden="true", and it is not focusable, so it never sits between the user and the control.
  • NativeSelect:OptGroup renders a real <optgroup>, whose label is announced when moving into the group.
  • Set aria-invalid="true" for the invalid styling, and point aria-describedby at the message explaining what is wrong. Prefer the native required attribute over a client-side check alone.

API Reference

<twig:NativeSelect>

Prop Type Default
size 
'default'|'sm' 'default'
Block Description
content The select options, typically NativeSelect:Option and NativeSelect:OptGroup components.

<twig:NativeSelect:OptGroup>

Block Description
content The NativeSelect:Option components in this group.

<twig:NativeSelect:Option>

Block Description
content The option label.