📖 Overview

SelectiveUI is a powerful JavaScript library for creating custom select boxes with many advanced features:

✨ Main features
  • Pure JavaScript, no dependencies
  • Multiple selection support
  • AJAX data loading with pagination
  • Search and filter
  • Keyboard navigation
  • Image mode
  • Customizable styling
  • Responsive design
🚀 Performance
  • RecyclerView pattern
  • Virtual scrolling (Possible support)
  • Auto Debouncing
  • Efficient DOM operations
  • Memory leak prevention
  • Size: ~60KB minified
🎯 Browser Support
  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+
  • Mobile browsers

📥 Installation

1. CDN

// Add to your HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/selective-ui@1.0.4/dist/selective-ui.min.css">
<script src="https://cdn.jsdelivr.net/npm/selective-ui@1.0.4/dist/selective-ui.min.js"></script>

2. NPM

npm install selective-ui

3. Download

Download files directly selective-ui.min.js and selective-ui.min.css

💡 Note: Ensure that CSS is loaded before JavaScript to avoid FOUC (Flash of Unstyled Content)

🎓 Basic usage

HTML

<select id="mySelect" name="myMultiSelect" data-width="200px">
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
    <option value="3">Option 3</option>
</select>

<select id="myGroupSelect" name="myGroupSelect" data-width="200px">
    <optgroup label="Fruits">
        <option value="apple">Apple 🍎</option>
        <option value="banana">Banana 🍌</option>
        <option value="orange">Orange 🍊</option>
    </optgroup>
    <optgroup label="Vegetables" data-collapsed="true">
        <option value="carrot">Carrot 🥕</option>
        <option value="lettuce">Lettuce 🥬</option>
        <option value="tomato">Tomato 🍅</option>
    </optgroup>
</select>

<select id="myMultiSelect" name="myMultiSelect[]" data-width="200px" multiple>
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
    <option value="3">Option 3</option>
</select>

<select id="myMultiGroupSelect" name="myMultiGroupSelect[]" data-width="200px" multiple>
    <optgroup label="Fruits">
        <option value="apple">Apple 🍎</option>
        <option value="banana">Banana 🍌</option>
        <option value="orange">Orange 🍊</option>
    </optgroup>
    <optgroup label="Vegetables" data-collapsed="true">
        <option value="carrot">Carrot 🥕</option>
        <option value="lettuce">Lettuce 🥬</option>
        <option value="tomato">Tomato 🍅</option>
    </optgroup>
</select>

JavaScript - Basic

// Basic initialization
// SelectiveUI.bind('#mySelect');
// SelectiveUI.bind('#myMultiSelect');

// With options
SelectiveUI.bind('#mySelect, #myGroupSelect', {
    placeholder: 'Choose an option',
    searchable: true
});
SelectiveUI.bind('#myMultiSelect, #myMultiGroupSelect', {
    placeholder: 'Choose an option',
    searchable: true
});









Interaction with APIs

// Get instance
const selectInstance = SelectiveUI.find('#mySelect');

// Get/Set value
const value = selectInstance.value;
selectInstance.value = '2';

// Get selected text
const text = selectInstance.valueText;

// Open/Close
selectInstance.open();
selectInstance.close();
selectInstance.toggle();

Smart Tag

<select id="mySelectSmartTag" data-allow-html="true" data-autoclose="true" data-width="200px">
    <option value="1"><`b style="color: red;"`>Option 1<`/b`></option>
    <option value="2"><`b style="color: green;"`>Option 2<`/b`></option>
    <option value="3"><`b style="color: blue;"`>Option 3<`/b`></option>
</select>

<script>
SelectiveUI.bind('#mySelectSmartTag');
</script>



⚙️ Configuration Options

Display Options

Option Type Default Description
placeholder String "Select value" Text displayed when no selection is made (applies to multiple selections)
width String "0px" Width (0px = auto)
height String "30px" Height
minWidth String "50px" Minimum width
panelHeight String "220px" Popup Height
accessoryStyle String "top" Function frame position: "top" | "bottom"

Behavior Options

Option Type Default Description
multiple Boolean false Allows multiple selection
searchable Boolean true Turn on the search feature
autoclose Boolean false Automatically closes after selection
selectall Boolean true Show the select all (multiple mode) button
disabled Boolean false Disable
readonly Boolean false Read-only mode (popups can be opened but data cannot be changed)
visible Boolean true Decide whether to hide/show all cards
loadingfield Boolean true Display notifications when interacting with the API
customDelimiter String "," Separators for certain actions
allowHtml Boolean false Allow HTML as (Smart Tag) or data-mask is in the options tab
maxSelected Number 0 Maximum selection limit (applies only to multiple, "0" means no limit)

Image Mode Options

Option Type Default Description
imageMode Boolean true | false (automatic) Turn on image mode
imageWidth String "60px" Image width
imageHeight String "60px" Image height
imageBorderRadius String "4px" Image border radius
imagePosition String "right" Image location: "left" | "right" | "top" | "bottom"

Text & Animation Options

Option Type Default Description
textLoading String "Processing..." Text displayed during loading
textNoData String "No data available" Text when no data is available
textNotFound String "Not found" Text when searching yields no results
textSelectAll String "Select all" Text the select all button
textDeselectAll String "Deselect all" Text deselect button
textAccessoryDeselect String "Deselect: " Text annotation for the deselect button in the function frame
animationtime Number 200 Animation duration (ms)
delaysearchtime Number 200 Delay when searching (ms)

Dataset Properties

Setup instructions

All options:

SelectiveUI.bind('#mySelect', {
    searchable: true,
    customDelimiter: ",",
    imagePosition: "left",
    ...
});

They can all be used as follows:

<select id="mySelect" data-searchable="true" data-custom-delimiter="," data-image-position="left"></select>

🔌 API Methods

Initialization

SelectiveUI.bind(selector, options)

Initialize SelectiveUI for element(s)

SelectiveUI.bind('.my-select', {
    searchable: true
});

SelectiveUI.find(selector)

Get the initialized instance of SelectiveUI

const instance = SelectiveUI.find('#mySelect');

Instance Methods

Method Return Description
open() self Open popup. Return instance
close() self Close popup. Return instance
toggle() self Toggle popup open/close. Return instance
refreshMask() self Update the displayed text (system-determined). Return instance
setValue(value, trigger) self Set a value. The value can be a string or an array;
trigger to decide whether or not to trigger the change event. | Boolean default: true. Return instance
selectAll() self Select all values ​​(only applies to multiple). Return instance
deSelectAll() self Deselect all values ​​(only applies to multiple). Return instance
ajax(Object) self Assign configuration ajax. Return instance
on(String, Object) self Assign configuration events. Return instance

Properties

Property Type Description
value String | Array Get/Set the selected value
valueArray Array Get the array of selected values
valueString String Get the string of values ​​(separated by customDelimiter)
valueText String | Array Get the text of the selected option
valueOptions Array Get an array of selected option objects
mask Array Get array text in display
placeholder String Get/Set placeholder text
oldValue String | Array Get the value before the change

Usage Examples

const select = SelectiveUI.find('#mySelect');

// Get value
console.log(select.value); // "1" or ["1", "2"]

// Set value
select.value = '2'; // Single
select.value = ['1', '2', '3']; // Multiple

// Get text
console.log(select.valueText); // "Option 2"

// Control popup
select.open();
select.close();
select.toggle();

🎪 Events

Available Events

Event Parameters Description
load (callback, instance) Trigger after initialization
beforeShow (callback, instance) Before the popup opens, It can be canceled
show (callback, instance) After opening the popup
beforeChange (callback, instance, value) Before changing the value. It can be canceled
change (callback, instance, value) After the value changed
beforeClose (callback, instance) Before closing the popup, It can be canceled
close (callback, instance) After closing the popup

Event Callback Object

Each event callback receives an object with methods:

function eventCallback(callback, instance, ...args) {
    // Stop propagation to the next event handler.
    callback.stopPropagation();
    
    // Cancel event (only works with beforeShow, beforeChange, beforeClose)
    callback.cancel();
}

Usage Examples

// Method 1: In options
SelectiveUI.bind('#mySelect', {
    on: {
        load: (callback, instance) => {
            console.log('SelectiveUI loaded!');
        },
        change: (callback, instance, value) => {
            console.log('Value changed to:', value);
        },
        beforeChange: (callback, instance, value) => {
            if (value === '3') {
                callback.cancel(); // Prevent change
            }
        }
    }
});

// Method 2: Using .on()
const instance = SelectiveUI.find('#mySelect');
instance.on('change', (callback, instance, value) => {
    console.log('Changed!', value);
});

🌐 AJAX Configuration

Basic AJAX Setup

SelectiveUI.bind('#mySelect', {
    ajax: {
        url: '/api/search',
        method: 'POST', // 'GET' or 'POST'
        keepSelected: true // Keep selected items after loading
    }
});

AJAX Options

Option Type Required Description
url String Required API endpoint URL
method String Optional "GET" or "POST". Default: "GET"
keepSelected Boolean Optional Keep selected items. Default: false
data Object | Function Optional Additional data sent to the server for search requests
dataByValues Function Optional Custom function to prepare payload when loading specific values. If not provided, default payload is used.

Request Parameters

Search Request (Normal)

SelectiveUI automatically sends the following parameters for search:

Parameter Type Description
search String Keyword search
page Number Current page (pagination)
selectedValue String Selected values (comma separated)

Load by Values Request (setValue)

When using setValue() with values not yet loaded, SelectiveUI sends:

Parameter Type Description
load_by_values String Flag "1" to indicate this is a load-by-values request
values String Comma-separated list of values to load (e.g., "101,202,303")
+ data params Mixed Additional parameters from data config (if function)
💡 Auto-load Missing Values:

When you use setValue() with values that don't exist in the current options, SelectiveUI automatically loads them from the server in the background. This works synchronously - existing values are selected immediately, while missing values are loaded without blocking the UI.

Response Format

The server needs to return JSON in one of the following formats (same for both search and load-by-values requests):

Format 1: Simple Array (Options only)

[
    { "value": "1", "text": "Option 1" },
    { "value": "2", "text": "Option 2" }
]

Format 2: With Groups (Simple)

[
    {
        "label": "Fruits",
        "options": [
            { "value": "apple", "text": "Apple 🍎" },
            { "value": "banana", "text": "Banana 🍌" }
        ]
    },
    {
        "label": "Vegetables",
        "options": [
            { "value": "carrot", "text": "Carrot 🥕" }
        ]
    },
    { "value": "other", "text": "Other (no group)" }
]

Format 3: With Groups (Explicit Type)

[
    {
        "type": "optgroup",
        "label": "Fruits",
        "data": {
            "collapsed": "false"
        },
        "options": [
            { 
                "value": "apple", 
                "text": "Apple 🍎",
                "data": {
                    "imgsrc": "/images/apple.jpg"
                }
            }
        ]
    },
    {
        "type": "option",
        "value": "other",
        "text": "Other item"
    }
]

Format 4: With Pagination

{
    "object": [
        { "value": "1", "text": "Option 1" },
        { "value": "2", "text": "Option 2" }
    ],
    "page": 0,
    "total_page": 5
}

Format 5: With Groups & Pagination

{
    "data": [
        {
            "label": "Fruits",
            "options": [
                { "value": "apple", "text": "Apple 🍎" }
            ]
        },
        { "value": "single", "text": "Single option" }
    ],
    "page": 0,
    "totalPages": 5,
    "hasMore": true
}

Format 6: With items key

{
    "items": [
        {
            "type": "optgroup",
            "label": "Electronics",
            "options": [
                { "value": "phone", "text": "Phone" }
            ]
        }
    ],
    "pagination": {
        "page": 0,
        "totalPages": 5,
        "hasMore": true
    }
}

Item Object Properties

Option Properties

Property Type Required Description
type String Optional "option" (automatically detect if not available label)
value String Required Value of options
text String Required Text is displayed
selected Boolean Optional Selected or not
data Object Optional Custom data (data-* attributes)
imgsrc String Optional Image URL (when imageMode = true)

OptGroup Properties

Property Type Required Description
type String Optional "optgroup" (Automatically detect if there is a label or options)
label String Required Group name
data Object Optional Custom data for optgroup (ví dụ: collapsed)
options Array Required Array of option objects

Pagination Properties

Property Type Optional Description
page Number Optional Current page number
totalPages | total_page Number Optional Total number of pages
hasMore Boolean Optional The next page will be counted automatically if there is none

Dynamic Data Function

SelectiveUI.bind('#mySelect', {
    ajax: {
        url: '/api/search',
        data: function(searchTerm, page) {
            return {
                search: searchTerm,
                page: page,
                category: 'electronics',
                limit: 20,
                userId: getCurrentUserId()
            };
        }
    }
});

Server-side Examples

PHP Example (With Groups)

// api/search.php
<?php
$search = $_POST['search'] ?? '';
$page = (int)($_POST['page'] ?? 0);
$limit = 20;
$offset = $page * $limit;

// Query with group
$query = "SELECT 
    c.name as category_name,
    p.id as value, 
    p.name as text,
    p.image_url as imgsrc
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.name LIKE ? 
ORDER BY c.name, p.name
LIMIT ? OFFSET ?";

$stmt = $pdo->prepare($query);
$stmt->execute(["%$search%", $limit, $offset]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Group results
$grouped = [];
$currentGroup = null;

foreach ($results as $row) {
    if ($row['category_name']) {
        if ($currentGroup === null || $currentGroup['label'] !== $row['category_name']) {
            if ($currentGroup !== null) {
                $grouped[] = $currentGroup;
            }
            $currentGroup = [
                'label' => $row['category_name'],
                'options' => []
            ];
        }
        $currentGroup['options'][] = [
            'value' => $row['value'],
            'text' => $row['text'],
            'data' => ['imgsrc' => $row['imgsrc']]
        ];
    } else {
        if ($currentGroup !== null) {
            $grouped[] = $currentGroup;
            $currentGroup = null;
        }
        $grouped[] = [
            'value' => $row['value'],
            'text' => $row['text']
        ];
    }
}
if ($currentGroup !== null) {
    $grouped[] = $currentGroup;
}

// Count total
$countQuery = "SELECT COUNT(*) FROM products WHERE name LIKE ?";
$total = $pdo->query($countQuery, ["%$search%"])->fetchColumn();
$totalPages = ceil($total / $limit);

// Response
header('Content-Type: application/json');
echo json_encode([
    'data' => $grouped,
    'page' => $page,
    'totalPages' => $totalPages,
    'hasMore' => $page < $totalPages - 1
]);
?>

Node.js Example (With Groups)

// api/search.js
app.post('/api/search', async (req, res) => {
    const { search = '', page = 0 } = req.body;
    const limit = 20;
    const offset = page * limit;

    // Query database
    const results = await db.query(`
        SELECT 
            c.name as category_name,
            p.id as value,
            p.name as text,
            p.image_url as imgsrc
        FROM products p
        LEFT JOIN categories c ON p.category_id = c.id
        WHERE p.name ILIKE $1
        ORDER BY c.name, p.name
        LIMIT $2 OFFSET $3
    `, [`%${search}%`, limit, offset]);

    // Group results
    const grouped = [];
    let currentGroup = null;

    results.rows.forEach(row => {
        if (row.category_name) {
            if (!currentGroup || currentGroup.label !== row.category_name) {
                if (currentGroup) grouped.push(currentGroup);
                currentGroup = {
                    label: row.category_name,
                    options: []
                };
            }
            currentGroup.options.push({
                value: row.value,
                text: row.text,
                data: { imgsrc: row.imgsrc }
            });
        } else {
            if (currentGroup) {
                grouped.push(currentGroup);
                currentGroup = null;
            }
            grouped.push({
                value: row.value,
                text: row.text
            });
        }
    });
    if (currentGroup) grouped.push(currentGroup);

    // Count total
    const total = await db.query(
        'SELECT COUNT(*) FROM products WHERE name ILIKE $1',
        [`%${search}%`]
    );
    const totalPages = Math.ceil(total.rows[0].count / limit);

    res.json({
        data: grouped,
        page: page,
        totalPages: totalPages,
        hasMore: page < totalPages - 1
    });
});

Complete AJAX Example with Groups

SelectiveUI.bind('#products', {
    ajax: {
        url: '/api/products/search',
        method: 'POST',
        keepSelected: true,
        data: function(search, page) {
            return {
                q: search,
                page: page,
                per_page: 20,
                include_categories: true
            };
        }
    },
    placeholder: 'Search products...',
    searchable: true,
    imageMode: true,
    imageWidth: '60px',
    imageHeight: '60px',
    multiple: true,
    on: {
        load: (callback, instance) => {
            console.log('Products loaded with groups');
        },
        change: (callback, instance, value) => {
            console.log('Selected products:', value);
        }
    }
});

Tips for AJAX with Groups

  1. Group Detection: The library automatically detects groups if an item has a label or options property
  2. Mixed Content: It is possible to mix groups and single options in the same response
  3. Group Data: Use data.collapsed = "true" to cause the default group to collapse
  4. Pagination: Pagination works the same way for both grouped and non-grouped data
  5. KeepSelected: Option keepSelected: true still works with items in groups
  6. Performance: Groups help organize data better, especially with large lists

Troubleshooting

Q: Groups not showing?

- Check if the response is in the correct format (label + options)
- Check the console log for parsing errors

Q: Does pagination not work with groups?

- Ensure that the page, totalPages are returned correctly
- Check infinite scroll trigger distance

Q: Are the selected items in the group missing?

- Use keepSelected: true in your ajax config
- The server needs to return the selectedValue in the request

💡 Examples

1. Basic Single Select

<select id="country">
    <option value="vn">Vietnam</option>
    <option value="us">United States</option>
    <option value="uk">United Kingdom</option>
</select>

<script>
SelectiveUI.bind('#country', {
    placeholder: 'Select country',
    searchable: true
});
</script>

2. Multiple Selection

<select id="human" data-placeholder="Human functions" multiple> 
    <option value="wa">Walk</option>
    <option value="sl">Sleep</option>
    <option value="ea">Eat</option>
    <option value="dr">Drink</option>
</select>

<script>
SelectiveUI.bind('#human');
</script>

3. With Images

<select id="products">
    <option value="1" data-imgsrc="img/product1.jpg">Product 1</option>
    <option value="2" data-imgsrc="img/product2.jpg">Product 2</option>
</select>

<script>
SelectiveUI.bind('#products', {
    imageMode: true,
    imageWidth: '80px',
    imageHeight: '80px',
    imagePosition: 'left',
    imageBorderRadius: '8px'
});
</script>

4. AJAX with Pagination

SelectiveUI.bind('#users', {
    ajax: {
        url: '/api/users',
        method: 'POST',
        keepSelected: true,
        data: function(search, page) {
            return {
                q: search,
                page: page,
                per_page: 20
            };
        }
    },
    placeholder: 'Search users...',
    searchable: true
});

5. Prevent Change with Validation

SelectiveUI.bind('#restricted', {
    on: {
        beforeChange: (callback, instance, newValue) => {
            if (!hasPermission(newValue)) {
                alert('You do not have permission!');
                callback.cancel();
            }
        }
    }
});

6. Custom Mask Display

<select id="users">
    <option value="1" data-mask="<b>John</b> Doe">John Doe</option>
    <option value="2" data-mask="<b>Jane</b> Smith">Jane Smith</option>
</select>

<script>
SelectiveUI.bind('#users', {
    allowHtml: true
});
</script>

7. Programmatic Control

const select = SelectiveUI.find('#mySelect');

// Set value
select.value = '3';

// Set multiple values
select.value = ['1', '2', '5'];

// Get selected text
console.log(select.valueText);

// Open programmatically
select.open();

// Close after 3 seconds
setTimeout(() => select.close(), 3000);

8. Destroy & Rebind

// Destroy instance
SelectiveUI.destroy('#mySelect');

// Rebind with new options
SelectiveUI.rebind('#mySelect', {
    multiple: true,
    placeholder: 'New placeholder'
});

🎨 Customization

CSS Variables

SelectiveUI uses CSS variables for easy customization:

:root {
    /* View/Input colors */
    --seui-view-text-color: #121212;
    --seui-view-background-color: #fff;
    --seui-view-border-color: #a3a3a3;
    --seui-view-border-radius: 5px;
    
    /* Popup colors */
    --seui-popup-background-color: #fbfbfb;
    --seui-popup-border-color: #a3a3a3;
    
    /* Checkbox colors */
    --seui-chkbox-background-color: #fff;
    --seui-chkbox-checked-background-color: #9dcff8;
    --seui-chkbox-hover-background-color: #dbdbdb;
}

Custom Theme Example

/* Dark Theme */
.dark-theme {
    --seui-view-text-color: #fff;
    --seui-view-background-color: #2d2d2d;
    --seui-view-border-color: #555;
    --seui-popup-background-color: #1e1e1e;
    --seui-popup-border-color: #555;
    --seui-chkbox-background-color: #3d3d3d;
    --seui-chkbox-checked-background-color: #0075ff;
}

Override CSS Classes

/* Custom option styling */
.selective-ui-option-view {
    padding: 15px;
    font-size: 16px;
    border-radius: 8px;
}

.selective-ui-option-view:hover {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
}

/* Custom search box */
.selective-ui-searchbox-input {
    font-size: 16px;
    padding: 10px;
}

Available CSS Classes

Class Description
.selective-ui-MAIN Main container
.selective-ui-view View panel (input area)
.selective-ui-popup Popup container
.selective-ui-option-view Individual option
.selective-ui-option-view.checked Selected option
.selective-ui-option-view.highlight Highlighted option (keyboard nav)
.selective-ui-searchbox Search container
.selective-ui-searchbox-input Search input
.selective-ui-empty-state Empty/no results state
.selective-ui-accessorybox Selected items display (multiple mode)

♿ Accessibility

📢 Important: SelectiveUI is designed with accessibility in mind. However, the current version may need improvements to meet WCAG 2.1 AA/AAA standards

Keyboard Navigation

Key Action
Tab Focus on the select option, or close the popup if it's currently open
Enter Open the popup or select the highlighted option
Arrow Down Navigate to the next option
Arrow Up Navigate to the previous option
Esc Close the popup

🔃 Versions

SPONSOR ME

Method Link
Paypal Donate me
Vietcombank 1047526877

V1.0.4 | 2026-01-07

Features
Add a loadByValues mechanism to the `setValue` and `value` instances if the value does not exist (for use with Ajax)
Fixes
The aria-* errors cause the Lighthouse score to drop
The map versions are not loading correctly in the current version

V1.0.3 | 2026-01-06

Features
Use English for default settings

V1.0.2 | 2026-01-06

Features
Update A11y
Updated nodeCloner to support: roles, ariaLive, ariaLabelledby, ariaControls, ariaHaspopup, ariaMultiselectable, ariaAutocomplete
Optimizations
The internal directory `view/` has been renamed to `views/` for easier understanding
Place the guard command file in the `utils/` directory
Fixes
Memory leak with observers when the popup is opened and closed repeatedly
Exception error in some use cases with find() (detected by unit tests)

V1.0.1 | 2026-01-02

Optimizations
Streamline and optimize the source code
Remove the internal event mounter
Use normalize for the function to remove the sign
Streamline the processing of Properties: disabled, readonly, visible
Remove unused code
Modernizing the IsIOS check function
Fixes
The selector disappears after setting the Properties: disabled, readonly, visible

V1.0.0 | 2025-12-31

Features
Broti compression support
Add .map scripts for debugging
Add minify script for esm
Clarify the source code through internal comments
Group support via AJAX
Fixes
Sometimes the search function doesn't work when you press enter
Inconsistent internal APIs make it difficult to maintain compatibility in the long term
An unexpected error occurred when an option was dynamically loaded with a bound Select tag

V1.0.0.beta1 | 2025-12-29

Features
Add the maxSelected configuration to limit the selectors
The visible configuration support determines whether to hide or show all tags
Restructure some attributes to match real-world conditions
(accessoryStyle, textLoading, textNoData, textNotFound, textSelectAll, textDeselectAll)
Add the attribute textAccessoryDeselect (comment for the deselect button in the function frame)
Larger font size for iOS devices
Supports A11y standard
Supports access via tabindex and enter key
Automate the input size system, eliminating pixel-fixing
Supports single-level groups with optgroup tags
Fixes
The movement animation did not end as expected
The placeholder was unexpectedly replaced
Surface values do not refresh when options are replaced
Memory leak involving objects that have not been thoroughly cleaned up
The popup displayed on iOS devices is skewed vertically when the keyboard appears
Unable to capture the cleanup event during search
Fix XSS security vulnerability

V1.0.0.beta0 | 2025-12-25

Features
Starter version