# OVERVIEW
---
# Getting Started
## Quickstart
Running tight on schedule? No worries! Check out our quickstart example to get started with Ark Ripple in seconds.
- [Next.js Template](https://stackblitz.com/edit/github-qcm2dskf)
- [Solid Start Template](https://stackblitz.com/edit/github-1hgkbbln)
- [Nuxt Template](https://stackblitz.com/edit/github-s3sg6syq)
## Setup Guide
( { autoReload: true, async load() { const response = await fetch( `https://dummyjson.com/quotes?limit=4&skip=${Math.floor(Math.random() * 50)}`, ); if (!response.ok) throw new Error('Failed to fetch quotes'); const data = await response.json(); return { items: data.quotes }; }, }, );} ``` ### Infinite Loading Implement pagination by returning a cursor that indicates more data is available. **Example: async-list/infinite-loading** ```ripple import { useAsyncList } from 'ark-ripple/collection'; import { Loader } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/async-list.module.css'; const LIMIT = 4; interface Post { userId: number; id: number; title: string; body: string; } export component InfiniteLoading() { const list = useAsyncListif (@list.error) {{'Error: '} {@list.error.message}}for (const quote of @list.items; key quote.id) {}{'"'} {quote.quote} {'"'}{'— '} {quote.author}( { autoReload: true, async load({ cursor }: { cursor?: number }) { const page = cursor || 1; const start = (page - 1) * LIMIT; const response = await fetch( `https://jsonplaceholder.typicode.com/posts?_start=${start}&_limit=${LIMIT}`, ); if (!response.ok) throw new Error('Failed to fetch posts'); const posts: Post[] = await response.json(); const hasNextPage = posts.length === LIMIT; return { items: posts, cursor: hasNextPage ? page + 1 : undefined, }; }, }, ); } ``` ### Filtering Filter data based on user input with automatic debouncing and loading states. **Example: async-list/filter** ```ripple import { useAsyncList } from 'ark-ripple/collection'; import { Loader } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/async-list.module.css'; const LIMIT = 4; interface User { id: number; name: string; email: string; department: string; role: string; } const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const mockUsers: User[] = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering', role: 'Senior Developer', }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Marketing', role: 'Marketing Manager', }, { id: 3, name: 'Carol Davis', email: 'carol@example.com', department: 'Engineering', role: 'Frontend Developer', }, { id: 4, name: 'David Wilson', email: 'david@example.com', department: 'Sales', role: 'Sales Representative', }, { id: 5, name: 'Eva Brown', email: 'eva@example.com', department: 'Engineering', role: 'DevOps Engineer', }, { id: 6, name: 'Frank Miller', email: 'frank@example.com', department: 'Support', role: 'Customer Success', }, { id: 7, name: 'Grace Lee', email: 'grace@example.com', department: 'Marketing', role: 'Content Creator', }, { id: 8, name: 'Henry Taylor', email: 'henry@example.com', department: 'Engineering', role: 'Backend Developer', }, { id: 9, name: 'Ivy Anderson', email: 'ivy@example.com', department: 'Sales', role: 'Account Manager', }, { id: 10, name: 'Jack Thompson', email: 'jack@example.com', department: 'Support', role: 'Technical Support', }, { id: 11, name: 'Kate Martinez', email: 'kate@example.com', department: 'Marketing', role: 'Brand Manager', }, { id: 12, name: 'Liam Garcia', email: 'liam@example.com', department: 'Engineering', role: 'Full Stack Developer', }, { id: 13, name: 'Mia Rodriguez', email: 'mia@example.com', department: 'Sales', role: 'Sales Director', }, { id: 14, name: 'Noah Lopez', email: 'noah@example.com', department: 'Support', role: 'Support Manager', }, { id: 15, name: 'Olivia White', email: 'olivia@example.com', department: 'Engineering', role: 'UI Designer', }, { id: 16, name: 'Paul Harris', email: 'paul@example.com', department: 'Marketing', role: 'Digital Marketer', }, { id: 17, name: 'Quinn Clark', email: 'quinn@example.com', department: 'Engineering', role: 'Mobile Developer', }, { id: 18, name: 'Ruby Lewis', email: 'ruby@example.com', department: 'Sales', role: 'Business Development', }, { id: 19, name: 'Sam Young', email: 'sam@example.com', department: 'Support', role: 'Documentation Specialist', }, { id: 20, name: 'Tina Walker', email: 'tina@example.com', department: 'Marketing', role: 'Social Media Manager', }, ]; export component Filter() { const list = useAsyncList{'Loaded '} {@list.items.length} {' posts'} {@list.cursor ? ' (more available)' : null} if (@list.cursor) { }if (@list.error) {{'Error: '} {@list.error.message}}for (const post of @list.items; key post.id) {}{@list.items.indexOf(post) + 1} {'. '} {post.title}{post.body}( { initialItems: mockUsers.slice(0, LIMIT), async load({ filterText }: { filterText?: string }) { await delay(500); if (!filterText) return { items: mockUsers.slice(0, LIMIT) }; const filtered = mockUsers.filter( (user) => user.name.toLowerCase().includes(filterText.toLowerCase()) || user.email.toLowerCase().includes(filterText.toLowerCase()), ); return { items: filtered.slice(0, LIMIT) }; }, }, ); } ``` ### Sorting (Client-side) Sort data on the client side after loading from the server. **Example: async-list/sort-client-side** ```ripple import { useAsyncList } from 'ark-ripple/collection'; import { ArrowDown, ArrowUpDown, ArrowUp, Loader } from 'lucide-ripple'; import styles from 'styles/async-list.module.css'; interface User { id: number; name: string; username: string; email: string; phone: string; website: string; } const collator = new Intl.Collator(); const cols: { key: keyof User; label: string }[] = [ { key: 'name', label: 'Name' }, { key: 'username', label: 'Username' }, { key: 'email', label: 'Email' }, ]; export component SortClientSide() { const list = useAsyncList{ @list.setFilterText((e.target as HTMLInputElement).value); }} /> if (@list.loading) {if (@list.error) {{'Searching'} } {'Error: '} {@list.error.message}}for (const user of @list.items; key user.id) {if (@list.items.length === 0 && !@list.loading) {}{user.name}{user.email}{user.department} {' • '} {user.role}{'No results found'}}( { autoReload: true, load: async () => { const response = await fetch('https://jsonplaceholder.typicode.com/users?_limit=5'); const data = await response.json(); return { items: data }; }, sort({ items, descriptor }) { return { items: items.sort((a, b) => { const { column, direction } = descriptor; let cmp = collator.compare( String(a[column as keyof User]), String(b[column as keyof User]), ); if (direction === 'descending') { cmp *= -1; } return cmp; }), }; }, }, ); const handleSort = (column: keyof User) => { const currentSort = @list.sortDescriptor; let direction: 'ascending' | 'descending' = 'ascending'; if (currentSort?.column === column && currentSort.direction === 'ascending') { direction = 'descending'; } @list.sort({ column, direction }); }; if (@list.loading) {} ``` ### Sorting (Server-side) Send sort parameters to the server and reload data when sorting changes. **Example: async-list/sort-server-side** ```ripple import { useAsyncList } from 'ark-ripple/collection'; import { ArrowDown, ArrowUpDown, ArrowUp, Loader } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/async-list.module.css'; interface Product { id: number; title: string; price: number; description: string; category: string; image: string; rating: { rate: number; count: number; }; } export component SortServerSide() { const list = useAsyncList} if (@list.error) {{'Loading'} {'Error: '} {@list.error.message}}{'Sorted by: '} {@list.sortDescriptor ? `${@list.sortDescriptor.column} (${@list.sortDescriptor.direction})` : 'none'}
for (const col of cols; key col.key) { for (const user of @list.items; key user.id) {handleSort(col.key)}> {col.label} {' '} if (!@list.sortDescriptor || @list.sortDescriptor.column !== col.key) { }} else if (@list.sortDescriptor.direction === 'ascending') { } else { } } {user.name} {user.username} {user.email} ( { autoReload: true, async load({ sortDescriptor }: { sortDescriptor?: { column: string; direction: string } }) { const url = new URL('https://fakestoreapi.com/products'); url.searchParams.set('limit', '5'); if (sortDescriptor) { const { direction } = sortDescriptor; url.searchParams.set('sort', direction === 'ascending' ? 'asc' : 'desc'); } const response = await fetch(url); const items = await response.json(); return { items }; }, }, ); const handleSort = (column: keyof Product) => { const currentSort = @list.sortDescriptor; let direction: 'ascending' | 'descending' = 'ascending'; if (currentSort?.column === column && currentSort.direction === 'ascending') { direction = 'descending'; } @list.sort({ column, direction }); }; } ``` ### Dependencies Automatically reload data when dependencies change, such as filter selections or external state. ```ripple import { useAsyncList } from 'ark-ripple/collection'; import { Loader } from 'lucide-ripple'; import { track } from 'ripple'; import field from 'styles/field.module.css'; import styles from 'styles/async-list.module.css'; const LIMIT = 5; interface User { id: number; name: string; email: string; department: string; role: string; } const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const departments = ['Engineering', 'Marketing', 'Sales', 'Support']; const roles = [ 'Senior Developer', 'Marketing Manager', 'Frontend Developer', 'Sales Representative', 'DevOps Engineer', 'Customer Success', 'Content Creator', 'Backend Developer', 'Account Manager', 'Technical Support', 'Brand Manager', 'Full Stack Developer', 'Sales Director', 'Support Manager', 'UI Designer', 'Digital Marketer', 'Mobile Developer', 'Business Development', 'Documentation Specialist', 'Social Media Manager', ]; const mockUsers: User[] = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering', role: 'Senior Developer', }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Marketing', role: 'Marketing Manager', }, { id: 3, name: 'Carol Davis', email: 'carol@example.com', department: 'Engineering', role: 'Frontend Developer', }, { id: 4, name: 'David Wilson', email: 'david@example.com', department: 'Sales', role: 'Sales Representative', }, { id: 5, name: 'Eva Brown', email: 'eva@example.com', department: 'Engineering', role: 'DevOps Engineer', }, { id: 6, name: 'Frank Miller', email: 'frank@example.com', department: 'Support', role: 'Customer Success', }, { id: 7, name: 'Grace Lee', email: 'grace@example.com', department: 'Marketing', role: 'Content Creator', }, { id: 8, name: 'Henry Taylor', email: 'henry@example.com', department: 'Engineering', role: 'Backend Developer', }, { id: 9, name: 'Ivy Anderson', email: 'ivy@example.com', department: 'Sales', role: 'Account Manager', }, { id: 10, name: 'Jack Thompson', email: 'jack@example.com', department: 'Support', role: 'Technical Support', }, { id: 11, name: 'Kate Martinez', email: 'kate@example.com', department: 'Marketing', role: 'Brand Manager', }, { id: 12, name: 'Liam Garcia', email: 'liam@example.com', department: 'Engineering', role: 'Full Stack Developer', }, { id: 13, name: 'Mia Rodriguez', email: 'mia@example.com', department: 'Sales', role: 'Sales Director', }, { id: 14, name: 'Noah Lopez', email: 'noah@example.com', department: 'Support', role: 'Support Manager', }, { id: 15, name: 'Olivia White', email: 'olivia@example.com', department: 'Engineering', role: 'UI Designer', }, { id: 16, name: 'Paul Harris', email: 'paul@example.com', department: 'Marketing', role: 'Digital Marketer', }, { id: 17, name: 'Quinn Clark', email: 'quinn@example.com', department: 'Engineering', role: 'Mobile Developer', }, { id: 18, name: 'Ruby Lewis', email: 'ruby@example.com', department: 'Sales', role: 'Business Development', }, { id: 19, name: 'Sam Young', email: 'sam@example.com', department: 'Support', role: 'Documentation Specialist', }, { id: 20, name: 'Tina Walker', email: 'tina@example.com', department: 'Marketing', role: 'Social Media Manager', }, ]; export component Dependencies() { let selectedDepartment = track(''); let selectedRole = track(''); const dependencies = track(() => [@selectedDepartment, @selectedRole]); const list = useAsyncListif (@list.loading) {if (@list.error) {{'Loading'} } {'Error: '} {@list.error.message}}for (const product of @list.items; key product.id) {}
{product.title}{' ### Dependencies Automatically reload data when dependencies change, such as filter selections or external state. **Example: async-list/dependencies** ```ripple import { useAsyncList } from 'ark-ripple/collection'; import { Loader } from 'lucide-ripple'; import { track } from 'ripple'; import field from 'styles/field.module.css'; import styles from 'styles/async-list.module.css'; const LIMIT = 5; interface User { id: number; name: string; email: string; department: string; role: string; } const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const departments = ['Engineering', 'Marketing', 'Sales', 'Support']; const roles = [ 'Senior Developer', 'Marketing Manager', 'Frontend Developer', 'Sales Representative', 'DevOps Engineer', 'Customer Success', 'Content Creator', 'Backend Developer', 'Account Manager', 'Technical Support', 'Brand Manager', 'Full Stack Developer', 'Sales Director', 'Support Manager', 'UI Designer', 'Digital Marketer', 'Mobile Developer', 'Business Development', 'Documentation Specialist', 'Social Media Manager', ]; const mockUsers: User[] = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering', role: 'Senior Developer', }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Marketing', role: 'Marketing Manager', }, { id: 3, name: 'Carol Davis', email: 'carol@example.com', department: 'Engineering', role: 'Frontend Developer', }, { id: 4, name: 'David Wilson', email: 'david@example.com', department: 'Sales', role: 'Sales Representative', }, { id: 5, name: 'Eva Brown', email: 'eva@example.com', department: 'Engineering', role: 'DevOps Engineer', }, { id: 6, name: 'Frank Miller', email: 'frank@example.com', department: 'Support', role: 'Customer Success', }, { id: 7, name: 'Grace Lee', email: 'grace@example.com', department: 'Marketing', role: 'Content Creator', }, { id: 8, name: 'Henry Taylor', email: 'henry@example.com', department: 'Engineering', role: 'Backend Developer', }, { id: 9, name: 'Ivy Anderson', email: 'ivy@example.com', department: 'Sales', role: 'Account Manager', }, { id: 10, name: 'Jack Thompson', email: 'jack@example.com', department: 'Support', role: 'Technical Support', }, { id: 11, name: 'Kate Martinez', email: 'kate@example.com', department: 'Marketing', role: 'Brand Manager', }, { id: 12, name: 'Liam Garcia', email: 'liam@example.com', department: 'Engineering', role: 'Full Stack Developer', }, { id: 13, name: 'Mia Rodriguez', email: 'mia@example.com', department: 'Sales', role: 'Sales Director', }, { id: 14, name: 'Noah Lopez', email: 'noah@example.com', department: 'Support', role: 'Support Manager', }, { id: 15, name: 'Olivia White', email: 'olivia@example.com', department: 'Engineering', role: 'UI Designer', }, { id: 16, name: 'Paul Harris', email: 'paul@example.com', department: 'Marketing', role: 'Digital Marketer', }, { id: 17, name: 'Quinn Clark', email: 'quinn@example.com', department: 'Engineering', role: 'Mobile Developer', }, { id: 18, name: 'Ruby Lewis', email: 'ruby@example.com', department: 'Sales', role: 'Business Development', }, { id: 19, name: 'Sam Young', email: 'sam@example.com', department: 'Support', role: 'Documentation Specialist', }, { id: 20, name: 'Tina Walker', email: 'tina@example.com', department: 'Marketing', role: 'Social Media Manager', }, ]; export component Dependencies() { let selectedDepartment = track(''); let selectedRole = track(''); const dependencies = track(() => [@selectedDepartment, @selectedRole]); const list = useAsyncList( { initialItems: mockUsers.slice(0, LIMIT), dependencies, async load({ filterText }: { filterText?: string }) { await delay(400); let items = mockUsers; if (@selectedDepartment) { items = items.filter((user) => user.department === @selectedDepartment); } if (@selectedRole) { items = items.filter((user) => user.role === @selectedRole); } if (filterText) { items = items.filter( (user) => user.name.toLowerCase().includes(filterText.toLowerCase()) || user.email.toLowerCase().includes(filterText.toLowerCase()), ); } return { items: items.slice(0, LIMIT) }; }, }, ); } ``` ## API Reference ### Props - **load** (`(params: LoadParams{ @list.setFilterText((e.target as HTMLInputElement).value); }} /> if (@list.loading) {if (@list.error) {{'Loading'} } {'Error: '} {@list.error.message}}{'Found '} {@list.items.length} {' users'}for (const user of @list.items; key user.id) {if (@list.items.length === 0 && !@list.loading) {}{user.name}{user.email}{user.department} {' • '} {user.role}{'No users found with current filters'}}) => Promise >`) - Function to load data asynchronously - **sort** (`(params: SortParams ) => Promise > | SortResult `) - Optional function for client-side sorting - **autoReload** (`boolean`, default: `false`) - Whether to automatically reload data on mount - **initialItems** (`T[]`, default: `[]`) - Initial items to display before first load - **dependencies** (`any[]`, default: `[]`) - Values that trigger a reload when changed - **initialFilterText** (`string`, default: `''`) - Initial filter text value - **initialSortDescriptor** (`SortDescriptor | null`) - Initial sort configuration ### Load Parameters The `load` function receives an object with the following properties: - **cursor** (`C | undefined`) - Current cursor for pagination - **filterText** (`string`) - Current filter text - **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration - **signal** (`AbortSignal`) - AbortController signal for request cancellation ### Load Result The `load` function should return an object with: - **items** (`T[]`) - The loaded items - **cursor** (`C | undefined`) - Optional cursor for next page ### Sort Parameters The `sort` function receives an object with: - **items** (`T[]`) - Current items to sort - **descriptor** (`SortDescriptor`) - Sort configuration with `column` and `direction` ### Return Value The hook returns an object with the following properties and methods: #### State Properties - **items** (`T[]`) - Current list of items - **loading** (`boolean`) - Whether a load operation is in progress - **error** (`Error | null`) - Any error from the last operation - **cursor** (`C | undefined`) - Current cursor for pagination - **filterText** (`string`) - Current filter text - **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration #### Methods - **reload** (`() => void`) - Reload data from the beginning - **loadMore** (`() => void`) - Load more items (when cursor is available) - **setFilterText** (`(text: string) => void`) - Set filter text and reload - **sort** (`(descriptor: SortDescriptor) => void`) - Apply sorting #### Types ```tsx interface SortDescriptor { column: string direction: 'ascending' | 'descending' } interface LoadParams { cursor?: C filterText: string sortDescriptor?: SortDescriptor | null signal: AbortSignal } interface LoadResult { items: T[] cursor?: C } ```} {product.price} {product.category} {' • '} {product.rating.rate} {' ('} {product.rating.count} {' reviews)'}( { initialItems: mockUsers.slice(0, LIMIT), dependencies, async load({ filterText }: { filterText?: string }) { await delay(400); let items = mockUsers; if (@selectedDepartment) { items = items.filter((user) => user.department === @selectedDepartment); } if (@selectedRole) { items = items.filter((user) => user.role === @selectedRole); } if (filterText) { items = items.filter( (user) => user.name.toLowerCase().includes(filterText.toLowerCase()) || user.email.toLowerCase().includes(filterText.toLowerCase()), ); } return { items: items.slice(0, LIMIT) }; }, }, ); } ``` ## API Reference ### Props - **load** (`(params: LoadParams{ @list.setFilterText((e.target as HTMLInputElement).value); }} /> if (@list.loading) {if (@list.error) {{'Loading'} } {'Error: '} {@list.error.message}}{'Found '} {@list.items.length} {' users'}for (const user of @list.items; key user.id) {if (@list.items.length === 0 && !@list.loading) {}{user.name}{user.email}{user.department} {' • '} {user.role}{'No users found with current filters'}}) => Promise >`) - Function to load data asynchronously - **sort** (`(params: SortParams ) => Promise > | SortResult `) - Optional function for client-side sorting - **autoReload** (`boolean`, default: `false`) - Whether to automatically reload data on mount - **initialItems** (`T[]`, default: `[]`) - Initial items to display before first load - **dependencies** (`any[]`, default: `[]`) - Values that trigger a reload when changed - **initialFilterText** (`string`, default: `''`) - Initial filter text value - **initialSortDescriptor** (`SortDescriptor | null`) - Initial sort configuration ### Load Parameters The `load` function receives an object with the following properties: - **cursor** (`C | undefined`) - Current cursor for pagination - **filterText** (`string`) - Current filter text - **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration - **signal** (`AbortSignal`) - AbortController signal for request cancellation ### Load Result The `load` function should return an object with: - **items** (`T[]`) - The loaded items - **cursor** (`C | undefined`) - Optional cursor for next page ### Sort Parameters The `sort` function receives an object with: - **items** (`T[]`) - Current items to sort - **descriptor** (`SortDescriptor`) - Sort configuration with `column` and `direction` ### Return Value The hook returns an object with the following properties and methods: #### State Properties - **items** (`T[]`) - Current list of items - **loading** (`boolean`) - Whether a load operation is in progress - **error** (`Error | null`) - Any error from the last operation - **cursor** (`C | undefined`) - Current cursor for pagination - **filterText** (`string`) - Current filter text - **sortDescriptor** (`SortDescriptor | null`) - Current sort configuration #### Methods - **reload** (`() => void`) - Reload data from the beginning - **loadMore** (`() => void`) - Load more items (when cursor is available) - **setFilterText** (`(text: string) => void`) - Set filter text and reload - **sort** (`(descriptor: SortDescriptor) => void`) - Apply sorting #### Types ```tsx interface SortDescriptor { column: string direction: 'ascending' | 'descending' } interface LoadParams { cursor?: C filterText: string sortDescriptor?: SortDescriptor | null signal: AbortSignal } interface LoadResult { items: T[] cursor?: C } ``` # List Selection The `useListSelection` hook manages selection state in lists and collections. It supports single and multiple selection modes with operations like select, deselect, toggle, and clear. ```tsx import { createListCollection, useListSelection } from 'ark-ripple/collection' const collection = createListCollection({ items: [ { label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }, { label: 'Cherry', value: 'cherry' }, ], }) const selection = useListSelection({ collection, selectionMode: 'single', deselectable: true, }) console.log(selection.selectedValues) // ['apple', 'banana', 'cherry'] ``` ## Examples ### Basic By default, the hook supports single selection mode that can be deselected. > Set `deselectable` to `false` to prevent deselecting the current selection. **Example: list-selection/basic** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { createListCollection, useListSelection } from 'ark-ripple/collection'; import { Check } from 'lucide-ripple'; import styles from 'styles/list-selection.module.css'; import checkboxStyles from 'styles/checkbox.module.css'; const collection = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, ], }, ); export component Basic() { const selection = useListSelection({ collection }); for (const item of collection.items; key item.value) {} ``` ### Multiple Selection Set `selectionMode` to `multiple` to allow multiple items to be selected. **Example: list-selection/multiple** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { createListCollection, useListSelection } from 'ark-ripple/collection'; import { Check } from 'lucide-ripple'; import styles from 'styles/list-selection.module.css'; import checkboxStyles from 'styles/checkbox.module.css'; const collection = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, { label: 'Solid', value: 'solid' }, ], }, ); export component Multiple() { const selection = useListSelection({ collection, selectionMode: 'multiple' }); const handleSelectAll = () => { if (@selection.isAllSelected()) { @selection.clear(); } else { @selection.setSelectedValues(collection.getValues()); } };@selection.select(item.value)} > }{item.label} } ``` ### Range Selection Here's an example of how to implement range selection that extends the selection from the first selected item to the clicked item. **Example: list-selection/range** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { createListCollection, useListSelection } from 'ark-ripple/collection'; import { Check } from 'lucide-ripple'; import styles from 'styles/list-selection.module.css'; import checkboxStyles from 'styles/checkbox.module.css'; const collection = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, { label: 'Solid', value: 'solid' }, ], }, ); export component Range() { const selection = useListSelection({ collection, selectionMode: 'multiple' }); const handleItemClick = (value: string, event: MouseEvent) => { event.preventDefault(); const firstSelected = @selection.firstSelectedValue; if (event.shiftKey && firstSelected) { @selection.extend(firstSelected, value); } else if (event.ctrlKey || event.metaKey) { @selection.toggle(value); } else { @selection.replace(value); } };{@selection.selectedValues.length} {' of '} {collection.items.length} {' selected'}for (const item of collection.items; key item.value) {@selection.select(item.value)} > }{item.label} for (const item of collection.items; key item.value) {} ``` ## API Reference ### Props - **collection** (`ListCollectionhandleItemClick(item.value, e)} > }{item.label} {'Click to select • Shift+Click for range • Cmd/Ctrl+Click to toggle'}
`) - The collection to manage selection for - **selectionMode** (`'single' | 'multiple' | 'none'`, default: `'single'`) - The selection mode - **deselectable** (`boolean`, default: `true`) - Whether selected items can be deselected - **initialSelectedValues** (`string[]`, default: `[]`) - Initial selected values - **resetOnCollectionChange** (`boolean`, default: `false`) - Whether to reset selection when collection changes ### Return Value The hook returns an object with the following properties and methods: #### State Properties - **selectedValues** (`string[]`) - Array of currently selected values - **isEmpty** (`boolean`) - Whether no items are selected - **firstSelectedValue** (`string | null`) - The first selected value in collection order - **lastSelectedValue** (`string | null`) - The last selected value in collection order #### Query Methods - **isSelected** (`(value: string | null) => boolean`) - Check if a value is selected - **canSelect** (`(value: string) => boolean`) - Check if a value can be selected - **isAllSelected** (`() => boolean`) - Check if all items are selected - **isSomeSelected** (`() => boolean`) - Check if some items are selected #### Selection Methods - **select** (`(value: string, forceToggle?: boolean) => void`) - Select a value - **deselect** (`(value: string) => void`) - Deselect a value - **toggle** (`(value: string) => void`) - Toggle selection of a value - **replace** (`(value: string | null) => void`) - Replace selection with a single value - **extend** (`(anchorValue: string, targetValue: string) => void`) - Extend selection from anchor to target - **setSelectedValues** (`(values: string[]) => void`) - Set the selected values - **setSelection** (`(values: string[]) => void`) - Set the selection (alias for setSelectedValues) - **clear** (`() => void`) - Clear all selections - **resetSelection** (`() => void`) - Reset selection to initial state # COMPONENTS --- # Accordion ## Anatomy ```tsx ``` ## Examples ### Default Value Set the `defaultValue` prop to specify which item should be expanded by default. **Example: basic** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component Basic() { for (const item of items; key item.value) { } const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Controlled Use the `value` and `onValueChange` props to control the expanded items. **Example: controlled** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { track } from 'ripple'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component Controlled() { let value = track} {item.title} {item.content}([]); { @value = details.value; }} > for (const item of items; key item.value) { } const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Root Provider An alternative way to control the accordion is to use the `RootProvider` component and the `useAccordion` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { useAccordion } from 'ark-ripple/accordion'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component RootProvider() { const accordion = useAccordion({ multiple: true, defaultValue: ['ark-ui'] });} {item.title} {item.content}} const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Collapsible Use the `collapsible` prop to allow the user to collapse all panels. **Example: collapsible** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component Collapsible() {for (const item of items; key item.value) { } {item.title} {item.content}for (const item of items; key item.value) { } const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Multiple Use the `multiple` prop to allow multiple panels to be expanded simultaneously. **Example: multiple** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component Multiple() {} {item.title} {item.content}for (const item of items; key item.value) { } const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Horizontal By default, the Accordion is oriented vertically. Use the `orientation` prop to switch to a horizontal layout. **Example: horizontal** ```ripple import { Accordion } from 'ark-ripple/accordion'; import styles from 'styles/accordion.module.css'; export component Horizontal() {} {item.title} {item.content}for (const item of items; key item.value) { } const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Lazy Mount Use the `lazyMount` prop to defer rendering of accordion content until the item is expanded. Combine with `unmountOnExit` to unmount content when collapsed, freeing up resources. **Example: lazy-mount** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component LazyMount() {} {item.title} {item.content}for (const item of items; key item.value) { } const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Context Use `Accordion.Context` or `useAccordionContext` to access the accordion state. **Example: context** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component Context() {} {item.title} {item.content}} const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ### Item State Use `Accordion.ItemContext` or `useAccordionItemContext` to access the state of an accordion item. **Example: item-context** ```ripple import { Accordion } from 'ark-ripple/accordion'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/accordion.module.css'; export component ItemContext() { component children({ context }) { } for (const item of items; key item.value) {} {item.title} {item.content}for (const item of items; key item.value) { } const items = [ { value: 'ark-ui', title: 'What is Ark UI?', content: 'A headless component library for building accessible web apps.', }, { value: 'getting-started', title: 'How to get started?', content: 'Install the package and import the components you need.', }, { value: 'maintainers', title: 'Who maintains this project?', content: 'Ark UI is maintained by the Chakra UI team.', }, ]; ``` ## Guides ### Content Animation Use the `--height` and/or `--width` CSS variables to animate the size of the content when it expands or closes: ```css @keyframes slideDown { from { opacity: 0.01; height: 0; } to { opacity: 1; height: var(--height); } } @keyframes slideUp { from { opacity: 1; height: var(--height); } to { opacity: 0.01; height: 0; } } [data-scope='accordion'][data-part='item-content'][data-state='open'] { animation: slideDown 250ms ease-in-out; } [data-scope='accordion'][data-part='item-content'][data-state='closed'] { animation: slideUp 200ms ease-in-out; } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `collapsible` | `boolean` | No | Whether an accordion item can be closed after it has been expanded. | | `defaultValue` | `string[]` | No | The initial value of the expanded accordion items. Use when you don't need to control the value of the accordion. | | `disabled` | `boolean` | No | Whether the accordion items are disabled | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string item: (value: string) => string itemContent: (value: string) => string itemTrigger: (value: string) => string }>` | No | The ids of the elements in the accordion. Useful for composition. | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `multiple` | `boolean` | No | Whether multiple accordion items can be expanded at the same time. | | `onFocusChange` | `(details: FocusChangeDetails) => void` | No | The callback fired when the focused accordion item changes. | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | The callback fired when the state of expanded/collapsed accordion items changes. | | `orientation` | `'horizontal' | 'vertical'` | No | The orientation of the accordion items. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | | `value` | `string[]` | No | The controlled value of the expanded accordion items. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | accordion | | `[data-part]` | root | | `[data-orientation]` | The orientation of the accordion | **ItemContent Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemContent Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | accordion | | `[data-part]` | item-content | | `[data-state]` | "open" | "closed" | | `[data-disabled]` | Present when disabled | | `[data-focus]` | Present when focused | | `[data-orientation]` | The orientation of the item | **ItemIndicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemIndicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | accordion | | `[data-part]` | item-indicator | | `[data-state]` | "open" | "closed" | | `[data-disabled]` | Present when disabled | | `[data-focus]` | Present when focused | | `[data-orientation]` | The orientation of the item | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `string` | Yes | The value of the accordion item. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `disabled` | `boolean` | No | Whether the accordion item is disabled. | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | accordion | | `[data-part]` | item | | `[data-state]` | "open" | "closed" | | `[data-focus]` | Present when focused | | `[data-disabled]` | Present when disabled | | `[data-orientation]` | The orientation of the item | **ItemTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | accordion | | `[data-part]` | item-trigger | | `[data-controls]` | | | `[data-orientation]` | The orientation of the item | | `[data-state]` | "open" | "closed" | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseAccordionReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `focusedValue` | `string` | The value of the focused accordion item. | | `value` | `string[]` | The value of the accordion | | `setValue` | `(value: string[]) => void` | Sets the value of the accordion | | `getItemState` | `(props: ItemProps) => ItemState` | Returns the state of an accordion item. | ## Accessibility This component complies with the [Accordion WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/). ### Keyboard Support **`Space`** Description: When focus is on an trigger of a collapsed item, the item is expanded **`Enter`** Description: When focus is on an trigger of a collapsed section, expands the section. **`Tab`** Description: Moves focus to the next focusable element **`Shift + Tab`** Description: Moves focus to the previous focusable element **`ArrowDown`** Description: Moves focus to the next trigger **`ArrowUp`** Description: Moves focus to the previous trigger. **`Home`** Description: When focus is on an trigger, moves focus to the first trigger. **`End`** Description: When focus is on an trigger, moves focus to the last trigger. # Angle Slider ## Anatomy ```tsx} {item.title} component children({ context }) { if (@context.expanded) { {'Expanded'} } if (@context.focused) { {'Focused'} }} {item.content}``` ## Examples ### Basic Here's a basic example of the Angle Slider component. **Example: basic** ```ripple import { AngleSlider } from 'ark-ripple/angle-slider'; import styles from 'styles/angle-slider.module.css'; const markers = [0, 45, 90, 135, 180, 225, 270, 315]; export component Basic() { } ``` ### Controlled Use the `value` and `onValueChange` props to control the value of the Angle Slider. **Example: controlled** ```ripple import { AngleSlider } from 'ark-ripple/angle-slider'; import { track } from 'ripple'; import styles from 'styles/angle-slider.module.css'; const markers = [0, 45, 90, 135, 180, 225, 270, 315]; export component Controlled() { let value = track(45); {'Rotation'} for (const value of markers; key value) { } { @value = e.value; }} > } ``` ### Steps Use the `step` prop to set the discrete steps of the Angle Slider. **Example: step** ```ripple import { AngleSlider } from 'ark-ripple/angle-slider'; import styles from 'styles/angle-slider.module.css'; const markers = [0, 45, 90, 135, 180, 225, 270, 315]; export component Step() {{'Rotation'} for (const marker of markers; key marker) { } } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `aria-label` | `string` | No | The accessible label for the slider thumb. | | `aria-labelledby` | `string` | No | The id of the element that labels the slider thumb. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `defaultValue` | `number` | No | The initial value of the slider. Use when you don't need to control the value of the slider. | | `disabled` | `boolean` | No | Whether the slider is disabled. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string thumb: string hiddenInput: string control: string valueText: string label: string }>` | No | The ids of the elements in the machine. Useful for composition. | | `invalid` | `boolean` | No | Whether the slider is invalid. | | `name` | `string` | No | The name of the slider. Useful for form submission. | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | The callback function for when the value changes. | | `onValueChangeEnd` | `(details: ValueChangeDetails) => void` | No | The callback function for when the value changes ends. | | `readOnly` | `boolean` | No | Whether the slider is read-only. | | `step` | `number` | No | The step value for the slider. | | `value` | `number` | No | The value of the slider. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | angle-slider | | `[data-part]` | root | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **Root CSS Variables:** | Variable | Description | |----------|-------------| | `--value` | The current value | | `--angle` | The angle in degrees | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | angle-slider | | `[data-part]` | control | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **HiddenInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | angle-slider | | `[data-part]` | label | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **MarkerGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Marker Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `number` | Yes | The value of the marker | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Marker Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | angle-slider | | `[data-part]` | marker | | `[data-value]` | The value of the item | | `[data-state]` | | | `[data-disabled]` | Present when disabled | **Marker CSS Variables:** | Variable | Description | |----------|-------------| | `--marker-value` | The logical marker value (e.g. 0, 45, 90) | | `--marker-display-value` | The rotation angle for display (mirrored in RTL) | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseAngleSliderReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Thumb Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Thumb Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | angle-slider | | `[data-part]` | thumb | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **ValueText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `value` | `number` | The current value of the angle slider | | `valueAsDegree` | `string` | The current value as a degree string | | `setValue` | `(value: number) => void` | Sets the value of the angle slider | | `dragging` | `boolean` | Whether the slider is being dragged. | # Avatar ## Anatomy ```tsx {'15 Step'} for (const value of markers; key value) { } ``` ## Examples ### Basic Display a user's profile image with a fallback. **Example: basic** ```ripple import { Avatar } from 'ark-ripple/avatar'; import styles from 'styles/avatar.module.css'; export component Basic() { } ``` ### Events Use `onStatusChange` to listen for loading state changes. **Example: events** ```ripple import { Avatar } from 'ark-ripple/avatar'; import { track } from 'ripple'; import styles from 'styles/avatar.module.css'; export component Events() { let status = track('loading...'); {'PA'} } ``` ### Root Provider An alternative way to control the avatar is to use the `RootProvider` component and the `useAvatar` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Avatar } from 'ark-ripple/avatar'; import { useAvatar } from 'ark-ripple/avatar'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/avatar.module.css'; export component RootProvider() { let count = track(0); const avatar = useAvatar();{ @status = e.status; }} > {'PA'} } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `ids` | `Partial<{ root: string; image: string; fallback: string }>` | No | The ids of the elements in the avatar. Useful for composition. | | `onStatusChange` | `(details: StatusChangeDetails) => void` | No | Functional called when the image loading status changes. | **Fallback Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Fallback Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | avatar | | `[data-part]` | fallback | | `[data-state]` | "hidden" | "visible" | **Image Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Image Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | avatar | | `[data-part]` | image | | `[data-state]` | "visible" | "hidden" | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseAvatarReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `loaded` | `boolean` | Whether the image is loaded. | | `setSrc` | `(src: string) => void` | Function to set new src. | | `setLoaded` | `VoidFunction` | Function to set loaded state. | | `setError` | `VoidFunction` | Function to set error state. | # Carousel ## Anatomy ```tsx{'PA'} ``` ## Examples **Example: basic** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const images = [ { src: 'https://picsum.photos/seed/1/500/300', alt: 'Nature landscape', index: 0 }, { src: 'https://picsum.photos/seed/2/500/300', alt: 'City skyline', index: 1 }, { src: 'https://picsum.photos/seed/3/500/300', alt: 'Mountain view', index: 2 }, { src: 'https://picsum.photos/seed/4/500/300', alt: 'Ocean sunset', index: 3 }, { src: 'https://picsum.photos/seed/5/500/300', alt: 'Forest path', index: 4 }, ]; export component Basic() { } ``` ### Controlled To create a controlled Carousel component, you can manage the state of the carousel using the `page` prop and update it when the `onPageChange` event handler is called: **Example: controlled** ```ripple import { Carousel } from 'ark-ripple/carousel'; import { track } from 'ripple'; import styles from 'styles/carousel.module.css'; const images = [ { src: 'https://picsum.photos/seed/1/500/300', alt: 'Nature landscape', index: 0 }, { src: 'https://picsum.photos/seed/2/500/300', alt: 'City skyline', index: 1 }, { src: 'https://picsum.photos/seed/3/500/300', alt: 'Mountain view', index: 2 }, { src: 'https://picsum.photos/seed/4/500/300', alt: 'Ocean sunset', index: 3 }, { src: 'https://picsum.photos/seed/5/500/300', alt: 'Forest path', index: 4 }, ]; export component Controlled() { let page = track(0); {'←'} for (const image of images; key image.index) { } ![]()
{'→'} for (const image of images; key image.index) { } { @page = e.page; }} > } ``` ### Root Provider An alternative way to control the carousel is to use the `RootProvider` component and the `useCarousel` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Carousel } from 'ark-ripple/carousel'; import { useCarousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const images = [ { src: 'https://picsum.photos/seed/1/500/300', alt: 'Nature landscape', index: 0 }, { src: 'https://picsum.photos/seed/2/500/300', alt: 'City skyline', index: 1 }, { src: 'https://picsum.photos/seed/3/500/300', alt: 'Mountain view', index: 2 }, { src: 'https://picsum.photos/seed/4/500/300', alt: 'Ocean sunset', index: 3 }, { src: 'https://picsum.photos/seed/5/500/300', alt: 'Forest path', index: 4 }, ]; export component RootProvider() { const carousel = useCarousel({ slideCount: images.length });{'←'} for (const image of images; key image.index) { } ![]()
{'→'} for (const image of images; key image.index) { } } ``` ### Autoplay Pass the `autoplay` and `loop` props to `Carousel.Root` to make the carousel play automatically. **Example: autoplay** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const images = [ { src: 'https://picsum.photos/seed/1/500/300', alt: 'Nature landscape', index: 0 }, { src: 'https://picsum.photos/seed/2/500/300', alt: 'City skyline', index: 1 }, { src: 'https://picsum.photos/seed/3/500/300', alt: 'Mountain view', index: 2 }, { src: 'https://picsum.photos/seed/4/500/300', alt: 'Ocean sunset', index: 3 }, { src: 'https://picsum.photos/seed/5/500/300', alt: 'Forest path', index: 4 }, ]; export component Autoplay() {{'←'} for (const image of images; key image.index) { } ![]()
{'→'} for (const image of images; key image.index) { } } ``` ### Pause on Hover This feature isn't built-in, but you can use the `play()` and `pause()` methods from `Carousel.Context` to implement pause on hover. **Example: pause-on-hover** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const images = [ { src: 'https://picsum.photos/seed/1/500/300', alt: 'Nature landscape', index: 0 }, { src: 'https://picsum.photos/seed/2/500/300', alt: 'City skyline', index: 1 }, { src: 'https://picsum.photos/seed/3/500/300', alt: 'Mountain view', index: 2 }, { src: 'https://picsum.photos/seed/4/500/300', alt: 'Ocean sunset', index: 3 }, { src: 'https://picsum.photos/seed/5/500/300', alt: 'Forest path', index: 4 }, ]; export component PauseOnHover() { for (const image of images; key image.index) { } ![]()
{'‹'} {'⏸'} {'›'} } ``` ### Thumbnail Indicators Replace default indicator dots with image thumbnails. Render each thumbnail inside `Carousel.Indicator` to create a visual preview of each slide: **Example: thumbnail-indicator** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const images = [ { src: 'https://picsum.photos/seed/1/500/300', alt: 'Nature landscape', index: 0 }, { src: 'https://picsum.photos/seed/2/500/300', alt: 'City skyline', index: 1 }, { src: 'https://picsum.photos/seed/3/500/300', alt: 'Mountain view', index: 2 }, { src: 'https://picsum.photos/seed/4/500/300', alt: 'Ocean sunset', index: 3 }, { src: 'https://picsum.photos/seed/5/500/300', alt: 'Forest path', index: 4 }, ]; export component ThumbnailIndicator() { component children({ context }) { {'Autoplay is: '} {@context.isPlaying ? 'playing' : 'paused'} } component children({ context }) { { @context.pause(); }} onPointerLeave={() => { @context.play(); }} > for (const image of images; key image.index) { }} ![]()
for (const image of images; key image.index) { } } ``` ### Vertical Add the `orientation="vertical"` prop to `Carousel.Root` to switch the carousel to vertical scrolling. This can be helpful for displaying vertical galleries or content feeds. **Example: vertical** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const images = [ { src: 'https://picsum.photos/seed/1/500/300', alt: 'Nature landscape', index: 0 }, { src: 'https://picsum.photos/seed/2/500/300', alt: 'City skyline', index: 1 }, { src: 'https://picsum.photos/seed/3/500/300', alt: 'Mountain view', index: 2 }, { src: 'https://picsum.photos/seed/4/500/300', alt: 'Ocean sunset', index: 3 }, { src: 'https://picsum.photos/seed/5/500/300', alt: 'Forest path', index: 4 }, ]; export component Vertical() { {'←'} for (const image of images; key image.index) { } ![]()
{'→'} for (const image of images; key image.index) { } ![]()
} ``` ### Dynamic Manage slides dynamically by storing them in state and syncing the carousel page. Pass the `page` prop and `onPageChange` handler to `Carousel.Root`, and update `slideCount` when slides are added or removed. This demonstrates bidirectional state synchronization between your component state and the carousel. **Example: dynamic-slides** ```ripple import { Carousel } from 'ark-ripple/carousel'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/carousel.module.css'; export component DynamicSlides() { let slides = track([0, 1, 2, 3, 4]); let page = track(0); for (const image of images; key image.index) { } ![]()
{'↑'} for (const image of images; key image.index) { } {'↓'} } ``` ### Scroll to Slide Use `Carousel.Context` to access the carousel API and call `api.scrollToIndex(index)` to programmatically navigate to a specific slide. This is useful for creating custom navigation or jump-to-slide functionality. **Example: scroll-to** ```ripple import { Carousel } from 'ark-ripple/carousel'; import button from 'styles/button.module.css'; import styles from 'styles/carousel.module.css'; const slides = [0, 1, 2, 3, 4]; export component ScrollTo() {{ @page = e.page; }} > for (const index of @slides; key index) { } {'Slide '} {index + 1}{'←'} for (const index of @slides; key index) { } {'→'} } ``` ### Slides Per Page Display multiple slides simultaneously by setting the `slidesPerPage` prop on `Carousel.Root`. Use `api.pageSnapPoints` from `Carousel.Context` to render the correct number of indicators based on pages rather than individual slides. Add the `spacing` prop to control the gap between slides. **Example: slides-per-page** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const slides = [0, 1, 2, 3, 4, 5]; const pageIndicators = [0, 1, 2]; export component SlidesPerPage() { component children({ context }) { } for (const index of slides; key index) { } {'Slide '} {index + 1}{'←'} {'→'} for (const index of slides; key index) { } } ``` ### Spacing Control the gap between slides using the `spacing` prop on `Carousel.Root`. Combine it with `slidesPerPage` to create layouts that show partial previews of adjacent slides. **Example: spacing** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const slides = [0, 1, 2, 3, 4, 5]; export component Spacing() { {'←'} {'→'} for (const index of slides; key index) { } {'Slide '} {index + 1}for (const index of pageIndicators; key index) { } {'spacing=\'48px\''} } ``` ### Variable Sizes To allow slides with different widths, set the `autoSize` prop on `Carousel.Root`. This lets each `Carousel.Item` define its own width, and the carousel will adjust automatically. You can also use the `snapAlign` prop on individual items to control where each one snaps into view. **Example: variable-size** ```ripple import { Carousel } from 'ark-ripple/carousel'; import styles from 'styles/carousel.module.css'; const items = [ { id: '1', width: '120px', label: 'Small', index: 0 }, { id: '2', width: '200px', label: 'Medium Size', index: 1 }, { id: '3', width: '80px', label: 'XS', index: 2 }, { id: '4', width: '250px', label: 'Large Content Here', index: 3 }, { id: '5', width: '150px', label: 'Regular', index: 4 }, ]; export component VariableSize() {for (const index of slides; key index) { } {index + 1}{'←'} component children({ context }) { for (const snapPoint of @context.pageSnapPoints; key snapPoint) { }} {'→'} } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `slideCount` | `number` | Yes | The total number of slides. Useful for SSR to render the initial ating the snap points. | | `allowMouseDrag` | `boolean` | No | Whether to allow scrolling via dragging with mouse | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoplay` | `boolean | { delay: number }` | No | Whether to scroll automatically. The default delay is 4000ms. | | `autoSize` | `boolean` | No | Whether to enable variable width slides. | | `defaultPage` | `number` | No | The initial page to scroll to when rendered. Use when you don't need to control the page of the carousel. | | `ids` | `Partial<{ root: string item: (index: number) => string itemGroup: string nextTrigger: string prevTrigger: string indicatorGroup: string indicator: (index: number) => string }>` | No | The ids of the elements in the carousel. Useful for composition. | | `inViewThreshold` | `number | number[]` | No | The threshold for determining if an item is in view. | | `loop` | `boolean` | No | Whether the carousel should loop around. | | `onAutoplayStatusChange` | `(details: AutoplayStatusDetails) => void` | No | Function called when the autoplay status changes. | | `onDragStatusChange` | `(details: DragStatusDetails) => void` | No | Function called when the drag status changes. | | `onPageChange` | `(details: PageChangeDetails) => void` | No | Function called when the page changes. | | `orientation` | `'horizontal' | 'vertical'` | No | The orientation of the element. | | `padding` | `string` | No | Defines the extra space added around the scrollable area, enabling nearby items to remain partially in view. | | `page` | `number` | No | The controlled page of the carousel. | | `slidesPerMove` | `number | 'auto'` | No | The number of slides to scroll at a time. When set to `auto`, the number of slides to scroll is determined by the `slidesPerPage` property. | | `slidesPerPage` | `number` | No | The number of slides to show at a time. | | `snapType` | `'proximity' | 'mandatory'` | No | The snap type of the item. | | `spacing` | `string` | No | The amount of space between items. | | `translations` | `IntlTranslations` | No | The localized messages to use. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | root | | `[data-orientation]` | The orientation of the carousel | **Root CSS Variables:** | Variable | Description | |----------|-------------| | `--slides-per-page` | The number of slides visible per page | | `--slide-spacing` | The spacing between slides | | `--slide-item-size` | The calculated size of each slide item | **AutoplayIndicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `fallback` | `string | number | bigint | boolean | ReactElement {'←'} {'→'} for (const item of items; key item.id) { } {item.label}component children({ context }) { for (const snapPoint of @context.pageSnapPoints; key snapPoint) { }} > | Iterable | ReactPortal | Promise<...>` | No | The fallback content to render when autoplay is paused. | **AutoplayTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **AutoplayTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | autoplay-trigger | | `[data-orientation]` | The orientation of the autoplaytrigger | | `[data-pressed]` | Present when pressed | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | control | | `[data-orientation]` | The orientation of the control | **IndicatorGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **IndicatorGroup Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | indicator-group | | `[data-orientation]` | The orientation of the indicatorgroup | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `index` | `number` | Yes | The index of the indicator. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `readOnly` | `boolean` | No | Whether the indicator is read only. | **Indicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | indicator | | `[data-orientation]` | The orientation of the indicator | | `[data-index]` | The index of the item | | `[data-readonly]` | Present when read-only | | `[data-current]` | Present when current | **ItemGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemGroup Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | item-group | | `[data-orientation]` | The orientation of the item | | `[data-dragging]` | Present when in the dragging state | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `index` | `number` | Yes | The index of the item. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `snapAlign` | `'center' | 'start' | 'end'` | No | The snap alignment of the item. | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | item | | `[data-index]` | The index of the item | | `[data-inview]` | Present when in viewport | | `[data-orientation]` | The orientation of the item | **NextTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **NextTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | next-trigger | | `[data-orientation]` | The orientation of the nexttrigger | **PrevTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **PrevTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | carousel | | `[data-part]` | prev-trigger | | `[data-orientation]` | The orientation of the prevtrigger | **ProgressText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseCarouselReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `page` | `number` | The current index of the carousel | | `pageSnapPoints` | `number[]` | The current snap points of the carousel | | `isPlaying` | `boolean` | Whether the carousel is auto playing | | `isDragging` | `boolean` | Whether the carousel is being dragged. This only works when `draggable` is true. | | `canScrollNext` | `boolean` | Whether the carousel is can scroll to the next view | | `canScrollPrev` | `boolean` | Whether the carousel is can scroll to the previous view | | `scrollToIndex` | `(index: number, instant?: boolean) => void` | Function to scroll to a specific item index | | `scrollTo` | `(page: number, instant?: boolean) => void` | Function to scroll to a specific page | | `scrollNext` | `(instant?: boolean) => void` | Function to scroll to the next page | | `scrollPrev` | `(instant?: boolean) => void` | Function to scroll to the previous page | | `getProgress` | `() => number` | Returns the current scroll progress as a percentage | | `getProgressText` | `() => string` | Returns the progress text | | `play` | `VoidFunction` | Function to start/resume autoplay | | `pause` | `VoidFunction` | Function to pause autoplay | | `isInView` | `(index: number) => boolean` | Whether the item is in view | | `refresh` | `VoidFunction` | Function to re-compute the snap points and clamp the page | ## Accessibility Complies with the [Carousel WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/). # Checkbox ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/checkbox.module.css'; export component Basic() { } ``` ### Default Checked Use the `defaultChecked` prop to set the initial checked state in an uncontrolled manner. The checkbox will manage its own state internally. **Example: default-checked** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check } from 'lucide-ripple'; export component DefaultChecked() { {'Checkbox'} } ``` ### Controlled Use the `checked` and `onCheckedChange` props to programatically control the checkbox's state. **Example: controlled** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { track } from 'ripple'; import { Check } from 'lucide-ripple'; export component Controlled() { let checked = track(true); {'Checkbox'} { @checked = e.checked; }} > } ``` ### Root Provider An alternative way to control the checkbox is to use the `RootProvider` component and the `useCheckbox` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Checkbox, useCheckbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import button from 'styles/button.module.css'; import { Check } from 'lucide-ripple'; export component RootProvider() { const checkbox = useCheckbox();{'Checkbox'} } ``` ### Disabled Use the `disabled` prop to make the checkbox non-interactive. **Example: disabled** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check } from 'lucide-ripple'; export component Disabled() {if (@checkbox.checked) { } else { } {'Checkbox'} } ``` ### Indeterminate Use the `indeterminate` prop to create a checkbox in an indeterminate state (partially checked). **Example: indeterminate** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check, Minus } from 'lucide-ripple'; export component Indeterminate() { {'Checkbox'} } ``` ### Field The checkbox integrates smoothly with the `Field` component to handle form state, helper text, and error text for proper accessibility. **Example: with-field** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { Field } from 'ark-ripple/field'; import { Check, Minus } from 'lucide-ripple'; import styles from 'styles/checkbox.module.css'; import field from 'styles/field.module.css'; export component WithField() { {'Checkbox'} } ``` ### Form Pass the `name` and `value` props to the `Checkbox.Root` component to make the checkbox part of a form. The checkbox's value will be submitted with the form when the user submits it. **Example: with-form** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import button from 'styles/button.module.css'; import { Check } from 'lucide-ripple'; export component WithForm() { } ``` ### Context Access the checkbox's state and methods with `Checkbox.Context` or the `useCheckboxContext` hook. **Example: context** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check } from 'lucide-ripple'; export component Context() { {'Label'} {'Additional Info'} {'Error Info'} } ``` ## Checkbox Group Use the `Checkbox.Group` component to manage a group of checkboxes. The `Checkbox.Group` component manages the state of the checkboxes and provides a way to access the checked values. ```tsx component children({ context }) { {'Checked: '} {String(@context.checked)} }``` **Example: group** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check } from 'lucide-ripple'; export component Group() { for (const item of items; key item.value) { } const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, ]; ``` ### Controlled Use the `value` and `onValueChange` props to programmatically control the checkbox group's state. This example demonstrates how to manage selected checkboxes in an array and display the current selection. **Example: group-controlled** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { track } from 'ripple'; import { Check } from 'lucide-ripple'; export component GroupControlled() { let value = track(['react']);} {item.label} } const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, ]; ``` ### Root Provider Use the `useCheckboxGroup` hook to create the checkbox group store and pass it to the `Checkbox.GroupProvider` component. This provides maximum control over the group programmatically, similar to how `RootProvider` works for individual checkboxes. **Example: group-provider** ```ripple import { Checkbox, useCheckboxGroup } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check } from 'lucide-ripple'; export component GroupProvider() { const group = useCheckboxGroup( { defaultValue: ['react'], name: 'framework', }, );{ @value = v; console.log(v); }} > for (const item of items; key item.value) { } {item.label} for (const item of items; key item.value) { } const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, ]; ``` ### Invalid Use the `invalid` prop on `Checkbox.Group` to mark the entire group as invalid for validation purposes. This applies the invalid state to all checkboxes within the group. **Example: group-with-invalid** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check } from 'lucide-ripple'; export component GroupWithInvalid() {} {item.label} for (const item of items; key item.value) { } const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, ]; ``` ### Max Selected Use the `maxSelectedValues` prop to limit the number of checkboxes that can be selected at once. Once the maximum is reached, remaining checkboxes become disabled. **Example: group-with-max-selected** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import { Check } from 'lucide-ripple'; export component GroupWithMaxSelected() {} {item.label} for (const item of items; key item.value) { } const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, { label: 'Svelte', value: 'svelte' }, ]; ``` ### Select All Implement a "select all" checkbox that controls all checkboxes within a group. The parent checkbox automatically shows an indeterminate state when some (but not all) items are selected, and becomes fully checked when all items are selected. **Example: group-with-select-all** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { Check, Minus } from 'lucide-ripple'; import { track, trackSplit } from 'ripple'; import styles from 'styles/checkbox.module.css'; component CheckboxItem(props: Checkbox.RootProps & { children?: any }) {} {item.label} } export component GroupWithSelectAll() { let value = track ([]); const handleSelectAll = (checked: boolean) => { @value = checked ? items.map((item) => item.value) : []; }; const allSelected = track(() => @value.length === items.length); const indeterminate = track(() => @value.length > 0 && @value.length < items.length); } const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, ]; ``` ### Form Use the `Checkbox.Group` component within a form to handle multiple checkbox values with form submission. The `name` prop ensures all selected values are collected as an array when the form is submitted using `FormData.getAll()`. **Example: group-with-form** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import styles from 'styles/checkbox.module.css'; import button from 'styles/button.module.css'; import { Check } from 'lucide-ripple'; export component GroupWithForm() { } const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, ]; ``` ### Fieldset Use the `Fieldset` component with `Checkbox.Group` to provide semantic grouping with legend, helper text, and error text support. **Example: group-with-fieldset** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { Fieldset } from 'ark-ripple/fieldset'; import { Check } from 'lucide-ripple'; import styles from 'styles/checkbox.module.css'; import fieldset from 'styles/fieldset.module.css'; export component GroupWithFieldset() {{ handleSelectAll(!!e.checked); }} > {'JSX Frameworks'} { @value = v; }} > for (const item of items; key item.value) { {item.label} }} const items = [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, ]; ``` ## Guides ### asChild Behavior The `Checkbox.Root` element of the checkbox is a `label` element. This is because the checkbox is a form control and should be associated with a label to provide context and meaning to the user. Otherwise, the HTML and accessibility structure will be invalid. > If you need to use the `asChild` property, make sure that the `label` element is the direct child of the > `Checkbox.Root` component. ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `checked` | `CheckedState` | No | The controlled checked state of the checkbox | | `defaultChecked` | `CheckedState` | No | The initial checked state of the checkbox when rendered. Use when you don't need to control the checked state of the checkbox. | | `disabled` | `boolean` | No | Whether the checkbox is disabled | | `form` | `string` | No | The id of the form that the checkbox belongs to. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string; hiddenInput: string; control: string; label: string }>` | No | The ids of the elements in the checkbox. Useful for composition. | | `invalid` | `boolean` | No | Whether the checkbox is invalid | | `name` | `string` | No | The name of the input field in a checkbox. Useful for form submission. | | `onCheckedChange` | `(details: CheckedChangeDetails) => void` | No | The callback invoked when the checked state changes. | | `readOnly` | `boolean` | No | Whether the checkbox is read-only | | `required` | `boolean` | No | Whether the checkbox is required | | `value` | `string` | No | The value of checkbox input. Useful for form submission. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-active]` | Present when active or pressed | | `[data-focus]` | Present when focused | | `[data-focus-visible]` | Present when focused with keyboard | | `[data-readonly]` | Present when read-only | | `[data-hover]` | Present when hovered | | `[data-disabled]` | Present when disabled | | `[data-state]` | "indeterminate" | "checked" | "unchecked" | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-active]` | Present when active or pressed | | `[data-focus]` | Present when focused | | `[data-focus-visible]` | Present when focused with keyboard | | `[data-readonly]` | Present when read-only | | `[data-hover]` | Present when hovered | | `[data-disabled]` | Present when disabled | | `[data-state]` | "indeterminate" | "checked" | "unchecked" | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | **Group Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `defaultValue` | `string[]` | No | The initial value of `value` when uncontrolled | | `disabled` | `boolean` | No | If `true`, the checkbox group is disabled | | `invalid` | `boolean` | No | If `true`, the checkbox group is invalid | | `maxSelectedValues` | `number` | No | The maximum number of selected values | | `name` | `string` | No | The name of the input fields in the checkbox group (Useful for form submission). | | `onValueChange` | `(value: string[]) => void` | No | The callback to call when the value changes | | `readOnly` | `boolean` | No | If `true`, the checkbox group is read-only | | `value` | `string[]` | No | The controlled value of the checkbox group | **GroupProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseCheckboxGroupContext` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **HiddenInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `indeterminate` | `boolean` | No | | **Indicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-active]` | Present when active or pressed | | `[data-focus]` | Present when focused | | `[data-focus-visible]` | Present when focused with keyboard | | `[data-readonly]` | Present when read-only | | `[data-hover]` | Present when hovered | | `[data-disabled]` | Present when disabled | | `[data-state]` | "indeterminate" | "checked" | "unchecked" | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-active]` | Present when active or pressed | | `[data-focus]` | Present when focused | | `[data-focus-visible]` | Present when focused with keyboard | | `[data-readonly]` | Present when read-only | | `[data-hover]` | Present when hovered | | `[data-disabled]` | Present when disabled | | `[data-state]` | "indeterminate" | "checked" | "unchecked" | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseCheckboxReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `checked` | `boolean` | Whether the checkbox is checked | | `disabled` | `boolean` | Whether the checkbox is disabled | | `indeterminate` | `boolean` | Whether the checkbox is indeterminate | | `focused` | `boolean` | Whether the checkbox is focused | | `checkedState` | `CheckedState` | The checked state of the checkbox | | `setChecked` | `(checked: CheckedState) => void` | Function to set the checked state of the checkbox | | `toggleChecked` | `VoidFunction` | Function to toggle the checked state of the checkbox | ## Accessibility Complies with the [Checkbox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/). ### Keyboard Support **`Space`** Description: Toggle the checkbox # Clipboard ## Anatomy ```tsx {'Select frameworks'} {'Choose your preferred frameworks'} for (const item of items; key item.value) { } {item.label} ``` ## Examples **Example: basic** ```ripple import { Clipboard } from 'ark-ripple/clipboard'; import { Check, Copy } from 'lucide-ripple'; import styles from 'styles/clipboard.module.css'; export component Basic() { } ``` ### Controlled Control the clipboard value externally by managing the state yourself and using `onValueChange` to handle updates. **Example: controlled** ```ripple import { Clipboard } from 'ark-ripple/clipboard'; import { track } from 'ripple'; import styles from 'styles/clipboard.module.css'; import button from 'styles/button.module.css'; import { Check, Copy } from 'lucide-ripple'; export component Controlled() { const url = track('https://ark-ui.rip'); {'Copy this link'} } ``` ### Root Provider An alternative way to control the clipboard is to use the `RootProvider` component and the `useClipboard` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Clipboard, useClipboard } from 'ark-ripple/clipboard'; import styles from 'styles/clipboard.module.css'; import { Check, Copy } from 'lucide-ripple'; export component RootProvider() { const clipboard = useClipboard({ value: 'https://ark-ui.rip' });{ @url = details.value; }} > {'Copy this link'} } ``` ### Context Access the clipboard's state with `Clipboard.Context` or the `useClipboardContext` hook. You get properties like `copied`, `value`, and `setValue`. > Alternatively, you can use the `useClipboardContext` hook to access the clipboard context. **Example: context** ```ripple import { Clipboard } from 'ark-ripple/clipboard'; import { Check, Copy } from 'lucide-ripple'; import styles from 'styles/clipboard.module.css'; import button from 'styles/button.module.css'; export component Context() {{'Copy this link'} } ``` ### Copy Status Use the `onStatusChange` prop to listen for copy operations. It exposes a `copied` property that you can use to display a success message. **Example: copy-status** ```ripple import { Clipboard } from 'ark-ripple/clipboard'; import { track } from 'ripple'; import { Check, Copy } from 'lucide-ripple'; import styles from 'styles/clipboard.module.css'; export component CopyStatus() { const copyCount = track(0); {'Copy this link'} component children({ context }) { } { if (details.copied) { @copyCount = @copyCount + 1; } }} > } ``` ### Timeout Configure the copy status timeout duration using the `timeout` prop. Default is 3000ms (3 seconds). **Example: timeout** ```ripple import { Clipboard } from 'ark-ripple/clipboard'; import { Check, Copy } from 'lucide-ripple'; import styles from 'styles/clipboard.module.css'; export component Timeout() {{'Copied '} {@copyCount} {' times'}
} ``` ### Value Text Use `Clipboard.ValueText` to display the current clipboard value. **Example: value-text** ```ripple import { Clipboard } from 'ark-ripple/clipboard'; import { Check, Copy } from 'lucide-ripple'; import styles from 'styles/clipboard.module.css'; export component ValueText() { {'Copy this link (5 second timeout)'} } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `defaultValue` | `string` | No | The initial value to be copied to the clipboard when rendered. Use when you don't need to control the value of the clipboard. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string; input: string; label: string }>` | No | The ids of the elements in the clipboard. Useful for composition. | | `onStatusChange` | `(details: CopyStatusDetails) => void` | No | The function to be called when the value is copied to the clipboard | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | The function to be called when the value changes | | `timeout` | `number` | No | The timeout for the copy operation | | `value` | `string` | No | The controlled value of the clipboard | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | clipboard | | `[data-part]` | root | | `[data-copied]` | Present when copied state is true | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | clipboard | | `[data-part]` | control | | `[data-copied]` | Present when copied state is true | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `copied` | `string | number | bigint | boolean | ReactElement > | Iterable | ReactPortal | Promise<...>` | No | | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | clipboard | | `[data-part]` | input | | `[data-copied]` | Present when copied state is true | | `[data-readonly]` | Present when read-only | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | clipboard | | `[data-part]` | label | | `[data-copied]` | Present when copied state is true | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseClipboardReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | clipboard | | `[data-part]` | trigger | | `[data-copied]` | Present when copied state is true | **ValueText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `copied` | `boolean` | Whether the value has been copied to the clipboard | | `value` | `string` | The value to be copied to the clipboard | | `setValue` | `(value: string) => void` | Set the value to be copied to the clipboard | | `copy` | `VoidFunction` | Copy the value to the clipboard | # Collapsible ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { Collapsible } from 'ark-ripple/collapsible'; import { ChevronRight } from 'lucide-ripple'; import styles from 'styles/collapsible.module.css'; export component Basic() { } ``` ### Disabled Use the `disabled` prop to disable the collapsible and prevent it from being toggled. **Example: disabled** ```ripple import { Collapsible } from 'ark-ripple/collapsible'; import { ChevronRight } from 'lucide-ripple'; import styles from 'styles/collapsible.module.css'; export component Disabled() { {'What is Ark UI?'} {'Ark UI is a headless component library for building accessible, high-quality UI components for React, Solid, Vue, and Svelte.'}} ``` ### Partial Collapse Use the `collapsedHeight` or `collapsedWidth` props to create a "show more/less" pattern. When set, the content maintains the specified dimensions when collapsed instead of collapsing to 0px. We expose the `--collapsed-height` or `--collapsed-width` variables to use in your CSS animations. **Example: partial-collapse** ```ripple import { Collapsible } from 'ark-ripple/collapsible'; import { ChevronRight } from 'lucide-ripple'; import styles from 'styles/collapsible.module.css'; export component PartialCollapse() { {'System Requirements'} {'This section is currently unavailable.'}} ``` > Interactive elements (links, buttons, inputs) within the collapsed area automatically become `inert` to prevent > keyboard navigation to hidden content. ### Nested Collapsibles You can nest collapsibles within collapsibles to create hierarchical content structures. **Example: nested** ```ripple import { Collapsible } from 'ark-ripple/collapsible'; import { ChevronRight } from 'lucide-ripple'; import styles from 'styles/collapsible.module.css'; export component Nested() { {'Read More'} {'Ark UI is a headless component library for building accessible, high-quality UI components for React, Solid, Vue, and Svelte. It provides unstyled, fully accessible components that you can customize to match your design system.'}
{'Built on top of Zag.js state machines, Ark UI ensures consistent behavior across all frameworks while giving you complete control over styling. Each component follows WAI-ARIA patterns for accessibility out of the box.'}
{'Whether you\'re building a design system from scratch or need reliable primitives for your next project, Ark UI provides the foundation you need without imposing any visual constraints.'}
} ``` ### Lazy Mount Use `lazyMount` to delay mounting the content until first opened, and `unmountOnExit` to remove it from the DOM when collapsed. Combining both ensures the component is only in the DOM while expanded. **Example: lazy-mount** ```ripple import { Collapsible } from 'ark-ripple/collapsible'; import { ChevronRight } from 'lucide-ripple'; import styles from 'styles/collapsible.module.css'; export component LazyMount() { {'Getting Started'} {'Welcome to the Ark UI documentation. Here are some topics to explore:'}
{'Installation'} {'Install Ark UI using your preferred package manager:'}
{'npm install ark-ripple'}{'Styling'} {'Ark UI components are unstyled by default. Use CSS modules, Tailwind, or any styling solution.'}
} ``` ### Root Provider An alternative way to control the collapsible is to use the `RootProvider` component and the `useCollapsible` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Collapsible } from 'ark-ripple/collapsible'; import { useCollapsible } from 'ark-ripple/collapsible'; import { ChevronRight } from 'lucide-ripple'; import styles from 'styles/collapsible.module.css'; export component RootProvider() { const collapsible = useCollapsible(); {'Session Details'} {'This content is lazily mounted when first opened and removed from the DOM when collapsed.'}} ``` ## Guides ### Indicator Animation To rotate the indicator icon (such as a chevron) when the collapsible opens and closes, use CSS transforms with the `data-state` attribute: ```css [data-scope='collapsible'][data-part='indicator'] { transition: transform 200ms; &[data-state='open'] { transform: rotate(180deg); } } ``` ### Open vs Visible When using `useCollapsible` or `useCollapsibleContext`, you can access the `open` and `visible` state properties. They seem similar but serve different purposes: - **`open`**: Indicates the intended state of the collapsible. This is `true` when the collapsible should be expanded and `false` when it should be collapsed. This changes immediately when triggered. - **`visible`**: Indicates whether the content is currently visible in the DOM. This accounts for exit animations - the content remains `visible` while the closing animation plays, even though `open` is already `false`. Once the animation completes, `visible` becomes `false`. ### Content Animation Use the `--height` and/or `--width` CSS variables to animate the size of the content when it expands or closes. If you use `collapsedHeight` or `collapsedWidth`, update your CSS animations to use the `--collapsed-height` or `--collapsed-width` variables as the starting/ending point: ```css @keyframes expand { from { height: var(--collapsed-height, 0); } to { height: var(--height); } } @keyframes collapse { from { height: var(--height); } to { height: var(--collapsed-height, 0); } } [data-scope='collapsible'][data-part='content'] { &[data-state='open'] { animation: expand 250ms; } &[data-state='closed'] { animation: collapse 250ms; } } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `collapsedHeight` | `string | number` | No | The height of the content when collapsed. | | `collapsedWidth` | `string | number` | No | The width of the content when collapsed. | | `defaultOpen` | `boolean` | No | The initial open state of the collapsible when rendered. Use when you don't need to control the open state of the collapsible. | | `disabled` | `boolean` | No | Whether the collapsible is disabled. | | `ids` | `Partial<{ root: string; content: string; trigger: string }>` | No | The ids of the elements in the collapsible. Useful for composition. | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | The callback invoked when the exit animation completes. | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | The callback invoked when the open state changes. | | `open` | `boolean` | No | The controlled open state of the collapsible. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | collapsible | | `[data-part]` | root | | `[data-state]` | "open" | "closed" | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | collapsible | | `[data-part]` | content | | `[data-collapsible]` | | | `[data-state]` | "open" | "closed" | | `[data-disabled]` | Present when disabled | | `[data-has-collapsed-size]` | Present when the content has collapsed width or height | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--height` | The height of the element | | `--width` | The width of the element | | `--collapsed-height` | The height of the Content | | `--collapsed-width` | The width of the Content | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Indicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | collapsible | | `[data-part]` | indicator | | `[data-state]` | "open" | "closed" | | `[data-disabled]` | Present when disabled | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseCollapsibleReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | collapsible | | `[data-part]` | trigger | | `[data-state]` | "open" | "closed" | | `[data-disabled]` | Present when disabled | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `open` | `boolean` | Whether the collapsible is open. | | `visible` | `boolean` | Whether the collapsible is visible (open or closing) | | `disabled` | `boolean` | Whether the collapsible is disabled | | `setOpen` | `(open: boolean) => void` | Function to open or close the collapsible. | | `measureSize` | `VoidFunction` | Function to measure the size of the content. | ## Accessibility ### Keyboard Support **`Space`** Description: Opens/closes the collapsible. **`Enter`** Description: Opens/closes the collapsible. # Color Picker ## Anatomy ```tsx{'Toggle Panel'} {'This panel can be toggled by the button above, which uses the useCollapsible hook for state management.'}``` ## Examples **Example: basic** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Pipette } from 'lucide-ripple'; import styles from 'styles/color-picker.module.css'; export component Basic() { } ``` ### Controlled Use the `value` and `onValueChange` props to programatically control the color picker's state. **Example: controlled** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Pipette } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/color-picker.module.css'; export component Controlled() { let color = track(parseColor('hsl(20, 100%, 50%)')); {'Color'} } ``` ### Open Controlled Control the open state of the color picker popover programmatically using the `open` and `onOpenChange` props. **Example: open-controlled** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Pipette } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/color-picker.module.css'; import button from 'styles/button.module.css'; export component OpenControlled() { let open = track(false);{ @color = e.value; }} > {'Color'} } ``` ### Root Provider An alternative way to control the color picker is to use the `RootProvider` component and the `useColorPicker` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { ColorPicker, parseColor, useColorPicker } from 'ark-ripple/color-picker'; import { Check, Pipette } from 'lucide-ripple'; import styles from 'styles/color-picker.module.css'; const swatches = ['red', 'blue', 'green', 'orange']; export component RootProvider() { const colorPicker = useColorPicker({ defaultValue: parseColor('#eb5e41') });{ @open = e.open; }} defaultValue={parseColor('#eb5e41')} > {'Color'} } ``` ### Disabled Use the `disabled` prop to disable the color picker. **Example: disabled** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import styles from 'styles/color-picker.module.css'; export component Disabled() {{'Color'} for (const color of swatches; key color) { } } ``` ### Inline Render the color picker inline without a popover by using the `inline` prop. **Example: inline** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Check } from 'lucide-ripple'; import styles from 'styles/color-picker.module.css'; const swatches = ['red', 'blue', 'green', 'orange']; export component Inline() { {'Color'} } ``` ### Input Only A minimal color picker with just an input field, value swatch, and eye dropper trigger. **Example: input-only** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Pipette } from 'lucide-ripple'; import styles from 'styles/color-picker.module.css'; export component InputOnly() { for (const color of swatches; key color) { } } ``` ### Slider Only Display only the channel sliders for RGB color selection. **Example: slider-only** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import styles from 'styles/color-picker.module.css'; export component SliderOnly() { {'Color'} } ``` ### Swatch Only A simple color picker with only preset color swatches. **Example: swatch-only** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Check } from 'lucide-ripple'; import styles from 'styles/color-picker.module.css'; const swatches = ['red', 'pink', 'orange', 'purple']; export component SwatchOnly() { {'R'} {'G'} {'B'}} ``` ### Swatches Include preset color swatches in the color picker content for quick color selection. **Example: swatches** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Check, Pipette } from 'lucide-ripple'; import styles from 'styles/color-picker.module.css'; const swatches = ['red', 'blue', 'green', 'orange']; export component Swatches() { for (const color of swatches; key color) { } } ``` ### Value Swatch Display the current color value as a swatch alongside the color area and sliders. **Example: value-swatch** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import styles from 'styles/color-picker.module.css'; export component ValueSwatch() { {'Color'} for (const color of swatches; key color) { } } ``` ### Field The `Field` component helps manage form-related state and accessibility attributes of a color picker. It includes handling ARIA labels, helper text, and error text to ensure proper accessibility. **Example: with-field** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Field } from 'ark-ripple/field'; import styles from 'styles/color-picker.module.css'; import field from 'styles/field.module.css'; export component WithField() { } ``` ### Form Usage Integrate the color picker with form libraries like React Hook Form using the `HiddenInput` component. **Example: form-usage** ```ripple import { ColorPicker, parseColor } from 'ark-ripple/color-picker'; import { Pipette } from 'lucide-ripple'; import styles from 'styles/color-picker.module.css'; import button from 'styles/button.module.css'; export component FormUsage() { const onSubmit = (e: Event) => { e.preventDefault(); const form = e.target as HTMLFormElement; alert(JSON.stringify(Object.fromEntries(new FormData(form)))); }; } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `closeOnSelect` | `boolean` | No | Whether to close the color picker when a swatch is selected | | `defaultFormat` | `ColorFormat` | No | The initial color format when rendered. Use when you don't need to control the color format of the color picker. | | `defaultOpen` | `boolean` | No | The initial open state of the color picker when rendered. Use when you don't need to control the open state of the color picker. | | `defaultValue` | `Color` | No | The initial color value when rendered. Use when you don't need to control the color value of the color picker. | | `disabled` | `boolean` | No | Whether the color picker is disabled | | `format` | `ColorFormat` | No | The controlled color format to use | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string; control: string; trigger: string; label: string; input: string; hiddenInput: string; content: string; area: string; areaGradient: string; positioner: string; formatSelect: string; areaThumb: string; channelInput: (id: string) => string; channelSliderTrack: (id: ColorChannel) => string; channe...` | No | The ids of the elements in the color picker. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `initialFocusEl` | `() => HTMLElement | null` | No | The initial focus element when the color picker is opened. | | `inline` | `boolean` | No | Whether to render the color picker inline | | `invalid` | `boolean` | No | Whether the color picker is invalid | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `name` | `string` | No | The name for the form input | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component | | `onFormatChange` | `(details: FormatChangeDetails) => void` | No | Function called when the color format changes | | `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Handler that is called when the user opens or closes the color picker. | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Handler that is called when the value changes, as the user drags. | | `onValueChangeEnd` | `(details: ValueChangeDetails) => void` | No | Handler that is called when the user stops dragging. | | `open` | `boolean` | No | The controlled open state of the color picker | | `openAutoFocus` | `boolean` | No | Whether to auto focus the color picker when it is opened | | `positioning` | `PositioningOptions` | No | The positioning options for the color picker | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `readOnly` | `boolean` | No | Whether the color picker is read-only | | `required` | `boolean` | No | Whether the color picker is required | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | | `value` | `Color` | No | The controlled color value of the color picker | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | root | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-invalid]` | Present when invalid | **Root CSS Variables:** | Variable | Description | |----------|-------------| | `--value` | The current value | **AreaBackground Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **AreaBackground Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | area-background | | `[data-invalid]` | Present when invalid | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | **Area Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `xChannel` | `ColorChannel` | No | | | `yChannel` | `ColorChannel` | No | | **Area Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | area | | `[data-invalid]` | Present when invalid | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | **AreaThumb Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **AreaThumb Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | area-thumb | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **AreaThumb CSS Variables:** | Variable | Description | |----------|-------------| | `--color` | The text color | **ChannelInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `channel` | `ExtendedColorChannel` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `orientation` | `Orientation` | No | | **ChannelInput Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | channel-input | | `[data-channel]` | The color channel of the channelinput | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **ChannelSliderLabel Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ChannelSliderLabel Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | channel-slider-label | | `[data-channel]` | The color channel of the channelsliderlabel | **ChannelSlider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `channel` | `ColorChannel` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `orientation` | `Orientation` | No | | **ChannelSlider Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | channel-slider | | `[data-channel]` | The color channel of the channelslider | | `[data-orientation]` | The orientation of the channelslider | **ChannelSliderThumb Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ChannelSliderThumb Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | channel-slider-thumb | | `[data-channel]` | The color channel of the channelsliderthumb | | `[data-disabled]` | Present when disabled | | `[data-orientation]` | The orientation of the channelsliderthumb | **ChannelSliderTrack Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ChannelSliderTrack Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | channel-slider-track | | `[data-channel]` | The color channel of the channelslidertrack | | `[data-orientation]` | The orientation of the channelslidertrack | **ChannelSliderValueText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ChannelSliderValueText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | channel-slider-value-text | | `[data-channel]` | The color channel of the channelslidervaluetext | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | content | | `[data-placement]` | The placement of the content | | `[data-nested]` | popover | | `[data-has-nested]` | popover | | `[data-state]` | "open" | "closed" | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | | `--nested-layer-count` | The number of nested color-pickers | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | control | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-invalid]` | Present when invalid | | `[data-state]` | "open" | "closed" | | `[data-focus]` | Present when focused | **EyeDropperTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **EyeDropperTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | eye-dropper-trigger | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **FormatSelect Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **FormatTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **HiddenInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | label | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | | `[data-focus]` | Present when focused | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner CSS Variables:** | Variable | Description | |----------|-------------| | `--reference-width` | The width of the reference element | | `--reference-height` | The height of the root | | `--available-width` | The available width in viewport | | `--available-height` | The available height in viewport | | `--x` | The x position for transform | | `--y` | The y position for transform | | `--z-index` | The z-index value | | `--transform-origin` | The transform origin for animations | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseColorPickerReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **SwatchGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **SwatchIndicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Swatch Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `string | Color` | Yes | The color value | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `respectAlpha` | `boolean` | No | Whether to include the alpha channel in the color | **Swatch Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | swatch | | `[data-state]` | "checked" | "unchecked" | | `[data-value]` | The value of the item | **Swatch CSS Variables:** | Variable | Description | |----------|-------------| | `--color` | The text color | **SwatchTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `string | Color` | Yes | The color value | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `disabled` | `boolean` | No | Whether the swatch trigger is disabled | **SwatchTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | swatch-trigger | | `[data-state]` | "checked" | "unchecked" | | `[data-value]` | The value of the item | | `[data-disabled]` | Present when disabled | **SwatchTrigger CSS Variables:** | Variable | Description | |----------|-------------| | `--color` | The text color | **TransparencyGrid Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `size` | `string` | No | | **TransparencyGrid CSS Variables:** | Variable | Description | |----------|-------------| | `--size` | The size (width and height) of the element | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | trigger | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-invalid]` | Present when invalid | | `[data-placement]` | The placement of the trigger | | `[data-state]` | "open" | "closed" | | `[data-focus]` | Present when focused | **ValueSwatch Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `respectAlpha` | `boolean` | No | Whether to include the alpha channel in the color | **ValueText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `format` | `ColorStringFormat` | No | | **ValueText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | color-picker | | `[data-part]` | value-text | | `[data-disabled]` | Present when disabled | | `[data-focus]` | Present when focused | **View Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `format` | `ColorFormat` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `dragging` | `boolean` | Whether the color picker is being dragged | | `open` | `boolean` | Whether the color picker is open | | `inline` | `boolean` | Whether the color picker is rendered inline | | `value` | `Color` | The current color value (as a string) | | `valueAsString` | `string` | The current color value (as a Color object) | | `setValue` | `(value: string | Color) => void` | Function to set the color value | | `getChannelValue` | `(channel: ColorChannel) => string` | Function to set the color value | | `getChannelValueText` | `(channel: ColorChannel, locale: string) => string` | Function to get the formatted and localized value of a specific channel | | `setChannelValue` | `(channel: ColorChannel, value: number) => void` | Function to set the color value of a specific channel | | `format` | `ColorFormat` | The current color format | | `setFormat` | `(format: ColorFormat) => void` | Function to set the color format | | `alpha` | `number` | The alpha value of the color | | `setAlpha` | `(value: number) => void` | Function to set the color alpha | | `setOpen` | `(open: boolean) => void` | Function to open or close the color picker | ## Accessibility ### Keyboard Support **`Enter`** Description: When focus is on the trigger, opens the color picker {'Label'} {'Additional Info'} {'Error Info'}
When focus is on a trigger of a swatch, selects the color (and closes the color picker)
When focus is on the input or channel inputs, selects the color **`ArrowLeft`** Description: When focus is on the color area, decreases the hue value of the color
When focus is on the channel sliders, decreases the value of the channel **`ArrowRight`** Description: When focus is on the color area, increases the hue value of the color
When focus is on the channel sliders, increases the value of the channel **`ArrowUp`** Description: When focus is on the color area, increases the saturation value of the color
When focus is on the channel sliders, increases the value of the channel **`ArrowDown`** Description: When focus is on the color area, decreases the saturation value of the color
When focus is on the channel sliders, decreases the value of the channel **`Esc`** Description: Closes the color picker and moves focus to the trigger # Combobox ## Anatomy ```tsx``` ## Examples **Example: basic** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component Basic() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems: [ { label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }, { label: 'Orange', value: 'orange' }, { label: 'Mango', value: 'mango' }, { label: 'Pineapple', value: 'pineapple' }, { label: 'Strawberry', value: 'strawberry' }, ], filter: @filterApi.contains, }, ); { filter(details.inputValue); }} > } ``` ### Auto Highlight Automatically highlight the first matching item as the user types by setting `inputBehavior="autohighlight"`. **Example: auto-highlight** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component AutoHighlight() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems: [ { label: 'Engineering', value: 'engineering' }, { label: 'Marketing', value: 'marketing' }, { label: 'Sales', value: 'sales' }, { label: 'Finance', value: 'finance' }, { label: 'Human Resources', value: 'hr' }, { label: 'Operations', value: 'operations' }, { label: 'Product', value: 'product' }, { label: 'Customer Success', value: 'customer-success' }, { label: 'Legal', value: 'legal' }, { label: 'Information Technology', value: 'information-technology' }, { label: 'Design', value: 'design' }, ], filter: @filterApi.contains, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); };{'Favorite Fruit'} for (const item of @collection.items; key item.value) { } {item.label} } ``` ### Inline Autocomplete Complete the input value with the first matching item by setting `inputBehavior="autocomplete"`. Use with `startsWith` filter for best results. **Example: inline-autocomplete** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component InlineAutocomplete() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems: [ { label: 'Whale', value: 'whale' }, { label: 'Dolphin', value: 'dolphin' }, { label: 'Shark', value: 'shark' }, { label: 'Octopus', value: 'octopus' }, { label: 'Jellyfish', value: 'jellyfish' }, { label: 'Seahorse', value: 'seahorse' }, ], filter: @filterApi.startsWith, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; {'Department'} {'No results found'} for (const item of @collection.items; key item.value) {} {item.label} } ``` ### Grouping To group related combobox items, use the `groupBy` prop on the collection and `collection.group()` to iterate the groups. **Example: grouping** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component Grouping() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems, filter: @filterApi.contains, groupBy: (item) => item.continent, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; {'Sea Creature'} {'No results found'} for (const item of @collection.items; key item.value) {} {item.label} } const initialItems = [ { label: 'Canada', value: 'ca', continent: 'North America' }, { label: 'United States', value: 'us', continent: 'North America' }, { label: 'Mexico', value: 'mx', continent: 'North America' }, { label: 'United Kingdom', value: 'uk', continent: 'Europe' }, { label: 'Germany', value: 'de', continent: 'Europe' }, { label: 'France', value: 'fr', continent: 'Europe' }, { label: 'Japan', value: 'jp', continent: 'Asia' }, { label: 'South Korea', value: 'kr', continent: 'Asia' }, { label: 'China', value: 'cn', continent: 'Asia' }, ]; ``` ### Field The `Field` component helps manage form-related state and accessibility attributes of a combobox. It includes handling ARIA labels, helper text, and error text to ensure proper accessibility. **Example: with-field** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { Field } from 'ark-ripple/field'; import { useFilter } from 'ark-ripple/locale'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/combobox.module.css'; const initialItems = [ { label: 'Engineering', value: 'engineering' }, { label: 'Design', value: 'design' }, { label: 'Marketing', value: 'marketing' }, { label: 'Sales', value: 'sales' }, { label: 'Human Resources', value: 'hr' }, { label: 'Finance', value: 'finance' }, ]; export component WithField() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems, filter: @filterApi.contains, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; {'Country'} for (const [continent, group] of @collection.group(); key continent) { } {continent} for (const item of group; key item.value) {} {item.label} } ``` ### Context Access the combobox's state with `Combobox.Context` or the `useComboboxContext` hook—useful for displaying the selected value or building custom UI. **Example: context** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component Context() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems: [ { label: 'Small', value: 'sm' }, { label: 'Medium', value: 'md' }, { label: 'Large', value: 'lg' }, { label: 'Extra Large', value: 'xl' }, ], filter: @filterApi.contains, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; {'Department'} for (const item of @collection.items; key item.value) { } {item.label} {'Select your primary department'} {'Department is required'} } ``` ### Root Provider An alternative way to control the combobox is to use the `RootProvider` component and the `useCombobox` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Combobox, useCombobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/combobox.module.css'; const initialItems = [ { label: 'Designer', value: 'designer' }, { label: 'Developer', value: 'developer' }, { label: 'Product Manager', value: 'pm' }, { label: 'Data Scientist', value: 'data-scientist' }, { label: 'DevOps Engineer', value: 'devops' }, { label: 'Marketing Lead', value: 'marketing' }, ]; export component RootProvider() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems, filter: @filterApi.contains, }, ); const combobox = useCombobox( { collection, onInputValueChange: (details) => { filter(details.inputValue); }, }, ); component children({ context }) { {'Selected: '} {@context.valueAsString || 'None'}
}{'Size'} for (const item of @collection.items; key item.value) { } {item.label} } ``` ### Links Use the `asChild` prop to render the combobox items as links. **Example: links** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component Links() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems, filter: @filterApi.contains, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); };{'Job Title'} for (const item of @collection.items; key item.value) { } {item.label} } const initialItems = [ { label: 'GitHub', href: 'https://github.com', value: 'github' }, { label: 'Stack Overflow', href: 'https://stackoverflow.com', value: 'stackoverflow' }, { label: 'MDN Web Docs', href: 'https://developer.mozilla.org', value: 'mdn' }, { label: 'npm', href: 'https://www.npmjs.com', value: 'npm' }, { label: 'TypeScript', href: 'https://www.typescriptlang.org', value: 'typescript' }, { label: 'React', href: 'https://react.dev', value: 'react' }, ]; ``` ### Rehydrate When a combobox has a `defaultValue` or `value` but the `collection` is not loaded yet, you can rehydrate the value to populate the input. **Example: rehydrate-value** ```ripple import { Combobox, useCombobox, useComboboxContext, useListCollection } from 'ark-ripple/combobox'; import { Portal } from 'ark-ripple/portal'; import { Check } from 'lucide-ripple'; import { track, effect } from 'ripple'; import styles from 'styles/combobox.module.css'; import { useAsync } from './use-async'; export component RehydrateValue() { let inputValue = track(''); let { collection, set } = useListCollection {'Developer Resources'} for (const item of @collection.items; key item.value) { component asChild({ propsFn }) { }{item.label} } ( { initialItems: [], itemToString: (item) => item.name, itemToValue: (item) => item.name, }, ); const combobox = useCombobox( { collection, defaultValue: ['C-3PO'], placeholder: 'Example: Dexter', inputValue, onInputValueChange: (e) => { @inputValue = e.inputValue; }, }, ); let state = useAsync(async (signal) => { const response = await fetch(`https://swapi.py4e.com/api/people/?search=${@inputValue}`, { signal, }); const data = await response.json(); set(data.results); }); effect(() => { void @inputValue; void state.load(); }); let hydrated = false; effect(() => { if (@combobox.value.length && @combobox.collection.size && !hydrated) { @combobox.syncSelectedItems(); hydrated = true; } }); } interface Character { name: string; height: string; mass: string; created: string; edited: string; url: string; } ``` ### Highlight Text Highlight the matching search text in combobox items based on the user's input. **Example: highlight-matching-text** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { Highlight } from 'ark-ripple/highlight'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component HighlightMatchingText() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems: [ { label: 'John Smith', value: 'john-smith' }, { label: 'Jane Doe', value: 'jane-doe' }, { label: 'Bob Johnson', value: 'bob-johnson' }, { label: 'Alice Williams', value: 'alice-williams' }, { label: 'Charlie Brown', value: 'charlie-brown' }, { label: 'Diana Ross', value: 'diana-ross' }, ], filter: @filterApi.contains, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; {'Search Star Wars Characters'} if (state.@loading) { {'Loading...'} } else if (state.@error) { {state.@error?.message} } else { for (const item of @collection.items; key item.name) { } } {item.name} {' - '} {item.height} {'cm / '} {item.mass} {'kg'} } ``` ### Dynamic Generate combobox items dynamically based on user input. This is useful for creating suggestions or autocomplete functionality. **Example: dynamic** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; const suggestList = ['gmail.com', 'yahoo.com', 'ark-ui.rip']; export component Dynamic() { let { collection, set } = useListCollection {'Assignee'} for (const item of @collection.items; key item.value) { } component children({ context }) { } ( { initialItems: [], }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { if (details.reason === 'input-change') { const items = suggestList.map((item) => `${details.inputValue}@${item}`); set(items); } }; } ``` ### Creatable Allow users to create new options when their search doesn't match any existing items. This is useful for tags, categories, or other custom values. **Example: creatable** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; interface Item { label: string; value: string; __new__?: boolean | undefined; } const NEW_OPTION_VALUE = '[[new]]'; const createNewOption = (value: string): Item => ({ label: value, value: NEW_OPTION_VALUE }); const isNewOptionValue = (value: string) => value === NEW_OPTION_VALUE; const replaceNewOptionValue = (values: string[], value: string) => values.map( (v) => (v === NEW_OPTION_VALUE ? value : v), ); const getNewOptionData = (inputValue: string): Item => ({ label: inputValue, value: inputValue, __new__: true, }); export component Creatable() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter, upsert, remove, update } = useListCollection {'Email'} for (const item of @collection.items; key item) { } {item} - ( { initialItems: [ { label: 'Bug', value: 'bug' }, { label: 'Feature', value: 'feature' }, { label: 'Enhancement', value: 'enhancement' }, { label: 'Documentation', value: 'docs' }, ], filter: @filterApi.contains, }, ); const isValidNewOption = (inputValue: string) => { const exactOptionMatch = @collection.filter((item) => item.toLowerCase() === inputValue.toLowerCase()).size > 0; return !exactOptionMatch && inputValue.trim().length > 0; }; let selectedValue = track
([]); let inputValue = track(''); { if (details.reason === 'input-change' || details.reason === 'item-select') { if (isValidNewOption(details.inputValue)) { upsert(NEW_OPTION_VALUE, createNewOption(details.inputValue)); } else if (details.inputValue.trim().length === 0) { remove(NEW_OPTION_VALUE); } filter(details.inputValue); } @inputValue = details.inputValue; }} onOpenChange={(details) => { if (details.reason === 'trigger-click') { filter(''); } }} onValueChange={(details) => { @selectedValue = replaceNewOptionValue(details.value, @inputValue); if (details.value.includes(NEW_OPTION_VALUE)) { update(NEW_OPTION_VALUE, getNewOptionData(@inputValue)); } }} > } ``` ### Multiple Selection Enable multiple selection by setting the `multiple` prop. Selected items can be displayed as tags above the input. **Example: multiple** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component Multiple() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter, remove } = useListCollection( { initialItems: [ { label: 'JavaScript', value: 'js' }, { label: 'TypeScript', value: 'ts' }, { label: 'Python', value: 'python' }, { label: 'Go', value: 'go' }, { label: 'Rust', value: 'rust' }, { label: 'Java', value: 'java' }, ], filter: @filterApi.contains, }, );{'Label'} for (const item of @collection.items; key item.value) { if (isNewOptionValue(item.value)) { }{'+ Create "'} {item.label} {'"'} } else {{item.label} {item.__new__ ? ' (new)' : ''} }{ filter(details.inputValue); }} onValueChange={(details) => { remove(...details.value); }} multiple > } ``` ### Async Search Load options asynchronously based on user input using the `useAsyncList` hook. This is useful for searching large datasets or fetching data from an API. **Example: async-search** ```ripple import { useAsyncList } from 'ark-ripple/collection'; import { Combobox, createListCollection } from 'ark-ripple/combobox'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, Loader, X } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/combobox.module.css'; interface Movie { id: string; title: string; year: number; director: string; genre: string; } export component AsyncSearch() { const list = useAsyncList{'Skills'} component children({ context }) { if (@context.selectedItems.length === 0) { {'None selected'} } for (const item of @context.selectedItems; key item.value) { {item.label} }}{'No skills found'} for (const item of @collection.items; key item.value) {} {item.label} ( { async load({ filterText, signal }: { filterText?: string; signal?: AbortSignal }) { if (!filterText) return { items: [] }; await new Promise((resolve) => setTimeout(resolve, 300)); if (signal?.aborted) return { items: [] }; const items = allMovies.filter( (movie) => movie.title.toLowerCase().includes(filterText.toLowerCase()) || movie.director.toLowerCase().includes(filterText.toLowerCase()) || movie.genre.toLowerCase().includes(filterText.toLowerCase()), ); return { items }; }, }, ); const collection = track( () => createListCollection( { items: @list.items, itemToString: (item) => item.title, itemToValue: (item) => item.id, }, ), ); { if (details.reason === 'input-change') { @list.setFilterText(details.inputValue); } }} > } const allMovies: Movie[] = [ { id: 'inception', title: 'Inception', year: 2010, director: 'Christopher Nolan', genre: 'Sci-Fi', }, { id: 'the-dark-knight', title: 'The Dark Knight', year: 2008, director: 'Christopher Nolan', genre: 'Action', }, { id: 'pulp-fiction', title: 'Pulp Fiction', year: 1994, director: 'Quentin Tarantino', genre: 'Crime', }, { id: 'the-godfather', title: 'The Godfather', year: 1972, director: 'Francis Ford Coppola', genre: 'Crime', }, { id: 'forrest-gump', title: 'Forrest Gump', year: 1994, director: 'Robert Zemeckis', genre: 'Drama', }, { id: 'the-matrix', title: 'The Matrix', year: 1999, director: 'The Wachowskis', genre: 'Sci-Fi', }, { id: 'interstellar', title: 'Interstellar', year: 2014, director: 'Christopher Nolan', genre: 'Sci-Fi', }, { id: 'parasite', title: 'Parasite', year: 2019, director: 'Bong Joon-ho', genre: 'Thriller' }, { id: 'the-shawshank-redemption', title: 'The Shawshank Redemption', year: 1994, director: 'Frank Darabont', genre: 'Drama', }, { id: 'fight-club', title: 'Fight Club', year: 1999, director: 'David Fincher', genre: 'Drama' }, { id: 'goodfellas', title: 'Goodfellas', year: 1990, director: 'Martin Scorsese', genre: 'Crime', }, { id: 'the-silence-of-the-lambs', title: 'The Silence of the Lambs', year: 1991, director: 'Jonathan Demme', genre: 'Thriller', }, ]; ``` ### Virtualized For very large lists, use virtualization with `@tanstack/virtual` to render only the visible items. Pass the `scrollToIndexFn` prop to enable keyboard navigation within the virtualized list. **Example: virtualized** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; import { createVirtualizer } from '../../../utils/use-virtualizer.ripple'; import { flushSync, track } from 'ripple'; export component Virtualized() { let contentRef = track{'Movie'} if (@list.loading) { } else if (@list.error) {{'Searching...'} {@list.error.message}} else if (@list.items.length === 0) {{@list.filterText ? 'No results found' : 'Start typing to search movies...'}} else { for (const movie of @list.items; key movie.id) {} } {movie.title} {movie.year} {' · '} {movie.director} (null); const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter, reset } = useListCollection( { initialItems: countries, filter: @filterApi.startsWith, }, ); const virtualizer = createVirtualizer( { get count() { return @collection.size; }, getScrollElement: () => @contentRef, estimateSize: () => 32, overscan: 10, }, ); const handleScrollToIndex: Combobox.RootProps ['scrollToIndexFn'] = (details) => { flushSync(() => { virtualizer.scrollToIndex(details.index, { align: 'center', behavior: 'auto' }); }); }; const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; } interface Country { value: string; label: string; emoji: string; } const countries: Country[] = [ { value: 'AD', label: 'Andorra', emoji: '🇦🇩' }, { value: 'AE', label: 'United Arab Emirates', emoji: '🇦🇪' }, { value: 'AF', label: 'Afghanistan', emoji: '🇦🇫' }, { value: 'AG', label: 'Antigua and Barbuda', emoji: '🇦🇬' }, { value: 'AI', label: 'Anguilla', emoji: '🇦🇮' }, { value: 'AL', label: 'Albania', emoji: '🇦🇱' }, { value: 'AM', label: 'Armenia', emoji: '🇦🇲' }, { value: 'AO', label: 'Angola', emoji: '🇦🇴' }, { value: 'AQ', label: 'Antarctica', emoji: '🇦🇶' }, { value: 'AR', label: 'Argentina', emoji: '🇦🇷' }, { value: 'AS', label: 'American Samoa', emoji: '🇦🇸' }, { value: 'AT', label: 'Austria', emoji: '🇦🇹' }, { value: 'AU', label: 'Australia', emoji: '🇦🇺' }, { value: 'AW', label: 'Aruba', emoji: '🇦🇼' }, { value: 'AX', label: 'Åland Islands', emoji: '🇦🇽' }, { value: 'AZ', label: 'Azerbaijan', emoji: '🇦🇿' }, { value: 'BA', label: 'Bosnia and Herzegovina', emoji: '🇧🇦' }, { value: 'BB', label: 'Barbados', emoji: '🇧🇧' }, { value: 'BD', label: 'Bangladesh', emoji: '🇧🇩' }, { value: 'BE', label: 'Belgium', emoji: '🇧🇪' }, { value: 'BF', label: 'Burkina Faso', emoji: '🇧🇫' }, { value: 'BG', label: 'Bulgaria', emoji: '🇧🇬' }, { value: 'BH', label: 'Bahrain', emoji: '🇧🇭' }, { value: 'BI', label: 'Burundi', emoji: '🇧🇮' }, { value: 'BJ', label: 'Benin', emoji: '🇧🇯' }, { value: 'BL', label: 'Saint Barthélemy', emoji: '🇧🇱' }, { value: 'BM', label: 'Bermuda', emoji: '🇧🇲' }, { value: 'BN', label: 'Brunei', emoji: '🇧🇳' }, { value: 'BO', label: 'Bolivia', emoji: '🇧🇴' }, { value: 'BR', label: 'Brazil', emoji: '🇧🇷' }, { value: 'BS', label: 'Bahamas', emoji: '🇧🇸' }, { value: 'BT', label: 'Bhutan', emoji: '🇧🇹' }, { value: 'BW', label: 'Botswana', emoji: '🇧🇼' }, { value: 'BY', label: 'Belarus', emoji: '🇧🇾' }, { value: 'BZ', label: 'Belize', emoji: '🇧🇿' }, { value: 'CA', label: 'Canada', emoji: '🇨🇦' }, { value: 'CD', label: 'Congo', emoji: '🇨🇩' }, { value: 'CF', label: 'Central African Republic', emoji: '🇨🇫' }, { value: 'CH', label: 'Switzerland', emoji: '🇨🇭' }, { value: 'CI', label: 'Côte d\'Ivoire', emoji: '🇨🇮' }, { value: 'CK', label: 'Cook Islands', emoji: '🇨🇰' }, { value: 'CL', label: 'Chile', emoji: '🇨🇱' }, { value: 'CM', label: 'Cameroon', emoji: '🇨🇲' }, { value: 'CN', label: 'China', emoji: '🇨🇳' }, { value: 'CO', label: 'Colombia', emoji: '🇨🇴' }, { value: 'CR', label: 'Costa Rica', emoji: '🇨🇷' }, { value: 'CU', label: 'Cuba', emoji: '🇨🇺' }, { value: 'CV', label: 'Cabo Verde', emoji: '🇨🇻' }, { value: 'CY', label: 'Cyprus', emoji: '🇨🇾' }, { value: 'CZ', label: 'Czech Republic', emoji: '🇨🇿' }, { value: 'DE', label: 'Germany', emoji: '🇩🇪' }, { value: 'DJ', label: 'Djibouti', emoji: '🇩🇯' }, { value: 'DK', label: 'Denmark', emoji: '🇩🇰' }, { value: 'DM', label: 'Dominica', emoji: '🇩🇲' }, { value: 'DO', label: 'Dominican Republic', emoji: '🇩🇴' }, { value: 'DZ', label: 'Algeria', emoji: '🇩🇿' }, { value: 'EC', label: 'Ecuador', emoji: '🇪🇨' }, { value: 'EE', label: 'Estonia', emoji: '🇪🇪' }, { value: 'EG', label: 'Egypt', emoji: '🇪🇬' }, { value: 'ER', label: 'Eritrea', emoji: '🇪🇷' }, { value: 'ES', label: 'Spain', emoji: '🇪🇸' }, { value: 'ET', label: 'Ethiopia', emoji: '🇪🇹' }, { value: 'FI', label: 'Finland', emoji: '🇫🇮' }, { value: 'FJ', label: 'Fiji', emoji: '🇫🇯' }, { value: 'FK', label: 'Falkland Islands', emoji: '🇫🇰' }, { value: 'FM', label: 'Micronesia', emoji: '🇫🇲' }, { value: 'FO', label: 'Faroe Islands', emoji: '🇫🇴' }, { value: 'FR', label: 'France', emoji: '🇫🇷' }, { value: 'GA', label: 'Gabon', emoji: '🇬🇦' }, { value: 'GB', label: 'United Kingdom', emoji: '🇬🇧' }, { value: 'GD', label: 'Grenada', emoji: '🇬🇩' }, { value: 'GE', label: 'Georgia', emoji: '🇬🇪' }, { value: 'GH', label: 'Ghana', emoji: '🇬🇭' }, { value: 'GI', label: 'Gibraltar', emoji: '🇬🇮' }, { value: 'GL', label: 'Greenland', emoji: '🇬🇱' }, { value: 'GM', label: 'Gambia', emoji: '🇬🇲' }, { value: 'GN', label: 'Guinea', emoji: '🇬🇳' }, { value: 'GQ', label: 'Equatorial Guinea', emoji: '🇬🇶' }, { value: 'GR', label: 'Greece', emoji: '🇬🇷' }, { value: 'GT', label: 'Guatemala', emoji: '🇬🇹' }, { value: 'GU', label: 'Guam', emoji: '🇬🇺' }, { value: 'GW', label: 'Guinea-Bissau', emoji: '🇬🇼' }, { value: 'GY', label: 'Guyana', emoji: '🇬🇾' }, { value: 'HK', label: 'Hong Kong', emoji: '🇭🇰' }, { value: 'HN', label: 'Honduras', emoji: '🇭🇳' }, { value: 'HR', label: 'Croatia', emoji: '🇭🇷' }, { value: 'HT', label: 'Haiti', emoji: '🇭🇹' }, { value: 'HU', label: 'Hungary', emoji: '🇭🇺' }, { value: 'ID', label: 'Indonesia', emoji: '🇮🇩' }, { value: 'IE', label: 'Ireland', emoji: '🇮🇪' }, { value: 'IL', label: 'Israel', emoji: '🇮🇱' }, { value: 'IM', label: 'Isle of Man', emoji: '🇮🇲' }, { value: 'IN', label: 'India', emoji: '🇮🇳' }, { value: 'IQ', label: 'Iraq', emoji: '🇮🇶' }, { value: 'IR', label: 'Iran', emoji: '🇮🇷' }, { value: 'IS', label: 'Iceland', emoji: '🇮🇸' }, { value: 'IT', label: 'Italy', emoji: '🇮🇹' }, { value: 'JE', label: 'Jersey', emoji: '🇯🇪' }, { value: 'JM', label: 'Jamaica', emoji: '🇯🇲' }, { value: 'JO', label: 'Jordan', emoji: '🇯🇴' }, { value: 'JP', label: 'Japan', emoji: '🇯🇵' }, { value: 'KE', label: 'Kenya', emoji: '🇰🇪' }, { value: 'KG', label: 'Kyrgyzstan', emoji: '🇰🇬' }, { value: 'KH', label: 'Cambodia', emoji: '🇰🇭' }, { value: 'KI', label: 'Kiribati', emoji: '🇰🇮' }, { value: 'KM', label: 'Comoros', emoji: '🇰🇲' }, { value: 'KN', label: 'Saint Kitts and Nevis', emoji: '🇰🇳' }, { value: 'KP', label: 'North Korea', emoji: '🇰🇵' }, { value: 'KR', label: 'South Korea', emoji: '🇰🇷' }, { value: 'KW', label: 'Kuwait', emoji: '🇰🇼' }, { value: 'KY', label: 'Cayman Islands', emoji: '🇰🇾' }, { value: 'KZ', label: 'Kazakhstan', emoji: '🇰🇿' }, { value: 'LA', label: 'Laos', emoji: '🇱🇦' }, { value: 'LB', label: 'Lebanon', emoji: '🇱🇧' }, { value: 'LC', label: 'Saint Lucia', emoji: '🇱🇨' }, { value: 'LI', label: 'Liechtenstein', emoji: '🇱🇮' }, { value: 'LK', label: 'Sri Lanka', emoji: '🇱🇰' }, { value: 'LR', label: 'Liberia', emoji: '🇱🇷' }, { value: 'LS', label: 'Lesotho', emoji: '🇱🇸' }, { value: 'LT', label: 'Lithuania', emoji: '🇱🇹' }, { value: 'LU', label: 'Luxembourg', emoji: '🇱🇺' }, { value: 'LV', label: 'Latvia', emoji: '🇱🇻' }, { value: 'LY', label: 'Libya', emoji: '🇱🇾' }, { value: 'MA', label: 'Morocco', emoji: '🇲🇦' }, { value: 'MC', label: 'Monaco', emoji: '🇲🇨' }, { value: 'MD', label: 'Moldova', emoji: '🇲🇩' }, { value: 'ME', label: 'Montenegro', emoji: '🇲🇪' }, { value: 'MG', label: 'Madagascar', emoji: '🇲🇬' }, { value: 'MH', label: 'Marshall Islands', emoji: '🇲🇭' }, { value: 'MK', label: 'North Macedonia', emoji: '🇲🇰' }, { value: 'ML', label: 'Mali', emoji: '🇲🇱' }, { value: 'MM', label: 'Myanmar', emoji: '🇲🇲' }, { value: 'MN', label: 'Mongolia', emoji: '🇲🇳' }, { value: 'MO', label: 'Macao', emoji: '🇲🇴' }, { value: 'MR', label: 'Mauritania', emoji: '🇲🇷' }, { value: 'MS', label: 'Montserrat', emoji: '🇲🇸' }, { value: 'MT', label: 'Malta', emoji: '🇲🇹' }, { value: 'MU', label: 'Mauritius', emoji: '🇲🇺' }, { value: 'MV', label: 'Maldives', emoji: '🇲🇻' }, { value: 'MW', label: 'Malawi', emoji: '🇲🇼' }, { value: 'MX', label: 'Mexico', emoji: '🇲🇽' }, { value: 'MY', label: 'Malaysia', emoji: '🇲🇾' }, { value: 'MZ', label: 'Mozambique', emoji: '🇲🇿' }, { value: 'NA', label: 'Namibia', emoji: '🇳🇦' }, { value: 'NC', label: 'New Caledonia', emoji: '🇳🇨' }, { value: 'NE', label: 'Niger', emoji: '🇳🇪' }, { value: 'NF', label: 'Norfolk Island', emoji: '🇳🇫' }, { value: 'NG', label: 'Nigeria', emoji: '🇳🇬' }, { value: 'NI', label: 'Nicaragua', emoji: '🇳🇮' }, { value: 'NL', label: 'Netherlands', emoji: '🇳🇱' }, { value: 'NO', label: 'Norway', emoji: '🇳🇴' }, { value: 'NP', label: 'Nepal', emoji: '🇳🇵' }, { value: 'NR', label: 'Nauru', emoji: '🇳🇷' }, { value: 'NU', label: 'Niue', emoji: '🇳🇺' }, { value: 'NZ', label: 'New Zealand', emoji: '🇳🇿' }, { value: 'OM', label: 'Oman', emoji: '🇴🇲' }, { value: 'PA', label: 'Panama', emoji: '🇵🇦' }, { value: 'PE', label: 'Peru', emoji: '🇵🇪' }, { value: 'PF', label: 'French Polynesia', emoji: '🇵🇫' }, { value: 'PG', label: 'Papua New Guinea', emoji: '🇵🇬' }, { value: 'PH', label: 'Philippines', emoji: '🇵🇭' }, { value: 'PK', label: 'Pakistan', emoji: '🇵🇰' }, { value: 'PL', label: 'Poland', emoji: '🇵🇱' }, { value: 'PR', label: 'Puerto Rico', emoji: '🇵🇷' }, { value: 'PS', label: 'Palestine', emoji: '🇵🇸' }, { value: 'PT', label: 'Portugal', emoji: '🇵🇹' }, { value: 'PW', label: 'Palau', emoji: '🇵🇼' }, { value: 'PY', label: 'Paraguay', emoji: '🇵🇾' }, { value: 'QA', label: 'Qatar', emoji: '🇶🇦' }, { value: 'RO', label: 'Romania', emoji: '🇷🇴' }, { value: 'RS', label: 'Serbia', emoji: '🇷🇸' }, { value: 'RU', label: 'Russia', emoji: '🇷🇺' }, { value: 'RW', label: 'Rwanda', emoji: '🇷🇼' }, { value: 'SA', label: 'Saudi Arabia', emoji: '🇸🇦' }, { value: 'SB', label: 'Solomon Islands', emoji: '🇸🇧' }, { value: 'SC', label: 'Seychelles', emoji: '🇸🇨' }, { value: 'SD', label: 'Sudan', emoji: '🇸🇩' }, { value: 'SE', label: 'Sweden', emoji: '🇸🇪' }, { value: 'SG', label: 'Singapore', emoji: '🇸🇬' }, { value: 'SI', label: 'Slovenia', emoji: '🇸🇮' }, { value: 'SK', label: 'Slovakia', emoji: '🇸🇰' }, { value: 'SL', label: 'Sierra Leone', emoji: '🇸🇱' }, { value: 'SM', label: 'San Marino', emoji: '🇸🇲' }, { value: 'SN', label: 'Senegal', emoji: '🇸🇳' }, { value: 'SO', label: 'Somalia', emoji: '🇸🇴' }, { value: 'SR', label: 'Suriname', emoji: '🇸🇷' }, { value: 'SS', label: 'South Sudan', emoji: '🇸🇸' }, { value: 'ST', label: 'Sao Tome and Principe', emoji: '🇸🇹' }, { value: 'SV', label: 'El Salvador', emoji: '🇸🇻' }, { value: 'SY', label: 'Syria', emoji: '🇸🇾' }, { value: 'SZ', label: 'Eswatini', emoji: '🇸🇿' }, { value: 'TC', label: 'Turks and Caicos Islands', emoji: '🇹🇨' }, { value: 'TD', label: 'Chad', emoji: '🇹🇩' }, { value: 'TG', label: 'Togo', emoji: '🇹🇬' }, { value: 'TH', label: 'Thailand', emoji: '🇹🇭' }, { value: 'TJ', label: 'Tajikistan', emoji: '🇹🇯' }, { value: 'TK', label: 'Tokelau', emoji: '🇹🇰' }, { value: 'TL', label: 'Timor-Leste', emoji: '🇹🇱' }, { value: 'TM', label: 'Turkmenistan', emoji: '🇹🇲' }, { value: 'TN', label: 'Tunisia', emoji: '🇹🇳' }, { value: 'TO', label: 'Tonga', emoji: '🇹🇴' }, { value: 'TR', label: 'Türkiye', emoji: '🇹🇷' }, { value: 'TT', label: 'Trinidad and Tobago', emoji: '🇹🇹' }, { value: 'TV', label: 'Tuvalu', emoji: '🇹🇻' }, { value: 'TW', label: 'Taiwan', emoji: '🇹🇼' }, { value: 'TZ', label: 'Tanzania', emoji: '🇹🇿' }, { value: 'UA', label: 'Ukraine', emoji: '🇺🇦' }, { value: 'UG', label: 'Uganda', emoji: '🇺🇬' }, { value: 'US', label: 'United States', emoji: '🇺🇸' }, { value: 'UY', label: 'Uruguay', emoji: '🇺🇾' }, { value: 'UZ', label: 'Uzbekistan', emoji: '🇺🇿' }, { value: 'VA', label: 'Vatican City', emoji: '🇻🇦' }, { value: 'VC', label: 'Saint Vincent and the Grenadines', emoji: '🇻🇨' }, { value: 'VE', label: 'Venezuela', emoji: '🇻🇪' }, { value: 'VG', label: 'British Virgin Islands', emoji: '🇻🇬' }, { value: 'VI', label: 'U.S. Virgin Islands', emoji: '🇻🇮' }, { value: 'VN', label: 'Vietnam', emoji: '🇻🇳' }, { value: 'VU', label: 'Vanuatu', emoji: '🇻🇺' }, { value: 'WF', label: 'Wallis and Futuna', emoji: '🇼🇫' }, { value: 'WS', label: 'Samoa', emoji: '🇼🇸' }, { value: 'YE', label: 'Yemen', emoji: '🇾🇪' }, { value: 'YT', label: 'Mayotte', emoji: '🇾🇹' }, { value: 'ZA', label: 'South Africa', emoji: '🇿🇦' }, { value: 'ZM', label: 'Zambia', emoji: '🇿🇲' }, { value: 'ZW', label: 'Zimbabwe', emoji: '🇿🇼' }, ]; ``` ### Custom Object Use the `itemToString` and `itemToValue` props to map custom objects to the required interface. **Example: custom-object** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; export component CustomObject() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems: [ { country: 'United States', code: 'US', flag: '🇺🇸' }, { country: 'Canada', code: 'CA', flag: '🇨🇦' }, { country: 'Australia', code: 'AU', flag: '🇦🇺' }, ], itemToString: (item) => item.country, itemToValue: (item) => item.code, filter: @filterApi.contains, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; {'Country'} { @contentRef = el; return () => { @contentRef = null; }; }} class={styles.Scroller} >for (const virtualItem of virtualizer.getVirtualItems(); key ((@collection.items[virtualItem.index])).value) { let item = @collection.items[virtualItem.index];} {item.emoji} {item.label} } ``` ### Limit Results Use the `limit` property on `useListCollection` to limit the number of rendered items in the DOM. **Example: limit-results** ```ripple import { Combobox, useListCollection } from 'ark-ripple/combobox'; import { useFilter } from 'ark-ripple/locale'; import { Portal } from 'ark-ripple/portal'; import { Check, ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/combobox.module.css'; const cities = [ { label: 'New York', value: 'new-york' }, { label: 'Los Angeles', value: 'los-angeles' }, { label: 'Chicago', value: 'chicago' }, { label: 'Houston', value: 'houston' }, { label: 'Phoenix', value: 'phoenix' }, { label: 'Philadelphia', value: 'philadelphia' }, { label: 'San Antonio', value: 'san-antonio' }, { label: 'San Diego', value: 'san-diego' }, { label: 'Dallas', value: 'dallas' }, { label: 'San Jose', value: 'san-jose' }, { label: 'Austin', value: 'austin' }, { label: 'Jacksonville', value: 'jacksonville' }, { label: 'Fort Worth', value: 'fort-worth' }, { label: 'Columbus', value: 'columbus' }, { label: 'Charlotte', value: 'charlotte' }, { label: 'San Francisco', value: 'san-francisco' }, { label: 'Indianapolis', value: 'indianapolis' }, { label: 'Seattle', value: 'seattle' }, { label: 'Denver', value: 'denver' }, { label: 'Boston', value: 'boston' }, ]; export component LimitResults() { const filterApi = useFilter({ sensitivity: 'base' }); let { collection, filter } = useListCollection( { initialItems: cities, limit: 5, filter: @filterApi.contains, }, ); const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue); }; {'Country'} for (const item of @collection.items; key item.code) { } {item.flag} {' '} {item.country} } ``` ## Guides ### Router Links Customize the `navigate` prop on `Combobox.Root` to integrate with your router.: ```tsx import { Combobox } from 'ark-ripple/combobox' import { useNavigate } from ' {'City'} for (const item of @collection.items; key item.value) { } {item.label} ' function Demo() { const navigate = useNavigate() return ( { navigate({ to: e.node.href }) }} > {/* ... */} ) } ``` ### Custom Objects By default, the combobox collection expects an array of objects with `label` and `value` properties. In some cases, you may need to deal with custom objects. Use the `itemToString` and `itemToValue` props to map the custom object to the required interface. ```tsx const items = [ { country: 'United States', code: 'US', flag: '🇺🇸' }, { country: 'Canada', code: 'CA', flag: '🇨🇦' }, { country: 'Australia', code: 'AU', flag: '🇦🇺' }, // ... ] const { collection } = useListCollection({ initialItems: items, itemToString: (item) => item.country, itemToValue: (item) => item.code, }) ``` ### Large Datasets The recommended way of managing large lists is to use the `limit` property on the `useListCollection` hook. This will limit the number of rendered items in the DOM to improve performance. ```tsx {3} const { collection } = useListCollection({ initialItems: items, limit: 10, }) ``` ### Available Size The following css variables are exposed to the `Combobox.Positioner` which you can use to style the `Combobox.Content` ```css /* width of the combobox control */ --reference-width:; /* width of the available viewport */ --available-width: ; /* height of the available viewport */ --available-height: ; ``` For example, if you want to make sure the maximum height doesn't exceed the available height, you can use the following: ```css [data-scope='combobox'][data-part='content'] { max-height: calc(var(--available-height) - 100px); } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `collection` | `ListCollection ` | Yes | The collection of items | | `allowCustomValue` | `boolean` | No | Whether to allow typing custom values in the input | | `alwaysSubmitOnEnter` | `boolean` | No | Whether to always submit on Enter key press, even if popup is open. Useful for single-field autocomplete forms where Enter should submit the form. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoFocus` | `boolean` | No | Whether to autofocus the input on mount | | `closeOnSelect` | `boolean` | No | Whether to close the combobox when an item is selected. | | `composite` | `boolean` | No | Whether the combobox is a composed with other composite widgets like tabs | | `defaultHighlightedValue` | `string` | No | The initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox. | | `defaultInputValue` | `string` | No | The initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input. | | `defaultOpen` | `boolean` | No | The initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox. | | `defaultValue` | `string[]` | No | The initial value of the combobox's selected items when rendered. Use when you don't need to control the value of the combobox's selected items. | | `disabled` | `boolean` | No | Whether the combobox is disabled | | `disableLayer` | `boolean` | No | Whether to disable registering this a dismissable layer | | `form` | `string` | No | The associate form of the combobox. | | `highlightedValue` | `string` | No | The controlled highlighted value of the combobox | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string label: string control: string input: string content: string trigger: string clearTrigger: string item: (id: string, index?: number | undefined) => string positioner: string itemGroup: (id: string | number) => string itemGroupLabel: (id: string | number) => string }>` | No | The ids of the elements in the combobox. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `inputBehavior` | `'none' | 'autohighlight' | 'autocomplete'` | No | Defines the auto-completion behavior of the combobox. - `autohighlight`: The first focused item is highlighted as the user types - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated | | `inputValue` | `string` | No | The controlled value of the combobox's input | | `invalid` | `boolean` | No | Whether the combobox is invalid | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `loopFocus` | `boolean` | No | Whether to loop the keyboard navigation through the items | | `multiple` | `boolean` | No | Whether to allow multiple selection. **Good to know:** When `multiple` is `true`, the `selectionBehavior` is automatically set to `clear`. It is recommended to render the selected items in a separate container. | | `name` | `string` | No | The `name` attribute of the combobox's input. Useful for form submission | | `navigate` | `(details: NavigateDetails) => void` | No | Function to navigate to the selected item | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component | | `onHighlightChange` | `(details: HighlightChangeDetails ) => void` | No | Function called when an item is highlighted using the pointer or keyboard navigation. | | `onInputValueChange` | `(details: InputValueChangeDetails) => void` | No | Function called when the input's value changes | | `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function called when the popup is opened | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component | | `onSelect` | `(details: SelectionDetails) => void` | No | Function called when an item is selected | | `onValueChange` | `(details: ValueChangeDetails ) => void` | No | Function called when a new item is selected | | `open` | `boolean` | No | The controlled open state of the combobox | | `openOnChange` | `boolean | ((details: InputValueChangeDetails) => boolean)` | No | Whether to show the combobox when the input value changes | | `openOnClick` | `boolean` | No | Whether to open the combobox popup on initial click on the input | | `openOnKeyPress` | `boolean` | No | Whether to open the combobox on arrow key press | | `placeholder` | `string` | No | The placeholder text of the combobox's input | | `positioning` | `PositioningOptions` | No | The positioning options to dynamically position the menu | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `readOnly` | `boolean` | No | Whether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with it | | `required` | `boolean` | No | Whether the combobox is required | | `scrollToIndexFn` | `(details: ScrollToIndexDetails) => void` | No | Function to scroll to a specific index | | `selectionBehavior` | `'clear' | 'replace' | 'preserve'` | No | The behavior of the combobox input when an item is selected - `replace`: The selected item string is set as the input value - `clear`: The input value is cleared - `preserve`: The input value is preserved | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `translations` | `IntlTranslations` | No | Specifies the localized strings that identifies the accessibility elements and their states | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | | `value` | `string[]` | No | The controlled value of the combobox's selected items | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | root | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **ClearTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ClearTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | clear-trigger | | `[data-invalid]` | Present when invalid | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | content | | `[data-state]` | "open" | "closed" | | `[data-nested]` | listbox | | `[data-has-nested]` | listbox | | `[data-placement]` | The placement of the content | | `[data-empty]` | Present when the content is empty | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | | `--nested-layer-count` | The number of nested comboboxs | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | control | | `[data-state]` | "open" | "closed" | | `[data-focus]` | Present when focused | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | **Empty Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | input | | `[data-invalid]` | Present when invalid | | `[data-autofocus]` | | | `[data-state]` | "open" | "closed" | **ItemGroupLabel Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemGroup Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | item-group | | `[data-empty]` | Present when the content is empty | **ItemIndicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemIndicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | item-indicator | | `[data-state]` | "checked" | "unchecked" | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `item` | `any` | No | The item to render | | `persistFocus` | `boolean` | No | Whether hovering outside should clear the highlighted state | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | item | | `[data-highlighted]` | Present when highlighted | | `[data-state]` | "checked" | "unchecked" | | `[data-disabled]` | Present when disabled | | `[data-value]` | The value of the item | **ItemText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | item-text | | `[data-state]` | "checked" | "unchecked" | | `[data-disabled]` | Present when disabled | | `[data-highlighted]` | Present when highlighted | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | label | | `[data-readonly]` | Present when read-only | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | | `[data-focus]` | Present when focused | **List Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **List Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | list | | `[data-empty]` | Present when the content is empty | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner CSS Variables:** | Variable | Description | |----------|-------------| | `--reference-width` | The width of the reference element | | `--reference-height` | The height of the root | | `--available-width` | The available width in viewport | | `--available-height` | The available height in viewport | | `--x` | The x position for transform | | `--y` | The y position for transform | | `--z-index` | The z-index value | | `--transform-origin` | The transform origin for animations | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseComboboxReturn ` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `focusable` | `boolean` | No | Whether the trigger is focusable | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | combobox | | `[data-part]` | trigger | | `[data-state]` | "open" | "closed" | | `[data-invalid]` | Present when invalid | | `[data-focusable]` | | | `[data-readonly]` | Present when read-only | | `[data-disabled]` | Present when disabled | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `focused` | `boolean` | Whether the combobox is focused | | `open` | `boolean` | Whether the combobox is open | | `inputValue` | `string` | The value of the combobox input | | `highlightedValue` | `string` | The value of the highlighted item | | `highlightedItem` | `V` | The highlighted item | | `setHighlightValue` | `(value: string) => void` | The value of the combobox input | | `clearHighlightValue` | `VoidFunction` | Function to clear the highlighted value | | `syncSelectedItems` | `VoidFunction` | Function to sync the selected items with the value. Useful when `value` is updated from async sources. | | `selectedItems` | `V[]` | The selected items | | `hasSelectedItems` | `boolean` | Whether there's a selected item | | `value` | `string[]` | The selected item keys | | `valueAsString` | `string` | The string representation of the selected items | | `selectValue` | `(value: string) => void` | Function to select a value | | `setValue` | `(value: string[]) => void` | Function to set the value of the combobox | | `clearValue` | `(value?: string) => void` | Function to clear the value of the combobox | | `focus` | `VoidFunction` | Function to focus on the combobox input | | `setInputValue` | `(value: string, reason?: InputValueChangeReason) => void` | Function to set the input value of the combobox | | `getItemState` | `(props: ItemProps) => ItemState` | Returns the state of a combobox item | | `setOpen` | `(open: boolean, reason?: OpenChangeReason) => void` | Function to open or close the combobox | | `collection` | `ListCollection ` | Function to toggle the combobox | | `reposition` | `(options?: Partial ) => void` | Function to set the positioning options | | `multiple` | `boolean` | Whether the combobox allows multiple selections | | `disabled` | `boolean` | Whether the combobox is disabled | ## Accessibility Complies with the [Combobox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/). ### Keyboard Support **`ArrowDown`** Description: When the combobox is closed, opens the listbox and highlights to the first option. When the combobox is open, moves focus to the next option. **`ArrowUp`** Description: When the combobox is closed, opens the listbox and highlights to the last option. When the combobox is open, moves focus to the previous option. **`Home`** Description: When the combobox is open, moves focus to the first option. **`End`** Description: When the combobox is open, moves focus to the last option. **`Escape`** Description: Closes the listbox. **`Enter`** Description: Selects the highlighted option and closes the combobox. **`Esc`** Description: Closes the combobox # Date Picker ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component Basic() { } ``` ### Default Value Use the `defaultValue` prop with `parseDate` to set the initial date value. **Example: default-value** ```ripple import { DatePicker, parseDate } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component DefaultValue() { {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Controlled Use the `value` and `onValueChange` props to control the date picker's value programmatically. **Example: controlled** ```ripple import { DatePicker, parseDate } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component Controlled() { let value = track([parseDate('2022-01-01')]); {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} { @value = e.value; }} > } ``` ### Root Provider An alternative way to control the date picker is to use the `RootProvider` component and the `useDatePicker` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { DatePicker, useDatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component RootProvider() { const datePicker = useDatePicker();{'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Default View Use the `defaultView` prop to set which view (day, month, or year) the calendar opens to initially. **Example: default-view** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component DefaultView() {{'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid( { columns: 4 }, ); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Month and Year Select Use `MonthSelect` and `YearSelect` components to create a header with dropdown selects for quick month/year navigation, alongside the prev/next triggers. **Example: month-year-select** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component MonthYearSelect() { {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Range To create a date picker that allows a range selection, you need to: - Set the `selectionMode` prop to `range`. - Render multiple inputs with the `index` prop set to `0` and `1`. **Example: range-selection** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component RangeSelection() { {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} } ``` ### Multiple Use the `selectionMode="multiple"` prop to allow selecting multiple dates. This example also shows how to display selected dates as removable tags. **Example: multi-selection** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { type DateValue } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight, X } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; const formatWithDay = (date: DateValue) => new Intl.DateTimeFormat('en', { dateStyle: 'medium', }).format(date.toDate('UTC')); export component MultiSelection() { {'Label'} {'Clear'} {'Last 7 days'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Max Selected Dates Use the `maxSelectedDates` prop with `selectionMode="multiple"` to limit the number of dates that can be selected. In this example, users can select up to 3 dates. **Example: max-selected-dates** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/date-picker.module.css'; export component MaxSelectedDates() { {'Label'} component children({ context }) { for (const date of @context.value; key date.toString()) { {formatWithDay(date)} } } {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Multiple Months To create a date picker that displays multiple months side by side: - Set the `numOfMonths` prop to the number of months you want to display. - Use the `datePicker.getOffset({ months: 1 })` to get data for the next month. **Example: multiple-months** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component MultipleMonths() { {'Label'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} } ``` ### Presets Use the `DatePicker.PresetTrigger` component to add quick-select preset options like "Last 7 days" or "This month". **Example: presets** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component Presets() { {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { const offset = @context.getOffset({ months: 1 }); } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of offset.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} } ``` ### Min and Max Use the `min` and `max` props with `parseDate` to restrict the selectable date range. Dates outside this range will be disabled. **Example: min-max** ```ripple import { DatePicker, parseDate } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component MinMax() { {'Label'} {'Clear'} {'Last 7 days'} {'Last 14 days'} {'Last 30 days'} {'Last 90 days'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Unavailable Use the `isDateUnavailable` prop to mark specific dates as unavailable. This example disables weekends. **Example: unavailable** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { isWeekend } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component Unavailable() { {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} isWeekend(date, locale)} > } ``` ### Locale Use the `locale` prop to set the language and formatting, and `startOfWeek` to set the first day of the week (0 = Sunday, 1 = Monday, etc.). **Example: locale** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component Locale() {{'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Month Picker Create a month-only picker by setting `defaultView="month"` and `minView="month"`. Use custom `format` and `parse` functions to handle month/year input format. **Example: month-picker** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { CalendarDate, type DateValue } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; const format = (date: DateValue) => { const month = String(date.month).padStart(2, '0'); const year = String(date.year); return `${month}/${year}`; }; const parse = (value: string) => { const fullRegex = /^(\d{1,2})\/(\d{4})$/; const fullMatch = value.match(fullRegex); if (fullMatch) { const [, month, year] = fullMatch.map(Number); return new CalendarDate(year, month, 1); } return undefined; }; export component MonthPicker() { {'Beschriftung'} {'Löschen'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Year Picker Create a year-only picker by setting `defaultView="year"` and `minView="year"`. Use custom `format` and `parse` functions to handle year-only input format. **Example: year-picker** ```ripple import { DatePicker, parseDate } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { CalendarDate, type DateValue } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; const format = (date: DateValue) => String(date.year); const parse = (value: string) => { const fullRegex = /^(\d{4})$/; const fullMatch = value.match(fullRegex); if (fullMatch) { const [, year] = fullMatch.map(Number); return new CalendarDate(year, 1, 1); } return undefined; }; export component YearPicker() { {'Label'} {'Clear'} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Inline Use the `inline` prop to display the date picker directly on the page, without a popup. > When using the `inline` prop, omit the `Portal`, `Positioner`, and `Content` components to render the calendar inline > within your layout. **Example: inline** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/date-picker.module.css'; export component Inline() { {'Label'} {'Clear'} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Custom Parsing Use the `parse` prop to implement custom date parsing logic. This allows users to enter dates in flexible formats like "25/12" or "25/12/24" which are automatically converted to valid dates. **Example: format-parse** ```ripple import { DatePicker, parseDate } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { type DateValue } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; const format = (date: DateValue) => { const day = String(date.day).padStart(2, '0'); const month = String(date.month).padStart(2, '0'); const year = String(date.year); return `${day}/${month}/${year}`; }; const parse = (value: string) => { const fullRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/; const fullMatch = value.match(fullRegex); if (fullMatch) { const [, day, month, year] = fullMatch.map(Number); return parseDate(`${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`); } return undefined; }; export component FormatParse() { {'Label'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Month Picker Range Create a month range picker by combining `selectionMode="range"` with `defaultView="month"` and `minView="month"`. This is useful for selecting billing periods or date ranges by month. **Example: month-picker-range** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { CalendarDate, type DateValue } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; const format = (date: DateValue) => { const month = String(date.month).padStart(2, '0'); const year = String(date.year); return `${month}/${year}`; }; const parse = (value: string) => { const fullRegex = /^(\d{1,2})\/(\d{4})$/; const fullMatch = value.match(fullRegex); if (fullMatch) { const [, month, year] = fullMatch.map(Number); return new CalendarDate(year, month, 1); } return undefined; }; export component MonthPickerRange() { {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Year Range Create a year range picker by combining `selectionMode="range"` with `defaultView="year"` and `minView="year"`. This is useful for selecting multi-year periods. **Example: year-picker-range** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { CalendarDate, type DateValue } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; const format = (date: DateValue) => String(date.year); const parse = (value: string) => { const fullRegex = /^(\d{4})$/; const fullMatch = value.match(fullRegex); if (fullMatch) { const [, year] = fullMatch.map(Number); return new CalendarDate(year, 1, 1); } return undefined; }; export component YearPickerRange() { {'Label'} {'Clear'} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Select Today Use the `selectToday` method from the date picker context to add a "Today" button that quickly selects the current date. **Example: select-today** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component SelectToday() { {'Label'} {'Clear'} component children({ context }) { {@context.getDecade().start} {' - '} {@context.getDecade().end} } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Fixed Weeks Use the `fixedWeeks` prop to always display 6 weeks in the calendar, preventing layout shifts when navigating between months. **Example: fixed-weeks** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component FixedWeeks() { {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} } ``` ### Form Use the `name` prop to integrate the date picker with native HTML forms. The selected date value will be submitted as form data. This example also uses `isDateUnavailable` to disable weekends. **Example: form** ```ripple import { DatePicker } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { isWeekend } from '@internationalized/date'; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component Form() { } ``` ### With Time Integrate a time input with the date picker using `CalendarDateTime` from `@internationalized/date`. The time input updates the hour and minute of the selected date value. **Example: with-time** ```ripple import { DatePicker, type CalendarDateTime } from 'ark-ripple/date-picker'; import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import { ChevronLeft, ChevronRight, Calendar } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/date-picker.module.css'; export component WithTime() { let value = track {'Label'} {'Clear'} component children({ context }) { } for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} component children({ context }) { } for (const months of @context.getMonthsGrid( { columns: 4, format: 'short' }, ); key months[0].value) { for (const month of months; key month.value) { }} {month.label} component children({ context }) { } for (const years of @context.getYearsGrid({ columns: 4 }); key years[0].value) { for (const year of years; key year.value) { }} {year.label} ([]); { @value = e.value as CalendarDateTime[]; }} > } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `closeOnSelect` | `boolean` | No | Whether the calendar should close after the date selection is complete. This is ignored when the selection mode is `multiple`. | | `createCalendar` | `(identifier: CalendarIdentifier) => Calendar` | No | A function that creates a Calendar object for a given calendar identifier. Enables non-Gregorian calendar support (Persian, Buddhist, Islamic, etc.) without bundling all calendars by default. | | `defaultFocusedValue` | `DateValue` | No | The initial focused date when rendered. Use when you don't need to control the focused date of the date picker. | | `defaultOpen` | `boolean` | No | The initial open state of the date picker when rendered. Use when you don't need to control the open state of the date picker. | | `defaultValue` | `DateValue[]` | No | The initial selected date(s) when rendered. Use when you don't need to control the selected date(s) of the date picker. | | `defaultView` | `DateView` | No | The default view of the calendar | | `disabled` | `boolean` | No | Whether the calendar is disabled. | | `fixedWeeks` | `boolean` | No | Whether the calendar should have a fixed number of weeks. This renders the calendar with 6 weeks instead of 5 or 6. | | `focusedValue` | `DateValue` | No | The controlled focused date. | | `format` | `(date: DateValue, details: LocaleDetails) => string` | No | The format of the date to display in the input. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string; label: (index: number) => string; table: (id: string) => string; tableHeader: (id: string) => string; tableBody: (id: string) => string; tableRow: (id: string) => string; content: string; ... 10 more ...; positioner: string; }>` | No | The ids of the elements in the date picker. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `inline` | `boolean` | No | Whether to render the date picker inline | | `invalid` | `boolean` | No | Whether the date picker is invalid | | `isDateUnavailable` | `(date: DateValue, locale: string) => boolean` | No | Returns whether a date of the calendar is available. | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `locale` | `string` | No | The locale (BCP 47 language tag) to use when formatting the date. | | `max` | `DateValue` | No | The maximum date that can be selected. | | `maxSelectedDates` | `number` | No | The maximum number of dates that can be selected. This is only applicable when `selectionMode` is `multiple`. | | `maxView` | `DateView` | No | The maximum view of the calendar | | `min` | `DateValue` | No | The minimum date that can be selected. | | `minView` | `DateView` | No | The minimum view of the calendar | | `name` | `string` | No | The `name` attribute of the input element. | | `numOfMonths` | `number` | No | The number of months to display. | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onFocusChange` | `(details: FocusChangeDetails) => void` | No | Function called when the focused date changes. | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function called when the calendar opens or closes. | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Function called when the value changes. | | `onViewChange` | `(details: ViewChangeDetails) => void` | No | Function called when the view changes. | | `onVisibleRangeChange` | `(details: VisibleRangeChangeDetails) => void` | No | Function called when the visible range changes. | | `open` | `boolean` | No | The controlled open state of the date picker | | `openOnClick` | `boolean` | No | Whether to open the calendar when the input is clicked. | | `outsideDaySelectable` | `boolean` | No | Whether day outside the visible range can be selected. | | `parse` | `(value: string, details: LocaleDetails) => DateValue | undefined` | No | Function to parse the date from the input back to a DateValue. | | `placeholder` | `string` | No | The placeholder text to display in the input. | | `positioning` | `PositioningOptions` | No | The user provided options used to position the date picker content | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `readOnly` | `boolean` | No | Whether the calendar is read-only. | | `required` | `boolean` | No | Whether the date picker is required | | `selectionMode` | `SelectionMode` | No | The selection mode of the calendar. - `single` - only one date can be selected - `multiple` - multiple dates can be selected - `range` - a range of dates can be selected | | `showWeekNumbers` | `boolean` | No | Whether to show the week number column in the day view. | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `startOfWeek` | `number` | No | The first day of the week. `0` - Sunday `1` - Monday `2` - Tuesday `3` - Wednesday `4` - Thursday `5` - Friday `6` - Saturday | | `timeZone` | `string` | No | The time zone to use | | `translations` | `IntlTranslations` | No | The localized messages to use. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | | `value` | `DateValue[]` | No | The controlled selected date(s). | | `view` | `DateView` | No | The view of the calendar | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | root | | `[data-state]` | "open" | "closed" | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-empty]` | Present when the content is empty | **ClearTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | content | | `[data-state]` | "open" | "closed" | | `[data-nested]` | popover | | `[data-has-nested]` | popover | | `[data-placement]` | The placement of the content | | `[data-inline]` | Present when the content is inline | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | | `--nested-layer-count` | The number of nested date-pickers | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | control | | `[data-disabled]` | Present when disabled | | `[data-placeholder-shown]` | Present when placeholder is shown | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `fixOnBlur` | `boolean` | No | Whether to fix the input value on blur. | | `index` | `number` | No | The index of the input to focus. | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | input | | `[data-index]` | The index of the item | | `[data-state]` | "open" | "closed" | | `[data-placeholder-shown]` | Present when placeholder is shown | | `[data-invalid]` | Present when invalid | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | label | | `[data-state]` | "open" | "closed" | | `[data-index]` | The index of the item | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | **MonthSelect Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **NextTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **NextTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | next-trigger | | `[data-disabled]` | Present when disabled | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner CSS Variables:** | Variable | Description | |----------|-------------| | `--reference-width` | The width of the reference element | | `--reference-height` | The height of the root | | `--available-width` | The available width in viewport | | `--available-height` | The available height in viewport | | `--x` | The x position for transform | | `--y` | The y position for transform | | `--z-index` | The z-index value | | `--transform-origin` | The transform origin for animations | **PresetTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `PresetTriggerValue` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **PrevTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **PrevTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | prev-trigger | | `[data-disabled]` | Present when disabled | **RangeText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseDatePickerReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **TableBody Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **TableBody Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | table-body | | `[data-view]` | The view of the tablebody | | `[data-disabled]` | Present when disabled | **TableCell Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `number | DateValue` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `columns` | `number` | No | | | `disabled` | `boolean` | No | | | `visibleRange` | `VisibleRange` | No | | **TableCellTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **TableHead Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **TableHead Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | table-head | | `[data-view]` | The view of the tablehead | | `[data-disabled]` | Present when disabled | **TableHeader Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **TableHeader Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | table-header | | `[data-view]` | The view of the tableheader | | `[data-disabled]` | Present when disabled | **Table Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `columns` | `number` | No | | **Table Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | table | | `[data-columns]` | | | `[data-view]` | The view of the table | **TableRow Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **TableRow Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | table-row | | `[data-disabled]` | Present when disabled | | `[data-view]` | The view of the tablerow | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | trigger | | `[data-placement]` | The placement of the trigger | | `[data-state]` | "open" | "closed" | | `[data-placeholder-shown]` | Present when placeholder is shown | **ValueText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `placeholder` | `string` | No | Text to display when no date is selected. | | `separator` | `string` | No | The separator to use between multiple date values when using default rendering. | **ValueTextRender Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `index` | `number` | Yes | | | `remove` | `() => void` | Yes | | | `value` | `DateValue` | Yes | | | `valueAsString` | `string` | Yes | | **ViewControl Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ViewControl Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | view-control | | `[data-view]` | The view of the viewcontrol | **View Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `view` | `DateView` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **View Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | view | | `[data-view]` | The view of the view | **ViewTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ViewTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | view-trigger | | `[data-view]` | The view of the viewtrigger | **WeekNumberCell Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `week` | `DateValue[]` | Yes | | | `weekIndex` | `number` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **WeekNumberCell Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | week-number-cell | | `[data-view]` | The view of the weeknumbercell | | `[data-week-index]` | | | `[data-type]` | The type of the item | | `[data-disabled]` | Present when disabled | **WeekNumberHeaderCell Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **WeekNumberHeaderCell Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | date-picker | | `[data-part]` | week-number-header-cell | | `[data-view]` | The view of the weeknumberheadercell | | `[data-type]` | The type of the item | | `[data-disabled]` | Present when disabled | **YearSelect Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `focused` | `boolean` | Whether the input is focused | | `open` | `boolean` | Whether the date picker is open | | `disabled` | `boolean` | Whether the date picker is disabled | | `invalid` | `boolean` | Whether the date picker is invalid | | `readOnly` | `boolean` | Whether the date picker is read-only | | `inline` | `boolean` | Whether the date picker is rendered inline | | `numOfMonths` | `number` | The number of months to display | | `showWeekNumbers` | `boolean` | Whether the week number column is shown in the day view | | `selectionMode` | `SelectionMode` | The selection mode (single, multiple, or range) | | `maxSelectedDates` | `number` | The maximum number of dates that can be selected (only for multiple selection mode). | | `isMaxSelected` | `boolean` | Whether the maximum number of selected dates has been reached. | | `view` | `DateView` | The current view of the date picker | | `getWeekNumber` | `(week: DateValue[]) => number` | Returns the ISO 8601 week number (1-53) for the given week (array of dates). | | `getDaysInWeek` | `(week: number, from?: DateValue) => DateValue[]` | Returns an array of days in the week index counted from the provided start date, or the first visible date if not given. | | `getOffset` | `(duration: DateDuration) => DateValueOffset` | Returns the offset of the month based on the provided number of months. | | `getRangePresetValue` | `(value: DateRangePreset) => DateValue[]` | Returns the range of dates based on the provided date range preset. | | `getMonthWeeks` | `(from?: DateValue) => DateValue[][]` | Returns the weeks of the month from the provided date. Represented as an array of arrays of dates. | | `isUnavailable` | `(date: DateValue) => boolean` | Returns whether the provided date is available (or can be selected) | | `weeks` | `DateValue[][]` | The weeks of the month. Represented as an array of arrays of dates. | | `weekDays` | `WeekDay[]` | The days of the week. Represented as an array of strings. | | `visibleRange` | `VisibleRange` | The visible range of dates. | | `visibleRangeText` | `VisibleRangeText` | The human readable text for the visible range of dates. | | `value` | `DateValue[]` | The selected date. | | `valueAsDate` | `Date[]` | The selected date as a Date object. | | `valueAsString` | `string[]` | The selected date as a string. | | `focusedValue` | `DateValue` | The focused date. | | `focusedValueAsDate` | `Date` | The focused date as a Date object. | | `focusedValueAsString` | `string` | The focused date as a string. | | `selectToday` | `VoidFunction` | Sets the selected date to today. | | `setValue` | `(values: DateValue[]) => void` | Sets the selected date to the given date. | | `setTime` | `(time: Time, index?: number) => void` | Sets the time for a specific date value. Converts CalendarDate to CalendarDateTime if needed. | | `setFocusedValue` | `(value: DateValue) => void` | Sets the focused date to the given date. | | `clearValue` | `(options?: { focus?: boolean; }) => void` | Clears the selected date(s). | | `setOpen` | `(open: boolean) => void` | Function to open or close the calendar. | | `focusMonth` | `(month: number) => void` | Function to set the selected month. | | `focusYear` | `(year: number) => void` | Function to set the selected year. | | `getYears` | `() => Cell[]` | Returns the months of the year | | `getYearsGrid` | `(props?: YearGridProps) => YearGridValue` | Returns the years of the decade based on the columns. Represented as an array of arrays of years. | | `getDecade` | `() => Range{'Label'} component children({ context }) { for (const weekDay of @context.weekDays; key weekDay.short) { {weekDay.short} }for (const week of @context.weeks; key week[0].toString()) { for (const day of week; key day.toString()) { }} {day.day} { @value = [e.value as CalendarDateTime]; }} /> } ` | Returns the start and end years of the decade. | | `getMonths` | `(props?: MonthFormatOptions) => Cell[]` | Returns the months of the year | | `getMonthsGrid` | `(props?: MonthGridProps) => MonthGridValue` | Returns the months of the year based on the columns. Represented as an array of arrays of months. | | `format` | `(value: DateValue, opts?: Intl.DateTimeFormatOptions) => string` | Formats the given date value based on the provided options. | | `setView` | `(view: DateView) => void` | Sets the view of the date picker. | | `goToNext` | `VoidFunction` | Goes to the next month/year/decade. | | `goToPrev` | `VoidFunction` | Goes to the previous month/year/decade. | | `getDayTableCellState` | `(props: DayTableCellProps) => DayTableCellState` | Returns the state details for a given cell. | | `getMonthTableCellState` | `(props: TableCellProps) => TableCellState` | Returns the state details for a given month cell. | | `getYearTableCellState` | `(props: TableCellProps) => TableCellState` | Returns the state details for a given year cell. | ## Accessibility ### Keyboard Support **`ArrowLeft`** Description: Moves focus to the previous day within the current week. **`ArrowRight`** Description: Moves focus to the next day within the current week. **`ArrowUp`** Description: Moves focus to the same day of the week in the previous week. **`ArrowDown`** Description: Moves focus to the same day of the week in the next week. **`Home`** Description: Moves focus to the first day of the current month. **`End`** Description: Moves focus to the last day of the current month. **`PageUp`** Description: Moves focus to the same day of the month in the previous month. **`PageDown`** Description: Moves focus to the same day of the month in the next month. **`Enter`** Description: Selects the focused date and closes the date picker. **`Esc`** Description: Closes the date picker without selecting any date. # Dialog ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component Basic() { } ``` ### Controlled Manage the dialog state using the `open` and `onOpenChange` props. **Example: controlled** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component Controlled() { let open = track(false); {'Open Dialog'} {'✕'} {'Welcome Back'} {'Sign in to your account to continue.'} { @open = e.open; }} > } ``` ### Root Provider An alternative way to control the dialog is to use the `RootProvider` component and the `useDialog` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Dialog, useDialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component RootProvider() { const dialog = useDialog();{'Open Dialog'} {'✕'} {'Session Settings'} {'Manage your session preferences and security options.'} } ``` ### Alert Dialog For critical confirmations or destructive actions, use `role="alertdialog"`. Alert dialogs differ from regular dialogs in important ways: - **Automatic focus**: The close/cancel button receives focus when opened, prioritizing the safest action - **Requires explicit dismissal**: Cannot be closed by clicking outside, only via button clicks or Escape key **Example: alert-dialog** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component AlertDialog() {{'✕'} {'Controlled Externally'} {'This dialog is controlled via the useDialog hook.'} } ``` ### Lazy Mount Use `lazyMount` to render dialog content only when first opened. Combine with `unmountOnExit` to unmount when closed, freeing up resources. **Example: lazy-mount** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component LazyMount() { {'Delete Account'} {'Are you absolutely sure?'} {'This action cannot be undone. This will permanently delete your account and remove your data from our servers.'} {'Cancel'} } ``` ### Initial Focus Use `initialFocusEl` to control which element receives focus when the dialog opens. **Example: initial-focus** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import field from 'styles/field.module.css'; import styles from 'styles/dialog.module.css'; export component InitialFocus() { let inputEl: HTMLInputElement; {'Open Dialog'} {'✕'} {'Lazy Loaded'} {'This dialog content is only mounted when opened and unmounts on close.'} inputEl}> } ``` ### Final Focus Use `finalFocusEl` to control which element receives focus when the dialog closes. Defaults to the trigger element. **Example: final-focus** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component FinalFocus() { let finalRef: HTMLButtonElement;{'Open Dialog'} {'✕'} {'Edit Profile'} {'The first input will be focused when the dialog opens.'} { inputEl = el; }} class={field.Input} placeholder="Enter your name..." />} ``` ### Non-Modal Use `modal={false}` to allow interaction with elements outside the dialog. Disables focus trapping and scroll prevention. **Example: non-modal** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component NonModal() {finalRef}> {'Open Dialog'} {'✕'} {'Focus Redirect'} {'When this dialog closes, focus will return to the button above instead of the trigger.'} } ``` ### Inside Scroll Make the content area scrollable while keeping header and footer fixed using `maxHeight` and `overflow: auto`. **Example: inside-scroll** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; const CONTENT_SECTIONS = [ { title: '1. Acceptance of Terms', body: 'By accessing and using this service, you accept and agree to be bound by the terms and provisions of this agreement.', }, { title: '2. Use License', body: 'Permission is granted to temporarily use this service for personal, non-commercial purposes only. This is the grant of a license, not a transfer of title.', }, { title: '3. User Responsibilities', body: 'You are responsible for maintaining the confidentiality of your account and password. You agree to accept responsibility for all activities that occur under your account.', }, { title: '4. Privacy Policy', body: 'Your use of this service is also governed by our Privacy Policy. Please review our Privacy Policy, which also governs the site and informs users of our data collection practices.', }, { title: '5. Limitations', body: 'In no event shall we be liable for any damages arising out of the use or inability to use the materials on this service.', }, { title: '6. Revisions', body: 'We may revise these terms of service at any time without notice. By using this service you are agreeing to be bound by the then current version of these terms.', }, { title: '7. Governing Law', body: 'These terms and conditions are governed by and construed in accordance with applicable laws and you irrevocably submit to the exclusive jurisdiction of the courts.', }, ]; export component InsideScroll() { {'Open Non-Modal Dialog'} {'✕'} {'Non-Modal Dialog'} {'This dialog allows interaction with elements outside. You can click buttons, select text, and interact with the page behind it.'} } ``` ### Outside Scroll Make the positioner scrollable so the dialog can extend beyond the viewport. **Example: outside-scroll** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; const CONTENT_SECTIONS = [ { title: '1. Information We Collect', body: 'We collect information you provide directly, such as when you create an account, make a purchase, or contact us for support. This may include your name, email address, and payment information.', }, { title: '2. How We Use Your Information', body: 'We use the information we collect to provide and improve our services, process transactions, send communications, and personalize your experience.', }, { title: '3. Information Sharing', body: 'We do not sell your personal information. We may share information with service providers who assist in our operations, or when required by law.', }, { title: '4. Data Security', body: 'We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, or destruction.', }, { title: '5. Your Rights', body: 'You have the right to access, correct, or delete your personal information. You may also opt out of marketing communications at any time.', }, { title: '6. Cookies and Tracking', body: 'We use cookies and similar technologies to enhance your experience, analyze usage patterns, and deliver targeted content. You can manage cookie preferences in your browser settings.', }, { title: '7. Third-Party Services', body: 'Our service may contain links to third-party websites. We are not responsible for the privacy practices of these external sites.', }, { title: '8. Children Privacy', body: 'Our services are not directed to children under 13. We do not knowingly collect personal information from children without parental consent.', }, { title: '9. International Transfers', body: 'Your information may be transferred to and processed in countries other than your own. We ensure appropriate safeguards are in place for such transfers.', }, { title: '10. Changes to This Policy', body: 'We may update this privacy policy from time to time. We will notify you of significant changes by posting a notice on our website or sending you an email.', }, { title: '11. Data Retention', body: 'We retain your personal information for as long as necessary to fulfill the purposes outlined in this policy, unless a longer retention period is required by law.', }, { title: '12. Contact Us', body: 'If you have questions about this privacy policy or our data practices, please contact our privacy team through the support channels provided on our website.', }, ]; export component OutsideScroll() { let contentRef: HTMLDivElement | null = null; {'Open Dialog'} {'✕'} {'Terms of Service'} {'Please review our terms before continuing.'} for (const item of CONTENT_SECTIONS; key item.title) {} {item.title}
{item.body}
{'Decline'} {'Accept'} contentRef}> } ``` ### Context Access the dialog's state and methods with `Dialog.Context` or the `useDialogContext` hook. **Example: context** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component Context() {{'Open Dialog'} { contentRef = el; }} class={styles.Content} style={{ margin: '4rem auto', maxHeight: 'none' }} > {'✕'} {'Privacy Policy'} {'This layout allows the dialog to extend beyond the viewport while keeping the outer container scrollable.'} for (const item of CONTENT_SECTIONS; key item.title) {} {item.title}
{item.body}
} ``` ### Open from Menu Open a dialog imperatively from a menu item using the `onClick` handler. **Example: open-from-menu** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Menu } from 'ark-ripple/menu'; import { Portal } from 'ark-ripple/portal'; import { ChevronDown, X } from 'lucide-ripple'; import { track } from 'ripple'; import dialog from 'styles/dialog.module.css'; import menu from 'styles/menu.module.css'; export component OpenFromMenu() { let open = track(false); {'Open Dialog'} {'✕'} {'Status'} component children({ context }) { {'Dialog is '} {@context.open ? 'open' : 'closed'} } {'Actions'} {'Edit'} {'Duplicate'} { @open = true; }} > {'Delete...'} { @open = e.open; }} role="alertdialog" > } ``` ### Nested Nest dialogs within one another. The parent receives `data-has-nested` and `--nested-layer-count` CSS variable for styling effects like zoom-out: ```css [data-part='content'][data-has-nested] { transform: scale(calc(1 - var(--nested-layer-count) * 0.05)); } ``` **Example: nested** ```ripple import { Dialog, useDialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/dialog.module.css'; export component Nested() { const parentDialog = useDialog(); const childDialog = useDialog();{'Confirm Delete'} {'Are you sure you want to delete this item? This action cannot be undone.'} } ``` ### Confirmation Intercept close attempts to show confirmation prompts, preventing data loss from unsaved changes. **Example: confirmation** ```ripple import { Dialog, useDialog } from 'ark-ripple/dialog'; import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import field from 'styles/field.module.css'; import styles from 'styles/dialog.module.css'; export component Confirmation() { let formContent = track(''); let isParentDialogOpen = track(false); const confirmDialog = useDialog(); let parentDialogProps = track( () => ({ open: isParentDialogOpen, onOpenChange: (details) => { if (!details.open && @formContent.trim()) { @confirmDialog.setOpen(true); } else { @isParentDialogOpen = details.open; } }, }), ); const parentDialog = useDialog(parentDialogProps); const handleConfirmClose = () => { @formContent = ''; const childSetter = @confirmDialog.setOpen; const parentSetter = @parentDialog.setOpen; childSetter(false); parentSetter(false); };{'✕'} {'Parent Dialog'} {'This is the parent dialog.'} {'✕'} {'Nested Dialog'} {'This dialog is nested within the parent.'} } ``` ## Guides ### Close Behavior - `closeOnEscape={false}` - Prevent closing on Escape - `closeOnInteractOutside={false}` - Prevent closing on outside click For conditional control, use `onEscapeKeyDown` or `onInteractOutside` with `e.preventDefault()`. ### Z-Index Stacking Use the `--layer-index` CSS variable for z-index management of stacked dialogs: ```css [data-part='content'] { z-index: calc(var(--layer-index)); } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `aria-label` | `string` | No | Human readable label for the dialog, in event the dialog title is not rendered | | `closeOnEscape` | `boolean` | No | Whether to close the dialog when the escape key is pressed | | `closeOnInteractOutside` | `boolean` | No | Whether to close the dialog when the outside is clicked | | `defaultOpen` | `boolean` | No | The initial open state of the dialog when rendered. Use when you don't need to control the open state of the dialog. | | `finalFocusEl` | `() => MaybeElement` | No | Element to receive focus when the dialog is closed | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ trigger: string positioner: string backdrop: string content: string closeTrigger: string title: string description: string }>` | No | The ids of the elements in the dialog. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `initialFocusEl` | `() => MaybeElement` | No | Element to receive focus when the dialog is opened | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `modal` | `boolean` | No | Whether to prevent pointer interaction outside the element and hide all content below it | | `onEscapeKeyDown` | `(event: KeyboardEvent) => void` | No | Function called when the escape key is pressed | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component | | `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function to call when the dialog's open state changes | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component | | `onRequestDismiss` | `(event: LayerDismissEvent) => void` | No | Function called when this layer is closed due to a parent layer being closed | | `open` | `boolean` | No | The controlled open state of the dialog | | `persistentElements` | `(() => Element | null)[]` | No | Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `preventScroll` | `boolean` | No | Whether to prevent scrolling behind the dialog when it's opened | | `restoreFocus` | `boolean` | No | Whether to restore focus to the element that had focus before the dialog was opened | | `role` | `'dialog' | 'alertdialog'` | No | The dialog's role | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `trapFocus` | `boolean` | No | Whether to trap focus inside the dialog when it's opened | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Backdrop Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Backdrop Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | dialog | | `[data-part]` | backdrop | | `[data-state]` | "open" | "closed" | **Backdrop CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | **CloseTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | dialog | | `[data-part]` | content | | `[data-state]` | "open" | "closed" | | `[data-nested]` | dialog | | `[data-has-nested]` | dialog | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | | `--nested-layer-count` | The number of nested dialogs | **Description Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseDialogReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Title Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | dialog | | `[data-part]` | trigger | | `[data-state]` | "open" | "closed" | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `open` | `boolean` | Whether the dialog is open | | `setOpen` | `(open: boolean) => void` | Function to open or close the dialog | ## Accessibility Complies with the [Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/). ### Keyboard Support **`Enter`** Description: When focus is on the trigger, opens the dialog. **`Tab`** Description: Moves focus to the next focusable element within the content. Focus is trapped within the dialog. **`Shift + Tab`** Description: Moves focus to the previous focusable element. Focus is trapped within the dialog. **`Esc`** Description: Closes the dialog and moves focus to trigger or the defined final focus element # Editable ## Anatomy ```tsx{'✕'} {'Edit Content'} {'Make changes to your content. You\'ll be asked to confirm before closing if there are unsaved changes.'} {'✕'} {'Unsaved Changes'} {'You have unsaved changes. Are you sure you want to close without saving?'} ``` ## Examples **Example: basic** ```ripple import { Editable } from 'ark-ripple/editable'; import { Pencil } from 'lucide-ripple'; import styles from 'styles/editable.module.css'; export component Basic() { } ``` ### Controlled Use the `value` and `onValueChange` props to control the editable state. **Example: controlled** ```ripple import { Editable } from 'ark-ripple/editable'; import { Pencil } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/editable.module.css'; export component Controlled() { let value = track('Hello World'); {'Label'} { @value = e.value; }} > } ``` ### Root Provider An alternative way to control the editable is to use the `RootProvider` component and the `useEditable` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Editable, useEditable } from 'ark-ripple/editable'; import { Pencil } from 'lucide-ripple'; import styles from 'styles/editable.module.css'; export component RootProvider() { const editable = useEditable({ defaultValue: 'Hello World' });{'Label'} } ``` ### Context Access the editable's state with `Editable.Context` or the `useEditableContext` hook—great for showing keyboard hints when editing. **Example: context** ```ripple import { Editable } from 'ark-ripple/editable'; import { Pencil } from 'lucide-ripple'; import styles from 'styles/editable.module.css'; export component Context() { {'Label'} } ``` ### Controls In some cases, you might need to use custom controls to toggle the edit and read mode. We use the render prop pattern to provide access to the internal state of the component. **Example: controls** ```ripple import { Editable } from 'ark-ripple/editable'; import { Check, Pencil, X } from 'lucide-ripple'; import styles from 'styles/editable.module.css'; export component Controls() { {'Label'} component children({ context }) { if (@context.editing) { {'Enter to save, Esc to cancel'} } else { } } } ``` ### Textarea Use the `asChild` prop on `Editable.Input` to render a textarea for multi-line editing. **Example: textarea** ```ripple import { Editable } from 'ark-ripple/editable'; import styles from 'styles/editable.module.css'; export component WithTextarea() { {'Label'} component children({ context }) { if (@context.editing) { }} else { } } ``` ### Field The `Field` component helps manage form-related state and accessibility attributes of an editable. It includes handling ARIA labels, helper text, and error text to ensure proper accessibility. **Example: with-field** ```ripple import { Editable } from 'ark-ripple/editable'; import { Field } from 'ark-ripple/field'; import field from 'styles/field.module.css'; import styles from 'styles/editable.module.css'; export component WithField() { {'Description'} component asChild({ propsFn }) { } {'Press Cmd + Enter to save'}} ``` ## Guides ### Auto-resizing To auto-grow the editable as the content changes, set the `autoResize` prop to `true`. ```tsx {'Bio'} {'Click to edit your bio'} {'Bio is required'} {/*...*/} ``` ### Max Length Use the `maxLength` prop to set a maximum number of characters that can be entered into the editable. ```tsx{/*...*/} ``` ### Double Click The editable supports two modes of activating the "edit" state: - when the preview part is focused (with pointer or keyboard). - when the preview part is double-clicked. To change the mode to double-click, pass the prop `activationMode="dblclick"`. ```tsx{/*...*/} ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `activationMode` | `ActivationMode` | No | The activation mode for the preview element. - "focus" - Enter edit mode when the preview is focused - "dblclick" - Enter edit mode when the preview is double-clicked - "click" - Enter edit mode when the preview is clicked - "none" - Edit can be triggered programmatically only | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoResize` | `boolean` | No | Whether the editable should auto-resize to fit the content. | | `defaultEdit` | `boolean` | No | Whether the editable is in edit mode by default. | | `defaultValue` | `string` | No | The initial value of the editable when rendered. Use when you don't need to control the value of the editable. | | `disabled` | `boolean` | No | Whether the editable is disabled. | | `edit` | `boolean` | No | Whether the editable is in edit mode. | | `finalFocusEl` | `() => HTMLElement | null` | No | The element to receive focus when the editable is closed. | | `form` | `string` | No | The associate form of the underlying input. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string area: string label: string preview: string input: string control: string submitTrigger: string cancelTrigger: string editTrigger: string }>` | No | The ids of the elements in the editable. Useful for composition. | | `invalid` | `boolean` | No | Whether the input's value is invalid. | | `maxLength` | `number` | No | The maximum number of characters allowed in the editable | | `name` | `string` | No | The name attribute of the editable component. Used for form submission. | | `onEditChange` | `(details: EditChangeDetails) => void` | No | Function to call when the edit mode changes. | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component | | `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Function to call when the value changes. | | `onValueCommit` | `(details: ValueChangeDetails) => void` | No | Function to call when the value is committed. | | `onValueRevert` | `(details: ValueChangeDetails) => void` | No | Function to call when the value is reverted. | | `placeholder` | `string | { edit: string; preview: string }` | No | The placeholder text for the editable. | | `readOnly` | `boolean` | No | Whether the editable is read-only. | | `required` | `boolean` | No | Whether the editable is required. | | `selectOnFocus` | `boolean` | No | Whether to select the text in the input when it is focused. | | `submitMode` | `SubmitMode` | No | The action that triggers submit in the edit mode: - "enter" - Trigger submit when the enter key is pressed - "blur" - Trigger submit when the editable is blurred - "none" - No action will trigger submit. You need to use the submit button - "both" - Pressing `Enter` and blurring the input will trigger submit | | `translations` | `IntlTranslations` | No | The translations for the editable. | | `value` | `string` | No | The controlled value of the editable. | **Area Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Area Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | editable | | `[data-part]` | area | | `[data-focus]` | Present when focused | | `[data-disabled]` | Present when disabled | | `[data-placeholder-shown]` | Present when placeholder is shown | **CancelTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **EditTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | editable | | `[data-part]` | input | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-invalid]` | Present when invalid | | `[data-autoresize]` | | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | editable | | `[data-part]` | label | | `[data-focus]` | Present when focused | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | **Preview Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Preview Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | editable | | `[data-part]` | preview | | `[data-placeholder-shown]` | Present when placeholder is shown | | `[data-readonly]` | Present when read-only | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-autoresize]` | | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseEditableReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **SubmitTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `editing` | `boolean` | Whether the editable is in edit mode | | `empty` | `boolean` | Whether the editable value is empty | | `value` | `string` | The current value of the editable | | `valueText` | `string` | The current value of the editable, or the placeholder if the value is empty | | `setValue` | `(value: string) => void` | Function to set the value of the editable | | `clearValue` | `VoidFunction` | Function to clear the value of the editable | | `edit` | `VoidFunction` | Function to enter edit mode | | `cancel` | `VoidFunction` | Function to exit edit mode, and discard any changes | | `submit` | `VoidFunction` | Function to exit edit mode, and submit any changes | ## Accessibility ### Keyboard Support **`Enter`** Description: Saves the edited content and exits edit mode. **`Escape`** Description: Discards the changes and exits edit mode. # Field ## Anatomy ```tsx``` ## Examples The `Field` component provides contexts such as `invalid`, `disabled`, `required`, and `readOnly` for form elements. While most Ark UI components natively support these contexts, you can also use the `Field` component with standard HTML form elements. ### Input This example shows how to use the `Field` component with a standard input field. **Example: input** ```ripple import { Field } from 'ark-ripple/field'; import styles from 'styles/field.module.css'; export component Input() { } ``` ### Textarea This example illustrates how to use the `Field` component with a textarea element. **Example: textarea** ```ripple import { Field } from 'ark-ripple/field'; import styles from 'styles/field.module.css'; export component Textarea() { {'Label'} {'Some additional Info'} {'Error Info'} } ``` ### Textarea Autoresize Pass the `autoresize` prop to the `Textarea` component to enable automatic resizing as the user types. **Example: textarea-autoresize** ```ripple import { Field } from 'ark-ripple/field'; import styles from 'styles/field.module.css'; export component TextareaAutoresize() { {'Label'} {'Some additional Info'} {'Error Info'} } ``` ### Select This example demonstrates how to integrate the `Field` component with a select dropdown. **Example: select** ```ripple import { Field } from 'ark-ripple/field'; import styles from 'styles/field.module.css'; export component Select() { {'Label'} {'Some additional Info'} {'Error Info'} } ``` ### Checkbox This example demonstrates how to integrate the `Field` and `Checkbox` components. ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { Field } from 'ark-ripple/field'; import { Check, Minus } from 'lucide-ripple'; import styles from 'styles/checkbox.module.css'; import field from 'styles/field.module.css'; export component WithField() { {'Label'} {'Some additional Info'} {'Error Info'} } ``` ### Root Provider An alternative way to control the field is to use the `RootProvider` component and the `useField` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Field } from 'ark-ripple/field'; import { useField } from 'ark-ripple/field'; import styles from 'styles/field.module.css'; import button from 'styles/button.module.css'; import { track } from 'ripple'; export component RootProvider() { let invalid = track(false); const field = useField({ id: 'field', invalid }); {'Label'} {'Additional Info'} {'Error Info'} } ``` ### Custom Control Use the `Field.Context` or `useFieldContext` hook to access the internal state of the field.This can help you wire up custom controls with the `Field` component. **Example: custom-control** ```ripple import { Field } from 'ark-ripple/field'; import styles from 'styles/field.module.css'; export component CustomControl() {{'Label'} {'Some additional Info'} {'Error Info'} } ``` ## API Reference **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `disabled` | `boolean` | No | Indicates whether the field is disabled. | | `ids` | `ElementIds` | No | The ids of the field parts. | | `invalid` | `boolean` | No | Indicates whether the field is invalid. | | `readOnly` | `boolean` | No | Indicates whether the field is read-only. | | `required` | `boolean` | No | Indicates whether the field is required. | | `target` | `string` | No | The target field item value the label should point to. | **ErrorText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **HelperText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **RequiredIndicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `fallback` | `string | number | bigint | boolean | ReactElement {'Any Control'} component children({ context }) { } {'Uses getInputProps() for maximum flexibility'} {'This field has an error'} > | Iterable | ReactPortal | Promise<...>` | No | | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `{ ariaDescribedby: string | undefined; ids: { root: string; control: string; label: string; errorText: string; helperText: string; }; refs: { rootRef: RefObject ; }; ... 11 more ...; getRequiredIndicatorProps: () => Omit<...>; }` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Select Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Textarea Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoresize` | `boolean` | No | Whether the textarea should autoresize | # Fieldset ## Anatomy ```tsx ``` ## Examples The `Fieldset` component provides contexts such as `invalid` and `disabled` for form elements. While most Ark UI components natively support these contexts, you can also use the `Field` component with standard HTML form elements. ### Basic **Example: basic** ```ripple import { Field } from 'ark-ripple/field'; import { Fieldset } from 'ark-ripple/fieldset'; import field from 'styles/field.module.css'; import styles from 'styles/fieldset.module.css'; export component Basic() { } ``` ### Field This example demonstrates how to use the `Field` component with a standard input field within a `Fieldset`. **Example: with-field** ```ripple import { Field } from 'ark-ripple/field'; import { Fieldset } from 'ark-ripple/fieldset'; import field from 'styles/field.module.css'; import styles from 'styles/fieldset.module.css'; export component WithField() { {'Contact Details'} {'Name'} {'Email'} } ``` ### Checkbox This example shows how to use the `Fieldset` component with other Ark UI form elements like `Checkbox`. **Example: with-checkbox** ```ripple import { Checkbox } from 'ark-ripple/checkbox'; import { Fieldset } from 'ark-ripple/fieldset'; import { Check } from 'lucide-ripple'; import checkbox from 'styles/checkbox.module.css'; import styles from 'styles/fieldset.module.css'; export component WithCheckbox() { {'Personal Information'} {'First Name'} {'As it appears on your ID'} {'Last Name'} } ``` ### Root Provider An alternative way to control the fieldset is to use the `RootProvider` component and the `useFieldset` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Field } from 'ark-ripple/field'; import { Fieldset, useFieldset } from 'ark-ripple/fieldset'; import field from 'styles/field.module.css'; import styles from 'styles/fieldset.module.css'; export component RootProvider() { const fieldset = useFieldset(); {'Email Preferences'} {'Product updates'} {'Marketing emails'} } ``` ### Input with Select This example shows how to use the `Fieldset` component with `Field.Input` and `Select` to create a interactive phone input component. **Example: phone-input** ```ripple import { Fieldset } from 'ark-ripple/fieldset'; import { Field } from 'ark-ripple/field'; import { track } from 'ripple'; import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/fieldset.module.css'; import field from 'styles/field.module.css'; import select from 'styles/select.module.css'; const extensions = createListCollection( { items: [ { label: '+1', value: '+1' }, { label: '+44', value: '+44' }, { label: '+49', value: '+49' }, { label: '+41', value: '+41' }, ], }, ); export component PhoneInput() { let inputRef = track {'Contact Details'} {'Name'} {'Email'} (null); const focusInput = () => { setTimeout(() => { @inputRef?.focus(); }); }; } ``` ## API Reference **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `invalid` | `boolean` | No | Indicates whether the fieldset is invalid. | **ErrorText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **HelperText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Legend Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `{ refs: { rootRef: RefObject {'Mobile Number'} for (const item of extensions.items; key item.value) { } {item.label} { @inputRef = el; return () => { @inputRef = null; }; }} /> ; }; ids: { legend: string; errorText: string; helperText: string; }; disabled: boolean; invalid: boolean; getRootProps: () => Omit<...>; getLegendProps: () => Omit<...>; getHelperTextProps: () => Omit<...>; getErrorTextProps: () => Omit<...>; }` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | # File Upload ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { Paperclip, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component Basic() { } ``` ### Initial Files Use the `defaultAcceptedFiles` prop to set the initial files in the file upload component. **Example: initial-files** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { File as FileIcon, Paperclip, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component InitialFiles() { {'File Upload'} {' Choose file(s)'} component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } } ``` ### Clear Trigger Use the `ClearTrigger` to remove all uploaded files at once. **Example: clear-trigger** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { Paperclip, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component ClearTrigger() { {'File Upload'} {' Choose file(s)'} component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } } ``` ### Dropzone Use the `Dropzone` to enable drag-and-drop. It exposes a `data-dragging` attribute for styling. **Example: dropzone** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { File, Upload, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component Dropzone() { {'File Upload'} {' Choose file(s)'} {'Clear Files'} component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } } ``` ### Directory Upload Use the `directory` prop to upload entire folders. Access file paths through `file.webkitRelativePath`. **Example: directory-upload** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { File, Folder, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component DirectoryUpload() { {'File Upload'} {'Drag and drop files here'} {'or click to browse'}component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } } ``` > When uploading directories with many files, set `maxFiles` to a higher value or remove it entirely to prevent > rejections. ### Accepted File Types Use the `accept` prop to restrict file types. Accepts MIME types (`image/png`) or extensions (`.pdf`). **Example: accepted-file-types** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { CircleAlert, Image, Upload, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component AcceptedFileTypes() { {'Upload Folder'} {' Select Folder'} component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } {file.webkitRelativePath || file.name} } ``` ### Error Handling Set constraints with `maxFiles`, `maxFileSize`, `minFileSize`, and `accept`. Rejected files include error codes like `TOO_MANY_FILES`, `FILE_INVALID_TYPE`, `FILE_TOO_LARGE`, or `FILE_EXISTS`. **Example: error-handling** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import type { FileUploadFileError } from 'ark-ripple/file-upload'; import { CircleAlert, CircleCheck, File, Upload, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; const errorMessages: Record {'Upload Images (PNG and JPEG only)'} {'Drop your images here'} {'Only PNG and JPEG files'}component children({ context }) { if (@context.acceptedFiles.length > 0) { for (const file of @context.acceptedFiles; key file.name) { } if (@context.rejectedFiles.length > 0) {} for (const fileRejection of @context.rejectedFiles; key fileRejection.file.name) { } }} for (const error of fileRejection.errors; key error) {{error}}= { TOO_MANY_FILES: 'Too many files selected (max 3 allowed)', FILE_INVALID_TYPE: 'Invalid file type (only images and PDFs allowed)', FILE_TOO_LARGE: 'File too large (max 1MB)', FILE_TOO_SMALL: 'File too small (min 1KB)', FILE_INVALID: 'Invalid file', FILE_EXISTS: 'File already exists', }; export component ErrorHandling() { } ``` ### File Transformations Use `transformFiles` to process files before they're added. Useful for image compression, format conversion, or resizing. **Example: transform-files** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { compressAccurately } from 'image-conversion'; import { Image, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; const transformFiles = async (files: File[]) => { return Promise.all( files.map(async (file) => { if (file.type.startsWith('image/')) { try { const blob = await compressAccurately(file, 200); return new File([blob], file.name, { type: blob.type }); } catch (error) { console.error('Compression failed for:', file.name, error); return file; } } return file; }), ); }; export component TransformFiles() { {'Upload Documents'} {'Drop files here'} {'Images and PDFs, max 1MB each'}component children({ context }) { if (@context.acceptedFiles.length > 0) { } if (@context.rejectedFiles.length > 0) {{'Accepted Files'} for (const file of @context.acceptedFiles; key file.name) { } } }{'Rejected Files'} for (const fileRejection of @context.rejectedFiles; key fileRejection.file.name) { } for (const error of fileRejection.errors; key error) {{errorMessages[error as FileUploadFileError] || error}}} ``` ### Field Use `Field` to add helper text and error handling. **Example: with-field** ```ripple import { Field } from 'ark-ripple/field'; import { FileUpload } from 'ark-ripple/file-upload'; import { File, Upload, X } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/file-upload.module.css'; export component WithField() { {'Upload with Compression'} {' Choose Images'} component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } } ``` ### Root Provider An alternative way to control the file upload is to use the `RootProvider` component and the `useFileUpload` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { FileUpload, useFileUpload } from 'ark-ripple/file-upload'; import { File, Upload, X } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/file-upload.module.css'; export component RootProvider() { const fileUpload = useFileUpload({ maxFiles: 5 }); {'Attachments'} {'Drop files here'} {'or click to browse'}component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } {'Upload up to 5 files'} {'Please upload at least one file'} } ``` ### Pasting Files Use `setClipboardFiles` to enable pasting images from the clipboard. **Example: pasting-files** ```ripple import { FileUpload, useFileUpload } from 'ark-ripple/file-upload'; import { Clipboard, X } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/file-upload.module.css'; export component PastingFiles() { const fileUpload = useFileUpload({ maxFiles: 3, accept: 'image/*' });{'File Upload'} {'Drop files here'} {'or click to browse'}component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } } ``` ### Media Capture Use `capture` to access the device camera. Set to `"environment"` for back camera or `"user"` for front camera. **Example: media-capture** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { Camera, File, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component MediaCapture() { {'Upload with Paste'} } ``` ### Rejected Files Access `rejectedFiles` from the context to display validation errors. **Example: rejected-files** ```ripple import { FileUpload } from 'ark-ripple/file-upload'; import { CircleAlert, CircleCheck, Upload, X } from 'lucide-ripple'; import styles from 'styles/file-upload.module.css'; export component RejectedFiles() { {'Capture Photo'} {' Open Camera'} component children({ context }) { for (const file of @context.acceptedFiles; key file.name) { } } {file.webkitRelativePath || file.name} { console.log('Rejected files:', details); }} > } ``` ## Guides ### File Previews Use `ItemPreview` with type matching to show appropriate previews based on file format. - `type="image/*"`: Shows image thumbnails using `ItemPreviewImage` - `type="video/*"`: For video file previews - `type="application/pdf"`: For PDF files - `type=".*"`: Generic fallback for any file type ```tsx{'Upload Files (Max 2)'} {'Drop files here'} {'Maximum 2 files allowed'}component children({ context }) { if (@context.acceptedFiles.length > 0) { } if (@context.rejectedFiles.length > 0) {{'Accepted Files'} for (const file of @context.acceptedFiles; key file.name) { } } }{'Rejected Files'} for (const { file, errors } of @context.rejectedFiles; key file.name) { } for (const error of errors; key error) { {error} }``` ### Disable Dropzone To disable drag-and-drop functionality, set `allowDrop` to `false`. ```tsx {/* ... */} ``` ### Prevent Document Drop By default, we prevent accidental navigation when files are dropped outside the dropzone. Set `preventDocumentDrop` to `false` to disable this. ```tsx{/* ... */} ``` ### Prevent Double Open Use `disableClick` on `Dropzone` when delegating clicks to a nested `Trigger`. This prevents the file picker from opening twice. ```tsx``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `accept` | `Record Choose Files Drag files here| FileMimeType | FileMimeType[]` | No | The accept file types | | `acceptedFiles` | `File[]` | No | The controlled accepted files | | `allowDrop` | `boolean` | No | Whether to allow drag and drop in the dropzone element | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `capture` | `'user' | 'environment'` | No | The default camera to use when capturing media | | `defaultAcceptedFiles` | `File[]` | No | The default accepted files when rendered. Use when you don't need to control the accepted files of the input. | | `directory` | `boolean` | No | Whether to accept directories, only works in webkit browsers | | `disabled` | `boolean` | No | Whether the file input is disabled | | `ids` | `Partial<{ root: string dropzone: string hiddenInput: string trigger: string label: string item: (id: string) => string itemName: (id: string) => string itemSizeText: (id: string) => string itemPreview: (id: string) => string itemDeleteTrigger: (id: string) => string }>` | No | The ids of the elements. Useful for composition. | | `invalid` | `boolean` | No | Whether the file input is invalid | | `locale` | `string` | No | The current locale. Based on the BCP 47 definition. | | `maxFiles` | `number` | No | The maximum number of files | | `maxFileSize` | `number` | No | The maximum file size in bytes | | `minFileSize` | `number` | No | The minimum file size in bytes | | `name` | `string` | No | The name of the underlying file input | | `onFileAccept` | `(details: FileAcceptDetails) => void` | No | Function called when the file is accepted | | `onFileChange` | `(details: FileChangeDetails) => void` | No | Function called when the value changes, whether accepted or rejected | | `onFileReject` | `(details: FileRejectDetails) => void` | No | Function called when the file is rejected | | `preventDocumentDrop` | `boolean` | No | Whether to prevent the drop event on the document | | `readOnly` | `boolean` | No | Whether the file input is read-only | | `required` | `boolean` | No | Whether the file input is required | | `transformFiles` | `(files: File[]) => Promise ` | No | Function to transform the accepted files to apply transformations | | `translations` | `IntlTranslations` | No | The localized messages to use. | | `validate` | `(file: File, details: FileValidateDetails) => FileError[] | null` | No | Function to validate a file | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | root | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-dragging]` | Present when in the dragging state | **ClearTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ClearTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | clear-trigger | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | **Dropzone Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `disableClick` | `boolean` | No | Whether to disable the click event on the dropzone | **Dropzone Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | dropzone | | `[data-invalid]` | Present when invalid | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-dragging]` | Present when in the dragging state | **HiddenInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemDeleteTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemDeleteTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | item-delete-trigger | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-type]` | The type of the item | **ItemGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `type` | `ItemType` | No | | **ItemGroup Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | item-group | | `[data-disabled]` | Present when disabled | | `[data-type]` | The type of the item | **ItemName Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemName Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | item-name | | `[data-disabled]` | Present when disabled | | `[data-type]` | The type of the item | **ItemPreviewImage Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemPreviewImage Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | item-preview-image | | `[data-disabled]` | Present when disabled | | `[data-type]` | The type of the item | **ItemPreview Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `type` | `string` | No | The file type to match against. Matches all file types by default. | **ItemPreview Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | item-preview | | `[data-disabled]` | Present when disabled | | `[data-type]` | The type of the item | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `file` | `File` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | item | | `[data-disabled]` | Present when disabled | | `[data-type]` | The type of the item | **ItemSizeText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemSizeText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | item-size-text | | `[data-disabled]` | Present when disabled | | `[data-type]` | The type of the item | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | label | | `[data-disabled]` | Present when disabled | | `[data-required]` | Present when required | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseFileUploadReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | file-upload | | `[data-part]` | trigger | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-invalid]` | Present when invalid | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `dragging` | `boolean` | Whether the user is dragging something over the root element | | `focused` | `boolean` | Whether the user is focused on the dropzone element | | `disabled` | `boolean` | Whether the file input is disabled | | `readOnly` | `boolean` | Whether the file input is in read-only mode | | `transforming` | `boolean` | Whether files are currently being transformed via `transformFiles` | | `maxFilesReached` | `boolean` | Whether the maximum number of files has been reached | | `remainingFiles` | `number` | The number of files that can still be added | | `openFilePicker` | `VoidFunction` | Function to open the file dialog | | `deleteFile` | `(file: File, type?: ItemType) => void` | Function to delete the file from the list | | `acceptedFiles` | `File[]` | The accepted files that have been dropped or selected | | `rejectedFiles` | `FileRejection[]` | The files that have been rejected | | `setFiles` | `(files: File[]) => void` | Sets the accepted files | | `clearFiles` | `VoidFunction` | Clears the accepted files | | `clearRejectedFiles` | `VoidFunction` | Clears the rejected files | | `getFileSize` | `(file: File) => string` | Returns the formatted file size (e.g. 1.2MB) | | `createFileUrl` | `(file: File, cb: (url: string) => void) => VoidFunction` | Returns the preview url of a file. Returns a function to revoke the url. | | `setClipboardFiles` | `(dt: DataTransfer) => boolean` | Sets the clipboard files Returns `true` if the clipboard data contains files, `false` otherwise. | # Floating Panel ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { FloatingPanel } from 'ark-ripple/floating-panel'; import { Portal } from 'ark-ripple/portal'; import { GripVertical, Minus, Maximize2, ArrowDownLeft, X } from 'lucide-ripple'; import styles from 'styles/floating-panel.module.css'; export component Basic() { } ``` ### Controlled size To control the size of the floating panel programmatically, you can pass the `size` `onResize` prop to the machine. **Example: controlled-size** ```ripple import { FloatingPanel } from 'ark-ripple/floating-panel'; import { Portal } from 'ark-ripple/portal'; import { GripVertical, Minus, Maximize2, ArrowDownLeft, X } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/floating-panel.module.css'; export component ControlledSize() { let size = track({ width: 400, height: 300 }); {'Toggle Panel'} {'Floating Panel'} {'Some content'}
{ @size = e.size; }} > } ``` ### Controlled position To control the position of the floating panel programmatically, you can pass the `position` and `onPositionChange` prop to the machine. **Example: controlled-position** ```ripple import { FloatingPanel } from 'ark-ripple/floating-panel'; import { Portal } from 'ark-ripple/portal'; import { GripVertical, Minus, Maximize2, ArrowDownLeft, X } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/floating-panel.module.css'; export component ControlledPosition() { let position = track({ x: 200, y: 200 });{'Toggle Panel'} {'Floating Panel'} {'Some content'}
{ @position = e.position; }} > } ``` ### Anchor Position Use the `getAnchorPosition` function to compute the initial position of the floating panel. This function is called when the panel is opened and receives the `triggerRect` and `boundaryRect`. **Example: anchor-position** ```ripple import { FloatingPanel } from 'ark-ripple/floating-panel'; import { Portal } from 'ark-ripple/portal'; import { GripVertical, Minus, Maximize2, ArrowDownLeft, X } from 'lucide-ripple'; import styles from 'styles/floating-panel.module.css'; export component AnchorPosition() {{'Toggle Panel'} {'Floating Panel'} {'Some content'}
{ if (!triggerRect) return { x: 0, y: 0 }; return { x: triggerRect.x + triggerRect.width / 2, y: triggerRect.y + triggerRect.height / 2, }; }} > } ``` ### Open State To control the open state of the floating panel programmatically, you can pass the `open` and `onOpenChange` prop to the machine. **Example: controlled-open** ```ripple import { FloatingPanel } from 'ark-ripple/floating-panel'; import { Portal } from 'ark-ripple/portal'; import { GripVertical, Minus, Maximize2, ArrowDownLeft, X } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/floating-panel.module.css'; export component ControlledOpen() { let open = track(false);{'Toggle Panel'} {'Floating Panel'} {'Some content'}
{ @open = e.open; }} > } ``` ### Lazy Mount To lazy mount the floating panel, you can pass the `lazyMount` prop to the machine. **Example: lazy-mount** ```ripple import { FloatingPanel } from 'ark-ripple/floating-panel'; import { Portal } from 'ark-ripple/portal'; import { GripVertical, Minus, Maximize2, ArrowDownLeft, X } from 'lucide-ripple'; import styles from 'styles/floating-panel.module.css'; export component LazyMount() {{'Toggle Panel'} {'Floating Panel'} {'Some content'}
console.log('onExitComplete invoked')}> } ``` ### Context To access the context of the floating panel, you can use the `useFloatingPanelContext` hook or the `FloatingPanel.Context` component. **Example: context** ```ripple import { FloatingPanel } from 'ark-ripple/floating-panel'; import { Portal } from 'ark-ripple/portal'; import { GripVertical, Minus, Maximize2, ArrowDownLeft, X } from 'lucide-ripple'; import styles from 'styles/floating-panel.module.css'; export component Context() {{'Toggle Panel'} {'Floating Panel'} {'Some content'}
} ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `allowOverflow` | `boolean` | No | Whether the panel should be strictly contained within the boundary when dragging | | `closeOnEscape` | `boolean` | No | Whether the panel should close when the escape key is pressed | | `defaultOpen` | `boolean` | No | The initial open state of the panel when rendered. Use when you don't need to control the open state of the panel. | | `defaultPosition` | `Point` | No | The initial position of the panel when rendered. Use when you don't need to control the position of the panel. | | `defaultSize` | `Size` | No | The default size of the panel | | `dir` | `'ltr' | 'rtl'` | No | The document's text/writing direction. | | `disabled` | `boolean` | No | Whether the panel is disabled | | `draggable` | `boolean` | No | Whether the panel is draggable | | `getAnchorPosition` | `(details: AnchorPositionDetails) => Point` | No | Function that returns the initial position of the panel when it is opened. If provided, will be used instead of the default position. | | `getBoundaryEl` | `() => HTMLElement | null` | No | The boundary of the panel. Useful for recalculating the boundary rect when the it is resized. | | `gridSize` | `number` | No | The snap grid for the panel | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ trigger: string; positioner: string; content: string; title: string; header: string }>` | No | The ids of the elements in the floating panel. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `lockAspectRatio` | `boolean` | No | Whether the panel is locked to its aspect ratio | | `maxSize` | `Size` | No | The maximum size of the panel | | `minSize` | `Size` | No | The minimum size of the panel | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function called when the panel is opened or closed | | `onPositionChange` | `(details: PositionChangeDetails) => void` | No | Function called when the position of the panel changes via dragging | | `onPositionChangeEnd` | `(details: PositionChangeDetails) => void` | No | Function called when the position of the panel changes via dragging ends | | `onSizeChange` | `(details: SizeChangeDetails) => void` | No | Function called when the size of the panel changes via resizing | | `onSizeChangeEnd` | `(details: SizeChangeDetails) => void` | No | Function called when the size of the panel changes via resizing ends | | `onStageChange` | `(details: StageChangeDetails) => void` | No | Function called when the stage of the panel changes | | `open` | `boolean` | No | The controlled open state of the panel | | `persistRect` | `boolean` | No | Whether the panel size and position should be preserved when it is closed | | `position` | `Point` | No | The controlled position of the panel | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `resizable` | `boolean` | No | Whether the panel is resizable | | `size` | `Size` | No | The size of the panel | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `strategy` | `'absolute' | 'fixed'` | No | The strategy to use for positioning | | `translations` | `IntlTranslations` | No | The translations for the floating panel. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Body Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Body Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | body | | `[data-dragging]` | Present when in the dragging state | | `[data-minimized]` | Present when minimized | | `[data-maximized]` | Present when maximized | | `[data-staged]` | Present when not in default stage | **CloseTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | content | | `[data-state]` | "open" | "closed" | | `[data-dragging]` | Present when in the dragging state | | `[data-topmost]` | Present when topmost | | `[data-behind]` | Present when not topmost | | `[data-minimized]` | Present when minimized | | `[data-maximized]` | Present when maximized | | `[data-staged]` | Present when not in default stage | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | control | | `[data-disabled]` | Present when disabled | | `[data-stage]` | The stage of the control | | `[data-minimized]` | Present when minimized | | `[data-maximized]` | Present when maximized | | `[data-staged]` | Present when not in default stage | **DragTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **DragTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | drag-trigger | | `[data-disabled]` | Present when disabled | **Header Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Header Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | header | | `[data-dragging]` | Present when in the dragging state | | `[data-topmost]` | Present when topmost | | `[data-behind]` | Present when not topmost | | `[data-minimized]` | Present when minimized | | `[data-maximized]` | Present when maximized | | `[data-staged]` | Present when not in default stage | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner CSS Variables:** | Variable | Description | |----------|-------------| | `--width` | The width of the element | | `--height` | The height of the element | | `--x` | The x position for transform | | `--y` | The y position for transform | | `--reference-width` | The width of the reference element | | `--reference-height` | The height of the root | | `--available-width` | The available width in viewport | | `--available-height` | The available height in viewport | | `--z-index` | The z-index value | | `--transform-origin` | The transform origin for animations | **ResizeTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `axis` | `ResizeTriggerAxis` | Yes | The axis of the resize handle | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ResizeTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | resize-trigger | | `[data-disabled]` | Present when disabled | | `[data-axis]` | The axis to resize | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseFloatingPanelReturn` | Yes | | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **StageTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `stage` | `Stage` | Yes | The stage of the panel | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **StageTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | stage-trigger | | `[data-stage]` | The stage of the stagetrigger | **Title Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | floating-panel | | `[data-part]` | trigger | | `[data-state]` | "open" | "closed" | | `[data-dragging]` | Present when in the dragging state | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `open` | `boolean` | Whether the panel is open | | `setOpen` | `(open: boolean) => void` | Function to open or close the panel | | `dragging` | `boolean` | Whether the panel is being dragged | | `resizing` | `boolean` | Whether the panel is being resized | | `position` | `Point` | The position of the panel | | `setPosition` | `(position: Point) => void` | Function to set the position of the panel | | `size` | `Size` | The size of the panel | | `setSize` | `(size: Size) => void` | Function to set the size of the panel | | `minimize` | `VoidFunction` | Function to minimize the panel | | `maximize` | `VoidFunction` | Function to maximize the panel | | `restore` | `VoidFunction` | Function to restore the panel before it was minimized or maximized | | `resizable` | `boolean` | Whether the panel is resizable | | `draggable` | `boolean` | Whether the panel is draggable | # Hover Card ## Anatomy ```tsx {'Toggle Panel'} component children({ context }) { {'floatingPanel is '} {@context.open ? 'open' : 'closed'}
}{'Floating Panel'} {'Some content'}
``` ## Examples **Example: basic** ```ripple import { HoverCard } from 'ark-ripple/hover-card'; import { Portal } from 'ark-ripple/portal'; import styles from 'styles/hover-card.module.css'; export component Basic() { } ``` ### Controlled The controlled `HoverCard` component provides an interface for managing the state of the hover card using the `open` and `onOpenChange` props: **Example: controlled** ```ripple import { HoverCard } from 'ark-ripple/hover-card'; import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/hover-card.module.css'; export component Controlled() { let open = track(false); {'Liked by '}
component asChild({ propsFn }) { {'@sarah_chen'} } {' and 3 others'}![]()
{'Sarah Chen'}
{'@sarah_chen'}
{'Design Engineer at Acme Inc. Building beautiful interfaces and design systems.'}
{'2,456'} {'Following'}{'14.5K'} {'Followers'}} ``` ### Root Provider An alternative way to control the hover card is to use the `RootProvider` component and the `useHoverCard` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { HoverCard, useHoverCard } from 'ark-ripple/hover-card'; import { Portal } from 'ark-ripple/portal'; import styles from 'styles/hover-card.module.css'; export component RootProvider() { const hoverCard = useHoverCard();{ @open = e.open; }} > {'Liked by '}
component asChild({ propsFn }) { {'@sarah_chen'} } {' and 3 others'}![]()
{'Sarah Chen'}
{'@sarah_chen'}
{'Design Engineer at Acme Inc.'}
} ``` ### Delay Control the open and close delay of the hover card using the `openDelay` and `closeDelay` props: **Example: delay** ```ripple import { HoverCard } from 'ark-ripple/hover-card'; import { Portal } from 'ark-ripple/portal'; import styles from 'styles/hover-card.module.css'; export component Delay() {{'Liked by '}
component asChild({ propsFn }) { {'@sarah_chen'} } {' and 3 others'}![]()
{'Sarah Chen'}
{'@sarah_chen'}
{'Design Engineer at Acme Inc.'}
} ``` ### Positioning The `HoverCard` component can be customized in its placement and distance from the trigger element through the `positioning` prop: **Example: positioning** ```ripple import { HoverCard } from 'ark-ripple/hover-card'; import { Portal } from 'ark-ripple/portal'; import styles from 'styles/hover-card.module.css'; export component Positioning() { {'Liked by '}
component asChild({ propsFn }) { {'@sarah_chen'} } {' and 3 others'}![]()
{'Sarah Chen'}
{'@sarah_chen'}
{'Design Engineer at Acme Inc.'}
} ``` ### Context Access the hover card's state with `HoverCard.Context` or the `useHoverCardContext` hook: **Example: context** ```ripple import { HoverCard } from 'ark-ripple/hover-card'; import { Portal } from 'ark-ripple/portal'; import { ChevronDown, ChevronUp } from 'lucide-ripple'; import styles from 'styles/hover-card.module.css'; export component Context() { const s = 'www'; {'Liked by '}
component asChild({ propsFn }) { {'@sarah_chen'} } {' and 3 others'}![]()
{'Sarah Chen'}
{'@sarah_chen'}
{'Design Engineer at Acme Inc.'}
} ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `closeDelay` | `number` | No | The duration from when the mouse leaves the trigger or content until the hover card closes. | | `defaultOpen` | `boolean` | No | The initial open state of the hover card when rendered. Use when you don't need to control the open state of the hover card. | | `disabled` | `boolean` | No | Whether the hover card is disabled | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ trigger: string; content: string; positioner: string; arrow: string }>` | No | The ids of the elements in the popover. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component | | `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function called when the hover card opens or closes. | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component | | `open` | `boolean` | No | The controlled open state of the hover card | | `openDelay` | `number` | No | The duration from when the mouse enters the trigger until the hover card opens. | | `positioning` | `PositioningOptions` | No | The user provided options used to position the popover content | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Arrow Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Arrow CSS Variables:** | Variable | Description | |----------|-------------| | `--arrow-size` | The size of the arrow | | `--arrow-size-half` | Half the size of the arrow | | `--arrow-background` | Use this variable to style the arrow background | | `--arrow-offset` | The offset position of the arrow | **ArrowTip Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | hover-card | | `[data-part]` | content | | `[data-state]` | "open" | "closed" | | `[data-nested]` | popover | | `[data-has-nested]` | popover | | `[data-placement]` | The placement of the content | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | | `--nested-layer-count` | The number of nested hover-cards | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner CSS Variables:** | Variable | Description | |----------|-------------| | `--reference-width` | The width of the reference element | | `--reference-height` | The height of the root | | `--available-width` | The available width in viewport | | `--available-height` | The available height in viewport | | `--x` | The x position for transform | | `--y` | The y position for transform | | `--z-index` | The z-index value | | `--transform-origin` | The transform origin for animations | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseHoverCardReturn` | Yes | | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | hover-card | | `[data-part]` | trigger | | `[data-placement]` | The placement of the trigger | | `[data-state]` | "open" | "closed" | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `open` | `boolean` | Whether the hover card is open | | `setOpen` | `(open: boolean) => void` | Function to open the hover card | | `reposition` | `(options?: Partial {'Liked by '}
component asChild({ propsFn }) { {'@sarah_chen '} {' and 3 others'}component children({ context }) { {s} if (@context.open) { }} else { } } ![]()
{'Sarah Chen'}
{'@sarah_chen'}
{'Design Engineer at Acme Inc.'}
) => void` | Function to reposition the popover | # Image Cropper ## Anatomy ```tsx ``` ## Examples ### Basic Set up a basic image cropper. Drag the handles to resize the selection, or drag inside to pan the image. **Example: basic** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import styles from 'styles/image-cropper.module.css'; export component Basic() { } ``` ### Aspect Ratio Lock the crop area to a specific aspect ratio. Use the `aspectRatio` prop—pass a number like `16/9` for widescreen or `1` for square. **Example: aspect-ratio** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import { RectangleHorizontal, Square, RectangleVertical } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; const aspects = [ { label: '16:9', value: 16 / 9 }, { label: '1:1', value: 1 }, { label: '9:16', value: 9 / 16 }, ]; const icons = [RectangleHorizontal, Square, RectangleVertical]; export component AspectRatio() { let aspectRatio = track(16 / 9);for (const position of ImageCropper.handles; key position) { } } ``` ### Circle Crop Use `cropShape="circle"` for profile pictures or avatars. The selection becomes a circle instead of a rectangle. **Example: circle** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import styles from 'styles/image-cropper.module.css'; export component Circle() {for (const position of ImageCropper.handles; key position) { } } ``` ### Initial Crop Start with a pre-defined crop area using the `initialCrop` prop. Pass an object with `x`, `y`, `width`, and `height` in pixels. **Example: initial-crop** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import styles from 'styles/image-cropper.module.css'; export component InitialCrop() {for (const position of ImageCropper.handles; key position) { } } ``` ### Controlled Zoom Control zoom programmatically with the `zoom` and `onZoomChange` props. Useful when you want external buttons to zoom in and out. **Example: controlled-zoom** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import { ZoomIn, ZoomOut } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component ControlledZoom() { let zoom = track(1);{'Starts with a pre-defined crop area'}
for (const position of ImageCropper.handles; key position) { } } ``` ### Zoom Limits Set `minZoom` and `maxZoom` to constrain how far users can zoom. Prevents over-zooming or zooming out past the image bounds. **Example: zoom-limits** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import { ZoomIn, ZoomOut } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component ZoomLimits() { let zoom = track(1); const minZoom = 0.5; const maxZoom = 2;{ @zoom = e.zoom; }} > for (const position of ImageCropper.handles; key position) { } } ``` ### Rotation Rotate the image with the `rotation` and `onRotationChange` props. Values are in degrees—common increments are 90 or 180. **Example: rotation** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import { RotateCcw, RotateCw } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component Rotation() { let rotation = track(0);{'Zoom constrained between '} {minZoom} {'x and '} {maxZoom} {'x'}
{ @zoom = e.zoom; }} {minZoom} {maxZoom} > for (const position of ImageCropper.handles; key position) { } } ``` ### Flip Flip the image horizontally or vertically using the `flip` prop. Pass an object with `horizontal` and `vertical` booleans. **Example: flip** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import { FlipHorizontal, FlipVertical } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component Flip() { let flip = track({ horizontal: false, vertical: false });{ @rotation = e.rotation; }} > for (const position of ImageCropper.handles; key position) { } } ``` ### Min and Max Size Constrain the crop area size with `minWidth`, `minHeight`, `maxWidth`, and `maxHeight`. Keeps the selection within sensible bounds. **Example: min-max-size** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import styles from 'styles/image-cropper.module.css'; export component MinMaxSize() {{ @flip = e.flip; }} > for (const position of ImageCropper.handles; key position) { } } ``` ### Fixed Crop Area Set `fixedCropArea` to `true` when the crop area should stay fixed while the image moves underneath. Useful for overlay-style cropping. **Example: fixed** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import styles from 'styles/image-cropper.module.css'; export component Fixed() {{'Crop area constrained to min 80px and max 200px'}
for (const position of ImageCropper.handles; key position) { } } ``` ### Crop Preview Use `getCroppedImage()` from the context to get the cropped result. Call it with `{ output: 'dataUrl' }` for a base64 string you can use in an `img` src. **Example: crop-preview** ```ripple import { ImageCropper, useImageCropper } from 'ark-ripple/image-cropper'; import { Crop } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component CropPreview() { const imageCropper = useImageCropper(); let preview = track(null); const handleCrop = async () => { const result = await @imageCropper.getCroppedImage({ output: 'dataUrl' }); if (typeof result === 'string') { @preview = result; } }; } ``` ### Reset The context exposes a `reset()` method that restores the image to its initial state. Handy for an "undo" or "start over" button. **Example: reset** ```ripple import { ImageCropper, useImageCropper } from 'ark-ripple/image-cropper'; import { FlipHorizontal, RotateCw, RotateCcw, RefreshCw, ZoomIn, ZoomOut } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component Reset() { const imageCropper = useImageCropper();for (const position of ImageCropper.handles; key position) { } {'Preview'} if (@preview) {}
} ``` ### Events Listen to `onCropChange` and `onZoomChange` to track crop position and zoom level. Use these to sync with external state or show live previews. **Example: events** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import { track } from 'ripple'; import styles from 'styles/image-cropper.module.css'; export component Events() { let cropData = track({ x: 0, y: 0, width: 0, height: 0 }); let zoom = track(1);for (const position of ImageCropper.handles; key position) { } } ``` ### Context Use `ImageCropper.Context` to access the cropper API from anywhere inside the root. You get methods like `zoomBy`, `rotateBy`, and `setZoom`. **Example: context** ```ripple import { ImageCropper } from 'ark-ripple/image-cropper'; import { ZoomIn, ZoomOut } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component Context() {{ @cropData = e.crop; }} onZoomChange={(e) => { @zoom = e.zoom; }} > for (const position of ImageCropper.handles; key position) { } {'Zoom'} {@zoom.toFixed(2)} {'x'}{'Position'} {Math.round(@cropData.x)} {', '} {Math.round(@cropData.y)}{'Size'} {Math.round(@cropData.width)} {' × '} {Math.round(@cropData.height)}} ``` ### Root Provider Use `RootProvider` with `useImageCropper` when you need to control the cropper from outside the component tree. Build custom toolbars or integrate with form state. **Example: root-provider** ```ripple import { ImageCropper, useImageCropper } from 'ark-ripple/image-cropper'; import { ZoomIn, ZoomOut } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/image-cropper.module.css'; export component RootProvider() { const imageCropper = useImageCropper();component children({ context }) { } for (const position of ImageCropper.handles; key position) { } } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `aspectRatio` | `number` | No | The aspect ratio to maintain for the crop area (width / height). For example, an aspect ratio of 16 / 9 will maintain a width to height ratio of 16:9. If not provided, the crop area can be freely resized. | | `cropShape` | `'circle' | 'rectangle'` | No | The shape of the crop area. | | `defaultFlip` | `FlipState` | No | The initial flip state to apply to the image. | | `defaultRotation` | `number` | No | The initial rotation to apply to the image in degrees. | | `defaultZoom` | `number` | No | The initial zoom factor to apply to the image. | | `fixedCropArea` | `boolean` | No | Whether the crop area is fixed in size and position. | | `flip` | `FlipState` | No | The controlled flip state of the image. | | `ids` | `Partial<{ root: string viewport: string image: string selection: string handle: (position: string) => string }>` | No | The ids of the image cropper elements | | `initialCrop` | `Rect` | No | The initial rectangle of the crop area. If not provided, a smart default will be computed based on viewport size and aspect ratio. | | `maxHeight` | `number` | No | The maximum height of the crop area | | `maxWidth` | `number` | No | The maximum width of the crop area | | `maxZoom` | `number` | No | The maximum zoom factor allowed. | | `minHeight` | `number` | No | The minimum height of the crop area | | `minWidth` | `number` | No | The minimum width of the crop area | | `minZoom` | `number` | No | The minimum zoom factor allowed. | | `nudgeStep` | `number` | No | The base nudge step for keyboard arrow keys (in pixels). | | `nudgeStepCtrl` | `number` | No | The nudge step when Ctrl/Cmd key is held (in pixels). | | `nudgeStepShift` | `number` | No | The nudge step when Shift key is held (in pixels). | | `onCropChange` | `(details: CropChangeDetails) => void` | No | Callback fired when the crop area changes. | | `onFlipChange` | `(details: FlipChangeDetails) => void` | No | Callback fired when the flip state changes. | | `onRotationChange` | `(details: RotationChangeDetails) => void` | No | Callback fired when the rotation changes. | | `onZoomChange` | `(details: ZoomChangeDetails) => void` | No | Callback fired when the zoom level changes. | | `rotation` | `number` | No | The controlled rotation of the image in degrees (0 - 360). | | `translations` | `IntlTranslations` | No | Specifies the localized strings that identify accessibility elements and their states. | | `zoom` | `number` | No | The controlled zoom level of the image. | | `zoomSensitivity` | `number` | No | Controls how responsive pinch-to-zoom is. | | `zoomStep` | `number` | No | The amount of zoom applied per wheel step. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | image-cropper | | `[data-part]` | root | | `[data-fixed]` | | | `[data-shape]` | | | `[data-pinch]` | | | `[data-dragging]` | Present when in the dragging state | | `[data-panning]` | | **Root CSS Variables:** | Variable | Description | |----------|-------------| | `--crop-width` | The width of the Root | | `--crop-height` | The height of the Root | | `--crop-x` | The crop x value for the Root | | `--crop-y` | The crop y value for the Root | | `--image-zoom` | The image zoom value for the Root | | `--image-rotation` | The image rotation value for the Root | | `--image-offset-x` | The offset position for image | | `--image-offset-y` | The offset position for image | **Grid Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `axis` | `'horizontal' | 'vertical'` | Yes | The axis of the grid lines to display | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Grid Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | image-cropper | | `[data-part]` | grid | | `[data-axis]` | The axis to resize | | `[data-dragging]` | Present when in the dragging state | | `[data-panning]` | | **Handle Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `position` | `HandlePosition` | Yes | The position of the handle | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Handle Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | image-cropper | | `[data-part]` | handle | | `[data-position]` | | | `[data-disabled]` | Present when disabled | **Image Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Image Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | image-cropper | | `[data-part]` | image | | `[data-ready]` | | | `[data-flip-horizontal]` | | | `[data-flip-vertical]` | | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseImageCropperReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Selection Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Selection Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | image-cropper | | `[data-part]` | selection | | `[data-disabled]` | Present when disabled | | `[data-shape]` | | | `[data-measured]` | | | `[data-dragging]` | Present when in the dragging state | | `[data-panning]` | | **Viewport Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Viewport Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | image-cropper | | `[data-part]` | viewport | | `[data-disabled]` | Present when disabled | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `zoom` | `number` | The current zoom level of the image. | | `rotation` | `number` | The current rotation of the image in degrees. | | `flip` | `FlipState` | The current flip state of the image. | | `crop` | `Rect` | The current crop area rectangle in viewport coordinates. | | `offset` | `Point` | The current offset (pan position) of the image. | | `naturalSize` | `Size` | The natural (original) size of the image. | | `viewportRect` | `BoundingRect` | The viewport rectangle dimensions and position. | | `dragging` | `boolean` | Whether the crop area is currently being dragged. | | `panning` | `boolean` | Whether the image is currently being panned. | | `setZoom` | `(zoom: number) => void` | Function to set the zoom level of the image. | | `zoomBy` | `(delta: number) => void` | Function to zoom the image by a relative amount. | | `setRotation` | `(rotation: number) => void` | Function to set the rotation of the image. | | `rotateBy` | `(degrees: number) => void` | Function to rotate the image by a relative amount in degrees. | | `setFlip` | `(flip: Partialfor (const position of ImageCropper.handles; key position) { } ) => void` | Function to set the flip state of the image. | | `flipHorizontally` | `(value?: boolean) => void` | Function to flip the image horizontally. Pass a boolean to set explicitly or omit to toggle. | | `flipVertically` | `(value?: boolean) => void` | Function to flip the image vertically. Pass a boolean to set explicitly or omit to toggle. | | `resize` | `(handlePosition: HandlePosition, delta: number) => void` | Function to resize the crop area from a handle programmatically. | | `reset` | `() => void` | Function to reset the cropper to its initial state. | | `getCroppedImage` | `(options?: GetCroppedImageOptions) => Promise ` | Function to get the cropped image with all transformations applied. Returns a Promise that resolves to either a Blob or data URL. | | `getCropData` | `() => CropData` | Function to get the crop data in natural image pixel coordinates. These coordinates are relative to the original image dimensions, accounting for zoom, rotation, and flip transformations. Use this for server-side cropping or state persistence. | # Listbox ## Anatomy {/* */} ```tsx ``` ## Examples ### Basic Here's a basic example of the Listbox component. **Example: basic** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; export component Basic() { const collection = createListCollection( { items: [ { label: 'United States', value: 'us' }, { label: 'United Kingdom', value: 'uk' }, { label: 'Canada', value: 'ca' }, { label: 'Australia', value: 'au' }, { label: 'Germany', value: 'de' }, { label: 'France', value: 'fr' }, { label: 'Japan', value: 'jp' }, ], }, ); } ``` ### Controlled The Listbox component can be controlled by using the `value` and `onValueChange` props. This allows you to manage the selected value externally. **Example: controlled** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/listbox.module.css'; export component Controlled() { const collection = createListCollection( { items: [ { label: 'Small', value: 'sm' }, { label: 'Medium', value: 'md' }, { label: 'Large', value: 'lg' }, { label: 'Extra Large', value: 'xl' }, ], }, ); let value = track(['md']); {'Select Country'} for (const item of collection.items; key item.value) { } {item.label} { @value = e.value; }} > } ``` ### Root Provider An alternative way to control the listbox is to use the `RootProvider` component and the `useListbox` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Listbox, createListCollection, useListbox } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/listbox.module.css'; export component RootProvider() { const collection = createListCollection( { items: [ { label: 'Low', value: 'low' }, { label: 'Medium', value: 'medium' }, { label: 'High', value: 'high' }, { label: 'Critical', value: 'critical' }, ], }, ); const listbox = useListbox({ collection });{'Select Size'} for (const item of collection.items; key item.value) { } {item.label} } ``` ### Disabled Item Listbox items can be disabled using the `disabled` prop on the collection item. **Example: disabled-item** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; export component DisabledItem() { const collection = createListCollection( { items: [ { label: 'Free', value: 'free' }, { label: 'Pro', value: 'pro' }, { label: 'Enterprise', value: 'enterprise', disabled: true }, { label: 'Custom', value: 'custom' }, ], }, );{'Select Priority'} for (const item of collection.items; key item.value) { } {item.label} } ``` > You can also use the `isItemDisabled` within the `createListCollection` to disable items based on a condition. ### Multiple You can set the `selectionMode` property as `multiple` to allow the user to select multiple items at a time. **Example: multiple** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; export component Multiple() { const collection = createListCollection( { items: [ { label: 'Monday', value: 'mon' }, { label: 'Tuesday', value: 'tue' }, { label: 'Wednesday', value: 'wed' }, { label: 'Thursday', value: 'thu' }, { label: 'Friday', value: 'fri' }, { label: 'Saturday', value: 'sat' }, { label: 'Sunday', value: 'sun' }, ], }, ); {'Select Plan'} for (const item of collection.items; key item.value) { } {item.label} } ``` ### Grouping The Listbox component supports grouping items. You can use the `groupBy` function to group items based on a specific property. **Example: group** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; export component Group() { const collection = createListCollection( { items: [ { label: 'New York', value: 'nyc', region: 'North America' }, { label: 'Los Angeles', value: 'lax', region: 'North America' }, { label: 'Toronto', value: 'yyz', region: 'North America' }, { label: 'London', value: 'lhr', region: 'Europe' }, { label: 'Paris', value: 'cdg', region: 'Europe' }, { label: 'Berlin', value: 'ber', region: 'Europe' }, { label: 'Tokyo', value: 'nrt', region: 'Asia Pacific' }, { label: 'Singapore', value: 'sin', region: 'Asia Pacific' }, { label: 'Sydney', value: 'syd', region: 'Asia Pacific' }, ], groupBy: (item) => item.region, }, ); {'Select Days'} for (const item of collection.items; key item.value) { } {item.label} } ``` ### Extended Selection The extended selection mode allows users to select multiple items using keyboard modifiers like `Cmd` (Mac) or `Ctrl` (Windows/Linux). **Example: extended-select** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; export component ExtendedSelect() { const collection = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, { label: 'Solid', value: 'solid' }, { label: 'Preact', value: 'preact' }, ], }, ); {'Select Region'} for (const [region, items] of collection.group(); key region) { } {region} for (const item of items; key item.value) {} {item.label} } ``` ### Horizontal Use the `orientation` prop to display the listbox items horizontally. **Example: horizontal** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; export component Horizontal() { const collection = createListCollection( { items: [ { title: 'Midnight Dreams', artist: 'Luna Ray', image: 'https://picsum.photos/seed/album1/300/300', }, { title: 'Neon Skyline', artist: 'The Electric', image: 'https://picsum.photos/seed/album2/300/300', }, { title: 'Acoustic Sessions', artist: 'Sarah Woods', image: 'https://picsum.photos/seed/album3/300/300', }, { title: 'Urban Echoes', artist: 'Metro Collective', image: 'https://picsum.photos/seed/album4/300/300', }, { title: 'Summer Vibes', artist: 'Coastal Waves', image: 'https://picsum.photos/seed/album5/300/300', }, ], itemToValue: (item) => item.title, itemToString: (item) => item.title, }, ); {'Hold '} {'⌘'} {' or '} {'Ctrl'} {' to select multiple'} for (const item of collection.items; key item.value) { } {item.label} } ``` ### Grid Layout Use `createGridCollection` to display items in a grid layout with keyboard navigation support. **Example: grid** ```ripple import { createGridCollection } from 'ark-ripple/collection'; import { Listbox } from 'ark-ripple/listbox'; import styles from 'styles/listbox.module.css'; export component Grid() { const collection = createGridCollection( { items: [ { label: '😀', value: 'grinning' }, { label: '😍', value: 'heart-eyes' }, { label: '🥳', value: 'partying' }, { label: '😎', value: 'sunglasses' }, { label: '🤩', value: 'star-struck' }, { label: '😂', value: 'joy' }, { label: '🥰', value: 'smiling-hearts' }, { label: '😊', value: 'blush' }, { label: '🤗', value: 'hugging' }, { label: '😇', value: 'innocent' }, { label: '🔥', value: 'fire' }, { label: '✨', value: 'sparkles' }, { label: '💯', value: 'hundred' }, { label: '🎉', value: 'tada' }, { label: '❤️', value: 'heart' }, { label: '👍', value: 'thumbs-up' }, { label: '👏', value: 'clap' }, { label: '🚀', value: 'rocket' }, { label: '⭐', value: 'star' }, { label: '🌈', value: 'rainbow' }, ], columnCount: 5, }, ); {'Select Album'} for (const item of collection.items; key item.title) { } {item.title} {item.artist}
} ``` ### Filtering Use `useListCollection` with the `filter` function to enable filtering of items. **Example: filtering** ```ripple import { useListCollection } from 'ark-ripple/collection'; import { Listbox } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/listbox.module.css'; export component Filtering() { const { collection, filter } = useListCollection( { initialItems: [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, { label: 'Solid', value: 'solid' }, { label: 'Next.js', value: 'nextjs' }, { label: 'Nuxt.js', value: 'nuxtjs' }, { label: 'Remix', value: 'remix' }, { label: 'Gatsby', value: 'gatsby' }, { label: 'Preact', value: 'preact' }, ], filter: (itemText, filterText) => itemText.toLowerCase().includes(filterText.toLowerCase()), }, ); {'Pick a reaction'} for (const item of collection.items; key item.value) { } {item.label} } ``` ### Select All Use `useListboxContext` to implement a "Select All" functionality that allows users to select or deselect all items at once. **Example: select-all** ```ripple import { Listbox, createListCollection, useListboxContext } from 'ark-ripple/listbox'; import { Check, Minus } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; const frameworks = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, { label: 'Next.js', value: 'nextjs' }, { label: 'Nuxt.js', value: 'nuxtjs' }, { label: 'Remix', value: 'remix' }, { label: 'Gatsby', value: 'gatsby' }, ], }, ); component SelectAllHeader() { const listbox = useListboxContext(); const isAllSelected = @listbox.value.length === frameworks.items.length; const isSomeSelected = @listbox.value.length > 0 && @listbox.value.length < frameworks.items.length; const handleSelectAll = () => { if (isAllSelected) { @listbox.setValue([]); } else { @listbox.setValue(frameworks.items.map((item) => item.value)); } }; } export component SelectAll() { {'Select Framework'} filter((e.target as HTMLInputElement).value)} /> for (const item of @collection.items; key item.value) { } {item.label} {'No frameworks found'} } ``` ### Value Text Use `Listbox.ValueText` to display the selected values as a comma-separated string. **Example: value-text** ```ripple import { Listbox, createListCollection } from 'ark-ripple/listbox'; import { Check } from 'lucide-ripple'; import styles from 'styles/listbox.module.css'; export component ValueText() { const collection = createListCollection( { items: [ { label: 'Red', value: 'red' }, { label: 'Blue', value: 'blue' }, { label: 'Green', value: 'green' }, { label: 'Yellow', value: 'yellow' }, { label: 'Purple', value: 'purple' }, ], }, ); for (const item of frameworks.items; key item.value) { } {item.label} } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `collection` | `ListCollection {'Colors: '} for (const item of collection.items; key item.value) { } {item.label} ` | Yes | The collection of items | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `defaultHighlightedValue` | `string` | No | The initial value of the highlighted item when opened. Use when you don't need to control the highlighted value of the listbox. | | `defaultValue` | `string[]` | No | The initial default value of the listbox when rendered. Use when you don't need to control the value of the listbox. | | `deselectable` | `boolean` | No | Whether to disallow empty selection | | `disabled` | `boolean` | No | Whether the listbox is disabled | | `disallowSelectAll` | `boolean` | No | Whether to disallow selecting all items when `meta+a` is pressed | | `highlightedValue` | `string` | No | The controlled key of the highlighted item | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string content: string label: string item: (id: string | number) => string itemGroup: (id: string | number) => string itemGroupLabel: (id: string | number) => string }>` | No | The ids of the elements in the listbox. Useful for composition. | | `loopFocus` | `boolean` | No | Whether to loop the keyboard navigation through the options | | `onHighlightChange` | `(details: HighlightChangeDetails ) => void` | No | The callback fired when the highlighted item changes. | | `onSelect` | `(details: SelectionDetails) => void` | No | Function called when an item is selected | | `onValueChange` | `(details: ValueChangeDetails ) => void` | No | The callback fired when the selected item changes. | | `orientation` | `'horizontal' | 'vertical'` | No | The orientation of the listbox. | | `scrollToIndexFn` | `(details: ScrollToIndexDetails) => void` | No | Function to scroll to a specific index | | `selectionMode` | `SelectionMode` | No | How multiple selection should behave in the listbox. - `single`: The user can select a single item. - `multiple`: The user can select multiple items without using modifier keys. - `extended`: The user can select multiple items by using modifier keys. | | `selectOnHighlight` | `boolean` | No | Whether to select the item when it is highlighted | | `typeahead` | `boolean` | No | Whether to enable typeahead on the listbox | | `value` | `string[]` | No | The controlled keys of the selected items | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | root | | `[data-orientation]` | The orientation of the listbox | | `[data-disabled]` | Present when disabled | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | content | | `[data-activedescendant]` | The id the active descendant of the content | | `[data-orientation]` | The orientation of the content | | `[data-layout]` | | | `[data-empty]` | Present when the content is empty | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--column-count` | The column count value for the Content | **Empty Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoHighlight` | `boolean` | No | Whether to automatically highlight the item when typing | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | input | | `[data-disabled]` | Present when disabled | **ItemGroupLabel Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemGroup Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | item-group | | `[data-disabled]` | Present when disabled | | `[data-orientation]` | The orientation of the item | | `[data-empty]` | Present when the content is empty | **ItemIndicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemIndicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | item-indicator | | `[data-state]` | "checked" | "unchecked" | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `highlightOnHover` | `boolean` | No | Whether to highlight the item on hover | | `item` | `any` | No | The item to render | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | item | | `[data-value]` | The value of the item | | `[data-selected]` | Present when selected | | `[data-layout]` | | | `[data-state]` | "checked" | "unchecked" | | `[data-orientation]` | The orientation of the item | | `[data-highlighted]` | Present when highlighted | | `[data-disabled]` | Present when disabled | **ItemText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | item-text | | `[data-state]` | "checked" | "unchecked" | | `[data-disabled]` | Present when disabled | | `[data-highlighted]` | Present when highlighted | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | label | | `[data-disabled]` | Present when disabled | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseListboxReturn ` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ValueText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `placeholder` | `string` | No | Text to display when no value is listboxed. | **ValueText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | listbox | | `[data-part]` | value-text | | `[data-disabled]` | Present when disabled | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `empty` | `boolean` | Whether the select value is empty | | `highlightedValue` | `string` | The value of the highlighted item | | `highlightedItem` | `V` | The highlighted item | | `highlightValue` | `(value: string) => void` | Function to highlight a value | | `clearHighlightedValue` | `VoidFunction` | Function to clear the highlighted value | | `selectedItems` | `V[]` | The selected items | | `hasSelectedItems` | `boolean` | Whether there's a selected option | | `value` | `string[]` | The selected item keys | | `valueAsString` | `string` | The string representation of the selected items | | `selectValue` | `(value: string) => void` | Function to select a value | | `selectAll` | `VoidFunction` | Function to select all values. **Note**: This should only be called when the selectionMode is `multiple` or `extended`. Otherwise, an exception will be thrown. | | `setValue` | `(value: string[]) => void` | Function to set the value of the select | | `clearValue` | `(value?: string) => void` | Function to clear the value of the select. If a value is provided, it will only clear that value, otherwise, it will clear all values. | | `getItemState` | `(props: ItemProps ) => ItemState` | Returns the state of a select item | | `collection` | `ListCollection ` | Function to toggle the select | | `disabled` | `boolean` | Whether the select is disabled | # Marquee ## Anatomy {/* */} ```tsx ``` ## Examples **Example: basic** ```ripple import { Marquee } from 'ark-ripple/marquee'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component Basic() { } ``` ### Auto Fill Use the `autoFill` prop to automatically duplicate content to fill the viewport. The `spacing` prop controls the gap between duplicated content instances: **Example: auto-fill** ```ripple import { Marquee } from 'ark-ripple/marquee'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, ]; export component AutoFill() { for (const item of items; key item.name) { {item.logo} {item.name} }} ``` ### Reverse Set the `reverse` prop to reverse the scroll direction: **Example: reverse** ```ripple import { Marquee } from 'ark-ripple/marquee'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component Reverse() { for (const item of items; key item.name) { {item.logo} {item.name} }} ``` ### Vertical Set `side="bottom"` (or `side="top"`) to create a vertical marquee: **Example: vertical** ```ripple import { Marquee } from 'ark-ripple/marquee'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component Vertical() { for (const item of items; key item.name) { {item.logo} {item.name} }} ``` ### Speed Control the animation speed using the `speed` prop, which accepts values in pixels per second: **Example: speed** ```ripple import { Marquee } from 'ark-ripple/marquee'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component Speed() { for (const item of items; key item.name) { {item.logo} {item.name} }} ``` ### Pause on Interaction Enable `pauseOnInteraction` to pause the marquee when users hover or focus on it, improving accessibility: **Example: pause-on-interaction** ```ripple import { Marquee } from 'ark-ripple/marquee'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component PauseOnInteraction() {{'Slow (25px/s)'}
for (const item of items; key item.name) { {item.logo} {item.name} }{'Normal (50px/s)'}
for (const item of items; key item.name) { {item.logo} {item.name} }{'Fast (100px/s)'}
for (const item of items; key item.name) { {item.logo} {item.name} }} ``` ### Programmatic Control Use the `useMarquee` hook with `Marquee.RootProvider` to access the marquee API and control playback programmatically: **Example: programmatic-control** ```ripple import { Marquee, useMarquee } from 'ark-ripple/marquee'; import button from 'styles/button.module.css'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component ProgrammaticControl() { const marquee = useMarquee({}); for (const item of items; key item.name) { {item.logo} {item.name} }} ``` > If you're using the `Marquee.RootProvider` component, you don't need to use the `Marquee.Root` component. ### Loops Set the `loopCount` prop to run the marquee a specific number of times. Use `onLoopComplete` to track each loop iteration and `onComplete` to know when all loops finish: **Example: finite-loops** ```ripple import { Marquee } from 'ark-ripple/marquee'; import { track } from 'ripple'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component FiniteLoops() { let loopCount = track(0); let completedCount = track(0);for (const item of items; key item.name) { {item.logo} {item.name} }} ``` ### Edges Add `Marquee.Edge` components to create fade effects at the start and end of the scrolling area: **Example: with-edges** ```ripple import { Marquee } from 'ark-ripple/marquee'; import styles from 'styles/marquee.module.css'; const items = [ { name: 'Apple', logo: '🍎' }, { name: 'Banana', logo: '🍌' }, { name: 'Cherry', logo: '🍒' }, { name: 'Grape', logo: '🍇' }, { name: 'Watermelon', logo: '🍉' }, { name: 'Strawberry', logo: '🍓' }, ]; export component WithEdges() {{ @loopCount = @loopCount + 1; }} onComplete={() => { @completedCount = @completedCount + 1; }} class={styles.Root} > for (const item of items; key item.name) { {item.logo} {item.name} }{'Loop completed: '} {@loopCount} {' times'}
{'Animation completed: '} {@completedCount} {' times'}
} ``` ## Guides ### Content Animation The Marquee component requires CSS keyframe animations to function properly. You'll need to define animations for both horizontal and vertical scrolling: ```css @keyframes marqueeX { from { transform: translateX(0); } to { transform: translateX(var(--marquee-translate)); } } @keyframes marqueeY { from { transform: translateY(0); } to { transform: translateY(var(--marquee-translate)); } } ``` The component automatically applies the appropriate animation (`marqueeX` or `marqueeY`) based on the scroll direction and uses the `--marquee-translate` CSS variable for seamless looping. You can target specific parts of the marquee using `data-part` attributes for custom styling: - `[data-part="root"]` - The root container - `[data-part="viewport"]` - The scrolling viewport - `[data-part="content"]` - The content wrapper (receives animation) - `[data-part="item"]` - Individual marquee items - `[data-part="edge"]` - Edge gradient overlays ### Best Practices - **Enable pause-on-interaction**: Use `pauseOnInteraction` to allow users to pause animations on hover or focus, improving accessibility and readability - **Use descriptive labels**: Provide meaningful `aria-label` values that describe the marquee content (e.g., "Partner logos", "Latest announcements") - **Avoid for critical information**: Don't use marquees for essential content that users must read, as continuously moving text can be difficult to process. Consider static displays for important information ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoFill` | `boolean` | No | Whether to automatically duplicate content to fill the container. | | `defaultPaused` | `boolean` | No | Whether the marquee is paused by default. | | `delay` | `number` | No | The delay before the animation starts (in seconds). | | `ids` | `Partial<{ root: string; viewport: string; content: (index: number) => string }>` | No | The ids of the elements in the marquee. Useful for composition. | | `loopCount` | `number` | No | The number of times to loop the animation (0 = infinite). | | `onComplete` | `() => void` | No | Function called when the marquee completes all loops and stops. Only fires for finite loops (loopCount > 0). | | `onLoopComplete` | `() => void` | No | Function called when the marquee completes one loop iteration. | | `onPauseChange` | `(details: PauseStatusDetails) => void` | No | Function called when the pause status changes. | | `paused` | `boolean` | No | Whether the marquee is paused. | | `pauseOnInteraction` | `boolean` | No | Whether to pause the marquee on user interaction (hover, focus). | | `reverse` | `boolean` | No | Whether to reverse the animation direction. | | `side` | `Side` | No | The side/direction the marquee scrolls towards. | | `spacing` | `string` | No | The spacing between marquee items. | | `speed` | `number` | No | The speed of the marquee animation in pixels per second. | | `translations` | `IntlTranslations` | No | The localized messages to use. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | marquee | | `[data-part]` | root | | `[data-state]` | "paused" | "idle" | | `[data-orientation]` | The orientation of the marquee | | `[data-paused]` | Present when paused | **Root CSS Variables:** | Variable | Description | |----------|-------------| | `--marquee-duration` | The marquee duration value for the Root | | `--marquee-spacing` | The marquee spacing value for the Root | | `--marquee-delay` | The marquee delay value for the Root | | `--marquee-loop-count` | The marquee loop count value for the Root | | `--marquee-translate` | The marquee translate value for the Root | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | marquee | | `[data-part]` | | | `[data-index]` | The index of the item | | `[data-orientation]` | The orientation of the content | | `[data-side]` | | | `[data-reverse]` | | | `[data-clone]` | | **Edge Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `side` | `Side` | Yes | The side where the edge gradient should appear. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Edge Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | marquee | | `[data-part]` | | | `[data-side]` | | | `[data-orientation]` | The orientation of the edge | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseMarqueeReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Viewport Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Viewport Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | marquee | | `[data-part]` | | | `[data-orientation]` | The orientation of the viewport | | `[data-side]` | | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `paused` | `boolean` | Whether the marquee is currently paused. | | `orientation` | `"horizontal" | "vertical"` | The current orientation of the marquee. | | `side` | `Side` | The current side/direction of the marquee. | | `multiplier` | `number` | The multiplier for auto-fill. Indicates how many times to duplicate content. When autoFill is enabled and content is smaller than container, this returns the number of additional copies needed. Otherwise returns 1. | | `contentCount` | `number` | The total number of content elements to render (original + clones). Use this value when rendering your content in a loop. | | `pause` | `VoidFunction` | Pause the marquee animation. | | `resume` | `VoidFunction` | Resume the marquee animation. | | `togglePause` | `VoidFunction` | Toggle the pause state. | | `restart` | `VoidFunction` | Restart the marquee animation from the beginning. | # Menu ## Anatomy ```tsx for (const item of items; key item.name) { {item.logo} {item.name} }``` ## Examples **Example: basic** ```ripple import { Menu } from 'ark-ripple/menu'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/menu.module.css'; export component Basic() { } ``` ### Item Selection Use `onSelect` to handle item selection. The callback receives the item's `id`. **Example: controlled** ```ripple import { Menu } from 'ark-ripple/menu'; import { ChevronDown } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/menu.module.css'; export component Controlled() { let open = track(false); {'File'} {'New File'} {'Open...'} {'Save'} {'Save As...'} } ``` ### Root Provider An alternative way to control the menu is to use the `RootProvider` component and the `useMenu` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Menu, useMenu } from 'ark-ripple/menu'; import { ChevronDown } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/menu.module.css'; export component RootProvider() { const menu = useMenu();{ @open = e.open; }} > {'Actions'} {'Edit'} {'Duplicate'} {'Archive'} {'Delete'} } ``` ### Grouping Use `Menu.ItemGroup` and `Menu.ItemGroupLabel` to organize related menu items. **Example: group** ```ripple import { Menu } from 'ark-ripple/menu'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/menu.module.css'; export component Group() {{'Edit'} {'Cut'} {'Copy'} {'Paste'} {'Delete'} } ``` ### Links To render menu items as links, use the `asChild` prop to replace the default element with an anchor tag. **Example: links** ```ripple import { Menu } from 'ark-ripple/menu'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/menu.module.css'; export component Links() { {'Edit'} {'Clipboard'} {'Cut'} {'Copy'} {'Paste'} {'Selection'} {'Select All'} {'Deselect'} } ``` ### Checkbox To add a checkbox to a menu item, use the `Menu.Checkbox` component. **Example: checkbox-items** ```ripple import { Menu } from 'ark-ripple/menu'; import { Check, ChevronDown } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/menu.module.css'; export component CheckboxItems() { let showToolbar = track(true); let showStatusBar = track(false); {'Help'} component asChild({ propsFn }) { {'Documentation'} } component asChild({ propsFn }) { {'GitHub'} } component asChild({ propsFn }) { {'Changelog'} } } ``` ### Radio Group To group radio option items, use the `Menu.RadioGroup` component. **Example: radio-items** ```ripple import { Menu } from 'ark-ripple/menu'; import { Check, ChevronDown } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/menu.module.css'; export component RadioItems() { let sortBy = track('date'); {'View'} { @showToolbar = v; }} value="toolbar" > {'Show Toolbar'} { @showStatusBar = v; }} value="status-bar" > {'Show Status Bar'} } ``` ### Context Menu To show the menu when a trigger element is right-clicked, use the `Menu.ContextTrigger` component. Context menus are also opened during a long-press of roughly `700ms` when the pointer is pen or touch. **Example: context** ```ripple import { Menu } from 'ark-ripple/menu'; import styles from 'styles/menu.module.css'; export component Context() { {'Sort'} { @sortBy = e.value; }} > {'Sort By'} {'Name'} {'Date Modified'} {'Size'} {'Type'} } ``` ### Nested To show a nested menu, render another `Menu` component and use the `Menu.TriggerItem` component to open the submenu. **Example: nested** ```ripple import { Menu } from 'ark-ripple/menu'; import { ChevronDown } from 'lucide-ripple'; import styles from 'styles/menu.module.css'; export component Nested() { {'Right click here'} {'Cut'} {'Copy'} {'Paste'} {'Delete'} } ``` ### Menu in Dialog When rendering a menu inside a dialog, use `lazyMount` and `unmountOnExit` to ensure proper cleanup when the dialog closes. **Example: menu-in-dialog** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Menu } from 'ark-ripple/menu'; import { Portal } from 'ark-ripple/portal'; import { ChevronDown, X } from 'lucide-ripple'; import button from 'styles/button.module.css'; import dialog from 'styles/dialog.module.css'; import styles from 'styles/menu.module.css'; export component MenuInDialog() { {'File'} {'New File'} {'Open...'} {'Share'} {'Email'} {'Message'} {'AirDrop'} {'Export'} {'PDF'} {'PNG'} {'SVG'} {'Print...'} } ``` ### Menu Item Dialog Open a confirmation dialog from a menu item. This pattern is useful for destructive actions like delete that require user confirmation. **Example: menu-item-dialog** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Menu } from 'ark-ripple/menu'; import { Portal } from 'ark-ripple/portal'; import { ChevronDown, X } from 'lucide-ripple'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import dialog from 'styles/dialog.module.css'; import styles from 'styles/menu.module.css'; export component MenuItemDialog() { let dialogOpen = track(false); {'Open Dialog'} {'Settings'} {'Configure your preferences below.'} {'Select theme'} {'Light'} {'Dark'} {'System'} {'Actions'} {'Edit'} {'Duplicate'} { @dialogOpen = true; }} > {'Delete...'} { @dialogOpen = e.open; }} role="alertdialog" > } ``` ## Guides ### Custom IDs Ark UI autogenerates ids for menu items internally. Passing a custom `id` prop breaks the internal `getElementById` functionality used by the component. ```tsx // ❌ Don't do this{'Confirm Delete'} {'Are you sure you want to delete this item? This action cannot be undone.'} Custom Item // ✅ Do thisCustom Item ``` ### Links To render a menu item as a link, render the link as the menu item itself using `asChild`, not as a child of the menu item. > This pattern ensures the link element receives the correct ARIA attributes and keyboard interactions from the menu > item. Here's an example of a reusable `MenuItemLink` component: ```tsx interface MenuItemLinkProps extends Menu.ItemProps { href?: string target?: string } export component MenuItemLink({ href, target, children, ...rest }: MenuItemLinkProps) { return (component asChild({ propsFn }) { <@children /> } ) } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `anchorPoint` | `Point` | No | The positioning point for the menu. Can be set by the context menu trigger or the button trigger. | | `aria-label` | `string` | No | The accessibility label for the menu | | `closeOnSelect` | `boolean` | No | Whether to close the menu when an option is selected | | `composite` | `boolean` | No | Whether the menu is a composed with other composite widgets like a combobox or tabs | | `defaultHighlightedValue` | `string` | No | The initial highlighted value of the menu item when rendered. Use when you don't need to control the highlighted value of the menu item. | | `defaultOpen` | `boolean` | No | The initial open state of the menu when rendered. Use when you don't need to control the open state of the menu. | | `highlightedValue` | `string` | No | The controlled highlighted value of the menu item. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ trigger: string contextTrigger: string content: string groupLabel: (id: string) => string group: (id: string) => string positioner: string arrow: string }>` | No | The ids of the elements in the menu. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `loopFocus` | `boolean` | No | Whether to loop the keyboard navigation. | | `navigate` | `(details: NavigateDetails) => void` | No | Function to navigate to the selected item if it's an anchor element | | `onEscapeKeyDown` | `(event: KeyboardEvent) => void` | No | Function called when the escape key is pressed | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component | | `onHighlightChange` | `(details: HighlightChangeDetails) => void` | No | Function called when the highlighted menu item changes. | | `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function called when the menu opens or closes | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component | | `onRequestDismiss` | `(event: LayerDismissEvent) => void` | No | Function called when this layer is closed due to a parent layer being closed | | `onSelect` | `(details: SelectionDetails) => void` | No | Function called when a menu item is selected. | | `open` | `boolean` | No | The controlled open state of the menu | | `positioning` | `PositioningOptions` | No | The options used to dynamically position the menu | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `typeahead` | `boolean` | No | Whether the pressing printable characters should trigger typeahead navigation | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Arrow Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Arrow CSS Variables:** | Variable | Description | |----------|-------------| | `--arrow-size` | The size of the arrow | | `--arrow-size-half` | Half the size of the arrow | | `--arrow-background` | Use this variable to style the arrow background | | `--arrow-offset` | The offset position of the arrow | **ArrowTip Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **CheckboxItem Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `checked` | `boolean` | Yes | Whether the option is checked | | `value` | `string` | Yes | The value of the option | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `closeOnSelect` | `boolean` | No | Whether the menu should be closed when the option is selected. | | `disabled` | `boolean` | No | Whether the menu item is disabled | | `onCheckedChange` | `(checked: boolean) => void` | No | Function called when the option state is changed | | `valueText` | `string` | No | The textual value of the option. Used in typeahead navigation of the menu. If not provided, the text content of the menu item will be used. | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | menu | | `[data-part]` | content | | `[data-state]` | "open" | "closed" | | `[data-nested]` | menu | | `[data-has-nested]` | menu | | `[data-placement]` | The placement of the content | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | | `--nested-layer-count` | The number of nested menus | **ContextTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ContextTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | menu | | `[data-part]` | context-trigger | | `[data-state]` | "open" | "closed" | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Indicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | menu | | `[data-part]` | indicator | | `[data-state]` | "open" | "closed" | **ItemGroupLabel Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemIndicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemIndicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | menu | | `[data-part]` | item-indicator | | `[data-disabled]` | Present when disabled | | `[data-highlighted]` | Present when highlighted | | `[data-state]` | "checked" | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `string` | Yes | The unique value of the menu item option. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `closeOnSelect` | `boolean` | No | Whether the menu should be closed when the option is selected. | | `disabled` | `boolean` | No | Whether the menu item is disabled | | `onSelect` | `VoidFunction` | No | The function to call when the item is selected | | `valueText` | `string` | No | The textual value of the option. Used in typeahead navigation of the menu. If not provided, the text content of the menu item will be used. | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | menu | | `[data-part]` | item | | `[data-disabled]` | Present when disabled | | `[data-highlighted]` | Present when highlighted | | `[data-value]` | The value of the item | | `[data-valuetext]` | The human-readable value | **ItemText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | menu | | `[data-part]` | item-text | | `[data-disabled]` | Present when disabled | | `[data-highlighted]` | Present when highlighted | | `[data-state]` | "checked" | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner CSS Variables:** | Variable | Description | |----------|-------------| | `--reference-width` | The width of the reference element | | `--reference-height` | The height of the root | | `--available-width` | The available width in viewport | | `--available-height` | The available height in viewport | | `--x` | The x position for transform | | `--y` | The y position for transform | | `--z-index` | The z-index value | | `--transform-origin` | The transform origin for animations | **RadioItemGroup Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `onValueChange` | `(e: ValueChangeDetails) => void` | No | | | `value` | `string` | No | | **RadioItem Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `string` | Yes | The value of the option | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `closeOnSelect` | `boolean` | No | Whether the menu should be closed when the option is selected. | | `disabled` | `boolean` | No | Whether the menu item is disabled | | `valueText` | `string` | No | The textual value of the option. Used in typeahead navigation of the menu. If not provided, the text content of the menu item will be used. | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseMenuReturn` | Yes | | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Separator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **TriggerItem Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | menu | | `[data-part]` | trigger | | `[data-placement]` | The placement of the trigger | | `[data-controls]` | | | `[data-state]` | "open" | "closed" | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `open` | `boolean` | Whether the menu is open | | `setOpen` | `(open: boolean) => void` | Function to open or close the menu | | `highlightedValue` | `string` | The id of the currently highlighted menuitem | | `setHighlightedValue` | `(value: string) => void` | Function to set the highlighted menuitem | | `setParent` | `(parent: ParentMenuService) => void` | Function to register a parent menu. This is used for submenus | | `setChild` | `(child: ChildMenuService) => void` | Function to register a child menu. This is used for submenus | | `reposition` | `(options?: Partial) => void` | Function to reposition the popover | | `getOptionItemState` | `(props: OptionItemProps) => OptionItemState` | Returns the state of the option item | | `getItemState` | `(props: ItemProps) => ItemState` | Returns the state of the menu item | | `addItemListener` | `(props: ItemListenerProps) => VoidFunction` | Setup the custom event listener for item selection event | ## Accessibility Complies with the [Menu WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/). ### Keyboard Support **`Space`** Description: Activates/Selects the highlighted item **`Enter`** Description: Activates/Selects the highlighted item **`ArrowDown`** Description: Highlights the next item in the menu **`ArrowUp`** Description: Highlights the previous item in the menu **`ArrowRight + ArrowLeft`** Description: When focus is on trigger, opens or closes the submenu depending on reading direction. **`Esc`** Description: Closes the menu and moves focus to the trigger # Number Input ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { NumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown } from 'lucide-ripple'; import styles from 'styles/number-input.module.css'; export component Basic() { } ``` ### Min and Max Pass the `min` prop or `max` prop to set an upper and lower limit for the input. By default, the input will restrict the value to stay within the specified range. **Example: min-max** ```ripple import { NumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown } from 'lucide-ripple'; import styles from 'styles/number-input.module.css'; export component MinMax() { {'Label'} } ``` > To allow values outside the min/max range, set `clampValueOnBlur` to `false`. ### Precision In some cases, you might need the value to be rounded to specific decimal points. Set the `formatOptions` and provide `Intl.NumberFormatOptions` such as `maximumFractionDigits` or `minimumFractionDigits`. **Example: fraction-digits** ```ripple import { NumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown } from 'lucide-ripple'; import styles from 'styles/number-input.module.css'; export component FractionDigits() { {'Label'} } ``` ### Scrubbing The NumberInput supports the scrubber interaction pattern. To use this pattern, render the `NumberInput.Scrubber` component. It uses the Pointer lock API and tracks the pointer movement. It also renders a virtual cursor which mimics the real cursor's pointer. **Example: scrubber** ```ripple import { NumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown, ArrowLeftRight } from 'lucide-ripple'; import styles from 'styles/number-input.module.css'; export component Scrubber() { {'Label'} } ``` ### Mouse Wheel The NumberInput exposes a way to increment/decrement the value using the mouse wheel event. To activate this, set the `allowMouseWheel` prop to `true`. **Example: mouse-wheel** ```ripple import { NumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown } from 'lucide-ripple'; import styles from 'styles/number-input.module.css'; export component MouseWheel() { {'Label'} } ``` ### Formatting To apply custom formatting to the input's value, set the `formatOptions` and provide `Intl.NumberFormatOptions` such as `style` and `currency`. **Example: formatting** ```ripple import { NumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown } from 'lucide-ripple'; import styles from 'styles/number-input.module.css'; export component Formatting() { {'Label'} } ``` ### Field The `Field` component helps manage form-related state and accessibility attributes of a number input. It includes handling ARIA labels, helper text, and error text to ensure proper accessibility. **Example: with-field** ```ripple import { Field } from 'ark-ripple/field'; import { NumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/number-input.module.css'; export component WithField() { {'Label'} } ``` ### Root Provider An alternative way to control the number input is to use the `RootProvider` component and the `useNumberInput` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { NumberInput, useNumberInput } from 'ark-ripple/number-input'; import { ChevronUp, ChevronDown } from 'lucide-ripple'; import styles from 'styles/number-input.module.css'; export component RootProvider() { const numberInput = useNumberInput(); {'Label'} {'Additional Info'} {'Error Info'} } ``` ## Guides ### Scrubber The `NumberInput.Scrubber` component provides an interactive scrub area that allows users to drag to change the input value. It renders as a `{'Label'} ` element and displays a custom cursor element during scrubbing interactions. This component utilizes the [Pointer Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API) for smooth dragging interactions. > **Note:** Browsers may show a notification when the Pointer Lock API is activated. The scrubber is automatically > disabled in Safari to prevent layout shifts. ### Controlled When controlling the NumberInput component, it's recommended to use string values instead of converting to numbers. This is especially important when using `formatOptions` for currency or locale-specific formatting. ```tsx const [value, setValue] = useState('0')setValue(details.value)}> {/* ... */} ``` Converting values to numbers can cause issues with locale-specific formatting, particularly for currencies that use different decimal and thousands separators (e.g., `1.523,30` vs `1,523.30`). By keeping values as strings, you preserve the correct formatting and avoid parsing issues. If you need to submit a numeric value in your form, use a hidden input that reads `valueAsNumber` from `NumberInput.Context`: ```tsxsetValue(details.value)}> ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `allowMouseWheel` | `boolean` | No | Whether to allow mouse wheel to change the value | | `allowOverflow` | `boolean` | No | Whether to allow the value overflow the min/max range | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `clampValueOnBlur` | `boolean` | No | Whether to clamp the value when the input loses focus (blur) | | `defaultValue` | `string` | No | The initial value of the input when rendered. Use when you don't need to control the value of the input. | | `disabled` | `boolean` | No | Whether the number input is disabled. | | `focusInputOnChange` | `boolean` | No | Whether to focus input when the value changes | | `form` | `string` | No | The associate form of the input element. | | `formatOptions` | `NumberFormatOptions` | No | The options to pass to the `Intl.NumberFormat` constructor | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string label: string input: string incrementTrigger: string decrementTrigger: string scrubber: string }>` | No | The ids of the elements in the number input. Useful for composition. | | `inputMode` | `InputMode` | No | Hints at the type of data that might be entered by the user. It also determines the type of keyboard shown to the user on mobile devices | | `invalid` | `boolean` | No | Whether the number input value is invalid. | | `locale` | `string` | No | The current locale. Based on the BCP 47 definition. | | `max` | `number` | No | The maximum value of the number input | | `min` | `number` | No | The minimum value of the number input | | `name` | `string` | No | The name attribute of the number input. Useful for form submission. | | `onFocusChange` | `(details: FocusChangeDetails) => void` | No | Function invoked when the number input is focused | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Function invoked when the value changes | | `onValueCommit` | `(details: ValueChangeDetails) => void` | No | Function invoked when the value is committed (when the input is blurred or the Enter key is pressed) | | `onValueInvalid` | `(details: ValueInvalidDetails) => void` | No | Function invoked when the value overflows or underflows the min/max range | | `pattern` | `string` | No | The pattern used to check the element's value against | | `readOnly` | `boolean` | No | Whether the number input is readonly | | `required` | `boolean` | No | Whether the number input is required | | `spinOnPress` | `boolean` | No | Whether to spin the value when the increment/decrement button is pressed | | `step` | `number` | No | The amount to increment or decrement the value by | | `translations` | `IntlTranslations` | No | Specifies the localized strings that identifies the accessibility elements and their states | | `value` | `string` | No | The controlled value of the input | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | root | | `[data-disabled]` | Present when disabled | | `[data-focus]` | Present when focused | | `[data-invalid]` | Present when invalid | | `[data-scrubbing]` | | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | control | | `[data-focus]` | Present when focused | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-scrubbing]` | | **DecrementTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **DecrementTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | decrement-trigger | | `[data-disabled]` | Present when disabled | | `[data-scrubbing]` | | **IncrementTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **IncrementTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | increment-trigger | | `[data-disabled]` | Present when disabled | | `[data-scrubbing]` | | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | input | | `[data-invalid]` | Present when invalid | | `[data-disabled]` | Present when disabled | | `[data-scrubbing]` | | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | label | | `[data-disabled]` | Present when disabled | | `[data-focus]` | Present when focused | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | | `[data-scrubbing]` | | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseNumberInputReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Scrubber Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Scrubber Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | scrubber | | `[data-disabled]` | Present when disabled | | `[data-scrubbing]` | | **ValueText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ValueText Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | number-input | | `[data-part]` | value-text | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-focus]` | Present when focused | | `[data-scrubbing]` | | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `focused` | `boolean` | Whether the input is focused. | | `invalid` | `boolean` | Whether the input is invalid. | | `empty` | `boolean` | Whether the input value is empty. | | `value` | `string` | The formatted value of the input. | | `valueAsNumber` | `number` | The value of the input as a number. | | `setValue` | `(value: number) => void` | Function to set the value of the input. | | `clearValue` | `VoidFunction` | Function to clear the value of the input. | | `increment` | `VoidFunction` | Function to increment the value of the input by the step. | | `decrement` | `VoidFunction` | Function to decrement the value of the input by the step. | | `setToMax` | `VoidFunction` | Function to set the value of the input to the max. | | `setToMin` | `VoidFunction` | Function to set the value of the input to the min. | | `focus` | `VoidFunction` | Function to focus the input. | ## Accessibility Complies with the [Spinbutton WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/). ### Keyboard Support **`ArrowUp`** Description: Increments the value of the number input by a predefined step. **`ArrowDown`** Description: Decrements the value of the number input by a predefined step. **`PageUp`** Description: Increments the value of the number input by a larger predefined step. **`PageDown`** Description: Decrements the value of the number input by a larger predefined step. **`Home`** Description: Sets the value of the number input to its minimum allowed value. **`End`** Description: Sets the value of the number input to its maximum allowed value. **`Enter`** Description: Submits the value entered in the number input. # Pagination ## Anatomy ```tsx{(context) => } ``` ## Examples **Example: basic** ```ripple import { Pagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component Basic() { } ``` ### Controlled To create a controlled Pagination component, manage the state of the current page using the `page` prop and update it when the `onPageChange` event handler is called: **Example: controlled** ```ripple import { track } from 'ripple'; import { Pagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component Controlled() { let currentPage = track(1); component children({ context }) { for (const [index, page] of @context.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else {{'…'} } } }{ @currentPage = details.page; }} class={styles.Root} > } ``` ### Root Provider An alternative way to control the pagination is to use the `RootProvider` component and the `usePagination` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Pagination, usePagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component RootProvider() { const pagination = usePagination({ count: 5000, pageSize: 10, siblingCount: 2 });component children({ context }) { for (const [index, page] of @context.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else {{'…'} } } } } ``` ### Customization You can customize the Pagination component by setting various props such as `dir`, `pageSize`, `siblingCount`, and `translations`. Here's an example of a customized Pagination: **Example: customized** ```ripple import { Pagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component Customized() {component children({ context }) { for (const [index, page] of @context.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else {{'…'} } } }`Page ${details.page}`, }} class={styles.Root} > } ``` ### Context Access pagination state and methods with `Pagination.Context` or the `usePaginationContext` hook. You get methods like `setPage`, `setPageSize`, `goToNextPage`, `goToPrevPage`, `goToFirstPage`, `goToLastPage`, as well as properties like `totalPages` and `pageRange`. **Example: context** ```ripple import { Pagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component Context() {component children({ context }) { for (const [index, page] of @context.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else {{'…'} } } }} ``` ### Data Slicing Use the `slice()` method to paginate actual data arrays. This method automatically slices your data based on the current page and page size. **Example: data-slicing** ```ripple import { Pagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; const users = [ { id: 1, name: 'Emma Wilson', email: 'emma@example.com' }, { id: 2, name: 'Liam Johnson', email: 'liam@example.com' }, { id: 3, name: 'Olivia Brown', email: 'olivia@example.com' }, { id: 4, name: 'Noah Davis', email: 'noah@example.com' }, { id: 5, name: 'Ava Martinez', email: 'ava@example.com' }, { id: 6, name: 'Ethan Garcia', email: 'ethan@example.com' }, { id: 7, name: 'Sophia Rodriguez', email: 'sophia@example.com' }, { id: 8, name: 'Mason Lee', email: 'mason@example.com' }, { id: 9, name: 'Isabella Walker', email: 'isabella@example.com' }, { id: 10, name: 'James Hall', email: 'james@example.com' }, { id: 11, name: 'Mia Allen', email: 'mia@example.com' }, { id: 12, name: 'Benjamin Young', email: 'benjamin@example.com' }, { id: 13, name: 'Charlotte King', email: 'charlotte@example.com' }, { id: 14, name: 'William Wright', email: 'william@example.com' }, { id: 15, name: 'Amelia Scott', email: 'amelia@example.com' }, { id: 16, name: 'Henry Green', email: 'henry@example.com' }, { id: 17, name: 'Harper Adams', email: 'harper@example.com' }, { id: 18, name: 'Sebastian Baker', email: 'sebastian@example.com' }, { id: 19, name: 'Evelyn Nelson', email: 'evelyn@example.com' }, { id: 20, name: 'Jack Carter', email: 'jack@example.com' }, ]; export component DataSlicing() { component children({ context }) { }{'Page '} {@context.page} {' of '} {@context.totalPages}
} ``` ### Page Range Display the current page range information using the `pageRange` property. This shows which items are currently visible (e.g., "Showing 1-10 of 100 results"). **Example: page-range** ```ripple import { Pagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component PageRange() { component children({ context }) { for (const user of @context.slice(users); key user.id) {{user.name} {user.email}}}for (const [index, page] of @context.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else {{'…'} } }} ``` ### Page Size Control the number of items per page dynamically using `setPageSize()`. This example shows how to integrate a native select element to change the page size. > **Note:** For uncontrolled behavior, use `defaultPageSize` to set the initial value. For controlled behavior, use > `pageSize` and `onPageSizeChange` to programmatically manage the page size. **Example: page-size-control** ```ripple import { Pagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component PageSizeControl() { component children({ context }) { for (const [index, page] of @context.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else {{'…'} } } }{'Showing '} {@context.pageRange.start + 1} {'-'} {@context.pageRange.end} {' of '} {@context.count} {' results'}
{'Page '} {@context.page} {' of '} {@context.totalPages}
} ``` ### Links Create pagination with link navigation for better SEO and accessibility. This example shows how to use the pagination component with anchor links instead of buttons. **Example: link** ```ripple import { Pagination, usePagination } from 'ark-ripple/pagination'; import { ChevronLeft, ChevronRight } from 'lucide-ripple'; import styles from 'styles/pagination.module.css'; export component Link() { const pagination = usePagination( { type: 'link', count: 100, pageSize: 10, siblingCount: 2, getPageUrl: ({ page }) => `/page=${page}`, }, ); component children({ context }) { for (const [index, page] of @context.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else {{'…'} } }{'Page '} {@context.page} {' of '} {@context.totalPages}
}} ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `boundaryCount` | `number` | No | Number of pages to show at the beginning and end | | `count` | `number` | No | Total number of data items | | `defaultPage` | `number` | No | The initial active page when rendered. Use when you don't need to control the active page of the pagination. | | `defaultPageSize` | `number` | No | The initial number of data items per page when rendered. Use when you don't need to control the page size of the pagination. | | `getPageUrl` | `(details: PageUrlDetails) => string` | No | Function to generate href attributes for pagination links. Only used when `type` is set to "link". | | `ids` | `Partial<{ root: string ellipsis: (index: number) => string firstTrigger: string prevTrigger: string nextTrigger: string lastTrigger: string item: (page: number) => string }>` | No | The ids of the elements in the accordion. Useful for composition. | | `onPageChange` | `(details: PageChangeDetails) => void` | No | Called when the page number is changed | | `onPageSizeChange` | `(details: PageSizeChangeDetails) => void` | No | Called when the page size is changed | | `page` | `number` | No | The controlled active page | | `pageSize` | `number` | No | The controlled number of data items per page | | `siblingCount` | `number` | No | Number of pages to show beside active page | | `translations` | `IntlTranslations` | No | Specifies the localized strings that identifies the accessibility elements and their states | | `type` | `'button' | 'link'` | No | The type of the trigger element | **Ellipsis Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `index` | `number` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **FirstTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **FirstTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pagination | | `[data-part]` | first-trigger | | `[data-disabled]` | Present when disabled | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `type` | `'page'` | Yes | | | `value` | `number` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pagination | | `[data-part]` | item | | `[data-index]` | The index of the item | | `[data-selected]` | Present when selected | **LastTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **LastTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pagination | | `[data-part]` | last-trigger | | `[data-disabled]` | Present when disabled | **NextTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **NextTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pagination | | `[data-part]` | next-trigger | | `[data-disabled]` | Present when disabled | **PrevTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **PrevTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pagination | | `[data-part]` | prev-trigger | | `[data-disabled]` | Present when disabled | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UsePaginationReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `page` | `number` | The current page. | | `count` | `number` | The total number of data items. | | `pageSize` | `number` | The number of data items per page. | | `totalPages` | `number` | The total number of pages. | | `pages` | `Pages` | The page range. Represented as an array of page numbers (including ellipsis) | | `previousPage` | `number` | The previous page. | | `nextPage` | `number` | The next page. | | `pageRange` | `PageRange` | The page range. Represented as an object with `start` and `end` properties. | | `slice` | ` for (const [index, page] of @pagination.pages.entries(); key page.value) { if (page.type === 'page') { {page.value} } else { {'…'} } } (data: V[]) => V[]` | Function to slice an array of data based on the current page. | | `setPageSize` | `(size: number) => void` | Function to set the page size. | | `setPage` | `(page: number) => void` | Function to set the current page. | | `goToNextPage` | `VoidFunction` | Function to go to the next page. | | `goToPrevPage` | `VoidFunction` | Function to go to the previous page. | | `goToFirstPage` | `VoidFunction` | Function to go to the first page. | | `goToLastPage` | `VoidFunction` | Function to go to the last page. | # Password Input ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { PasswordInput } from 'ark-ripple/password-input'; import { Eye, EyeOff } from 'lucide-ripple'; import styles from 'styles/password-input.module.css'; export component Basic() { } ``` ### Autocomplete Use the `autoComplete` prop to manage autocompletion in the input. - `new-password` — The user is creating a new password. - `current-password` — The user is entering an existing password. **Example: autocomplete** ```ripple import { PasswordInput } from 'ark-ripple/password-input'; import { Eye, EyeOff } from 'lucide-ripple'; import styles from 'styles/password-input.module.css'; export component Autocomplete() { {'Password'} } ``` ### Controlled Visibility Use the `visible` and `onVisibilityChange` props to control the visibility of the password input. **Example: controlled-visibility** ```ripple import { PasswordInput } from 'ark-ripple/password-input'; import { Eye, EyeOff } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/password-input.module.css'; export component ControlledVisibility() { let visible = track(false); {'Password'} { @visible = e.visible; }} > } ``` ### Root Provider An alternative way to control the password input is to use the `RootProvider` component and the `usePasswordInput` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { PasswordInput, usePasswordInput } from 'ark-ripple/password-input'; import { Eye, EyeOff } from 'lucide-ripple'; import styles from 'styles/password-input.module.css'; export component RootProvider() { const passwordInput = usePasswordInput();{'Password is '} {@visible ? 'visible' : 'hidden'} } ``` ### Field Here's an example of how to use the `PasswordInput` component with the `Field` component. **Example: with-field** ```ripple import { Field } from 'ark-ripple/field'; import { PasswordInput } from 'ark-ripple/password-input'; import { Eye, EyeOff } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/password-input.module.css'; export component WithField() {{'Password'} } ``` ### Password Managers Use the `ignorePasswordManager` prop to ignore password managers like 1Password, LastPass, etc. This is useful for non-login scenarios (e.g., "api keys", "secure notes", "temporary passwords") > **Currently, this only works for 1Password, LastPass, Bitwarden, Dashlane, and Proton Pass.** **Example: ignore-password-manager** ```ripple import { PasswordInput } from 'ark-ripple/password-input'; import { Eye, EyeOff } from 'lucide-ripple'; import styles from 'styles/password-input.module.css'; export component IgnorePasswordManager() { {'Password'} {'Enter your password'} {'Password is required'} } ``` ### Strength Meter Combine the `PasswordInput` with a password strength library to show visual feedback about password strength. This example uses the [`check-password-strength`](https://www.npmjs.com/package/check-password-strength) package to provide real-time strength validation. **Example: strength-meter** ```ripple import { PasswordInput } from 'ark-ripple/password-input'; import { passwordStrength, type Options } from 'check-password-strength'; import { Eye, EyeOff } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/password-input.module.css'; const strengthOptions: Options {'API Key'} = [ { id: 0, value: 'weak', minDiversity: 0, minLength: 0 }, { id: 1, value: 'medium', minDiversity: 2, minLength: 6 }, { id: 2, value: 'strong', minDiversity: 4, minLength: 8 }, ]; export component StrengthMeter() { let password = track('asdfasdf'); let strength = track(() => { if (!@password) return null; const { value } = passwordStrength(@password, strengthOptions); return value; }); } ``` ### Validation Combine with custom validation logic to show real-time feedback. Use the `invalid` prop to indicate validation errors. **Example: with-validation** ```ripple import { PasswordInput } from 'ark-ripple/password-input'; import { Eye, EyeOff } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/password-input.module.css'; export component WithValidation() { let password = track(''); let isValid = track(() => @password.length >= 8); {'Password'} if (@strength) { { @password = e.currentTarget.value; }} placeholder="Enter your password" /> }{@strength} {' password'}0}> } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoComplete` | `'current-password' | 'new-password'` | No | The autocomplete attribute for the password input. | | `defaultVisible` | `boolean` | No | The default visibility of the password input. | | `disabled` | `boolean` | No | Whether the password input is disabled. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ input: string; visibilityTrigger: string }>` | No | The ids of the password input parts | | `ignorePasswordManagers` | `boolean` | No | When `true`, the input will ignore password managers. **Only works for the following password managers** - 1Password, LastPass, Bitwarden, Dashlane, Proton Pass | | `invalid` | `boolean` | No | The invalid state of the password input. | | `name` | `string` | No | The name of the password input. | | `onVisibilityChange` | `(details: VisibilityChangeDetails) => void` | No | Function called when the visibility changes. | | `readOnly` | `boolean` | No | Whether the password input is read only. | | `required` | `boolean` | No | Whether the password input is required. | | `translations` | `Partial<{ visibilityTrigger: ((visible: boolean) => string) | undefined }>` | No | The localized messages to use. | | `visible` | `boolean` | No | Whether the password input is visible. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | password-input | | `[data-part]` | root | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | password-input | | `[data-part]` | control | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `fallback` | `string | number | bigint | boolean | ReactElement{'Password (min 8 characters)'} if (@password.length > 0 && !@isValid) { { @password = e.target.value; }} placeholder="Enter your password" /> {'Password must be at least 8 characters'}
} if (@isValid && @password.length > 0) {{'Password is valid'}
}> | Iterable | ReactPortal | Promise<...>` | No | The fallback content to display when the password is not visible. | **Indicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | password-input | | `[data-part]` | indicator | | `[data-state]` | "visible" | "hidden" | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | password-input | | `[data-part]` | input | | `[data-state]` | "visible" | "hidden" | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | password-input | | `[data-part]` | label | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-readonly]` | Present when read-only | | `[data-required]` | Present when required | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UsePasswordInputReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **VisibilityTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **VisibilityTrigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | password-input | | `[data-part]` | visibility-trigger | | `[data-readonly]` | Present when read-only | | `[data-disabled]` | Present when disabled | | `[data-state]` | "visible" | "hidden" | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `visible` | `boolean` | Whether the password input is visible. | | `disabled` | `boolean` | Whether the password input is disabled. | | `invalid` | `boolean` | Whether the password input is invalid. | | `focus` | `VoidFunction` | Focus the password input. | | `setVisible` | `(value: boolean) => void` | Set the visibility of the password input. | | `toggleVisible` | `VoidFunction` | Toggle the visibility of the password input. | # Pin Input ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { PinInput } from 'ark-ripple/pin-input'; import styles from 'styles/pin-input.module.css'; export component Basic() { } ``` ### Placeholder To customize the default pin input placeholder `○` for each input, pass the placeholder prop and set it to your desired value. **Example: custom-placeholder** ```ripple import { PinInput } from 'ark-ripple/pin-input'; import styles from 'styles/pin-input.module.css'; export component CustomPlaceholder() { {'Label'} for (const index of [0, 1, 2]; key index) { } } ``` ### Blur on Complete By default, the last input maintains focus when filled, and we invoke the `onValueComplete` callback. To blur the last input when the user completes the input, set the prop `blurOnComplete` to `true`. **Example: blur-on-complete** ```ripple import { PinInput } from 'ark-ripple/pin-input'; import styles from 'styles/pin-input.module.css'; export component BlurOnComplete() { {'Label'} for (const index of [0, 1, 2]; key index) { } } ``` ### OTP Mode To trigger smartphone OTP auto-suggestion, it is recommended to set the `autocomplete` attribute to "one-time-code". The pin input component provides support for this automatically when you set the `otp` prop to true. **Example: otp-mode** ```ripple import { PinInput } from 'ark-ripple/pin-input'; import styles from 'styles/pin-input.module.css'; export component OTPMode() { {'Label'} for (const index of [0, 1, 2]; key index) { } } ``` ### Masking When collecting private or sensitive information using the pin input, you might need to mask the value entered, similar to ``. Pass the `mask` prop to `true`. **Example: mask** ```ripple import { PinInput } from 'ark-ripple/pin-input'; import styles from 'styles/pin-input.module.css'; export component Mask() { {'Label'} for (const index of [0, 1, 2]; key index) { } } ``` ### Change Events The pin input component invokes several callback functions when the user enters: - `onValueChange` — Callback invoked when the value is changed. - `onValueComplete` — Callback invoked when all fields have been completed (by typing or pasting). - `onValueInvalid` — Callback invoked when an invalid value is entered into the input. An invalid value is any value that doesn't match the specified "type". ### Field The `Field` component helps manage form-related state and accessibility attributes of a pin input. It includes handling ARIA labels, helper text, and error text to ensure proper accessibility. **Example: with-field** ```ripple import { Field } from 'ark-ripple/field'; import { PinInput } from 'ark-ripple/pin-input'; import fieldStyles from 'styles/field.module.css'; import styles from 'styles/pin-input.module.css'; export component WithField() { {'Label'} for (const index of [0, 1, 2]; key index) { } } ``` ### Root Provider An alternative way to control the pin input is to use the `RootProvider` component and the `usePinInput` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { PinInput, usePinInput } from 'ark-ripple/pin-input'; import styles from 'styles/pin-input.module.css'; export component RootProvider() { const pinInput = usePinInput( { onValueComplete: (e: PinInput.ValueChangeDetails) => alert(e.valueAsString) }, ); {'Label'} for (const index of [0, 1, 2]; key index) { } {'Additional Info'} {'Error Info'} } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoFocus` | `boolean` | No | Whether to auto-focus the first input. | | `blurOnComplete` | `boolean` | No | Whether to blur the input when the value is complete | | `count` | `number` | No | The number of inputs to render to improve SSR aria attributes. This will be required in next major version. | | `defaultValue` | `string[]` | No | The initial value of the the pin input when rendered. Use when you don't need to control the value of the pin input. | | `disabled` | `boolean` | No | Whether the inputs are disabled | | `form` | `string` | No | The associate form of the underlying input element. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string hiddenInput: string label: string control: string input: (id: string) => string }>` | No | The ids of the elements in the pin input. Useful for composition. | | `invalid` | `boolean` | No | Whether the pin input is in the invalid state | | `mask` | `boolean` | No | If `true`, the input's value will be masked just like `type=password` | | `name` | `string` | No | The name of the input element. Useful for form submission. | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Function called on input change | | `onValueComplete` | `(details: ValueChangeDetails) => void` | No | Function called when all inputs have valid values | | `onValueInvalid` | `(details: ValueInvalidDetails) => void` | No | Function called when an invalid value is entered | | `otp` | `boolean` | No | If `true`, the pin input component signals to its fields that they should use `autocomplete="one-time-code"`. | | `pattern` | `string` | No | The regular expression that the user-entered input value is checked against. | | `placeholder` | `string` | No | The placeholder text for the input | | `readOnly` | `boolean` | No | Whether the pin input is in the valid state | | `required` | `boolean` | No | Whether the pin input is required | | `selectOnFocus` | `boolean` | No | Whether to select input value when input is focused | | `translations` | `IntlTranslations` | No | Specifies the localized strings that identifies the accessibility elements and their states | | `type` | `'numeric' | 'alphanumeric' | 'alphabetic'` | No | The type of value the pin-input should allow | | `value` | `string[]` | No | The controlled value of the the pin input. | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pin-input | | `[data-part]` | root | | `[data-invalid]` | Present when invalid | | `[data-disabled]` | Present when disabled | | `[data-complete]` | Present when the pin-input value is complete | | `[data-readonly]` | Present when read-only | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **HiddenInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `index` | `number` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Input Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pin-input | | `[data-part]` | input | | `[data-disabled]` | Present when disabled | | `[data-complete]` | Present when the input value is complete | | `[data-index]` | The index of the item | | `[data-invalid]` | Present when invalid | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | pin-input | | `[data-part]` | label | | `[data-invalid]` | Present when invalid | | `[data-disabled]` | Present when disabled | | `[data-complete]` | Present when the label value is complete | | `[data-required]` | Present when required | | `[data-readonly]` | Present when read-only | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UsePinInputReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `value` | `string[]` | The value of the input as an array of strings. | | `valueAsString` | `string` | The value of the input as a string. | | `complete` | `boolean` | Whether all inputs are filled. | | `count` | `number` | The number of inputs to render | | `items` | `number[]` | The array of input values. | | `setValue` | `(value: string[]) => void` | Function to set the value of the inputs. | | `clearValue` | `VoidFunction` | Function to clear the value of the inputs. | | `setValueAtIndex` | `(index: number, value: string) => void` | Function to set the value of the input at a specific index. | | `focus` | `VoidFunction` | Function to focus the pin-input. This will focus the first input. | ## Accessibility ### Keyboard Support **`ArrowLeft`** Description: Moves focus to the previous input **`ArrowRight`** Description: Moves focus to the next input **`Backspace`** Description: Deletes the value in the current input and moves focus to the previous input **`Delete`** Description: Deletes the value in the current input **`Control + V`** Description: Pastes the value into the input fields # Popover ## Anatomy ```tsx{'Label'} for (const index of [0, 1, 2]; key index) { } ``` ## Examples **Example: basic** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component Basic() { } ``` ### Controlled Use the `open` and `onOpenChange` props to control the open state of the popover. **Example: controlled** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component Controlled() { let open = track(false); {'Click Me'} {'Favorite Frameworks'} {'Manage and organize your favorite web frameworks.'} { @open = e.open; }} > } ``` ### Root Provider An alternative way to control the popover is to use the `RootProvider` component and the `usePopover` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Popover, usePopover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component RootProvider() { const popover = usePopover( { positioning: { placement: 'bottom-start', }, }, );{'Click Me'} {'✕'} {'Team Members'} {'Invite colleagues to collaborate on this project.'} } ``` ### Arrow Use `Popover.Arrow` and `Popover.ArrowTip` to render an arrow pointing to the trigger. **Example: arrow** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component Arrow() {{'Popover is '} {@popover.open ? 'open' : 'closed'}{'Toggle Popover'} {'Controlled Externally'} {'This popover is controlled via the usePopover hook.'} } ``` ### Placement To change the placement of the popover, set the `positioning` prop. **Example: positioning** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component Positioning() { {'Click Me'} {'✕'} {'Notifications'} {'You have 3 unread messages in your inbox.'} } ``` ### Close Behavior The popover is designed to close on blur and when the esc key is pressed. - To prevent it from closing on blur (clicking or focusing outside), pass the `closeOnInteractOutside` prop and set it to `false`. - To prevent it from closing when the esc key is pressed, pass the `closeOnEsc` prop and set it to `false`. **Example: close-behavior** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component CloseBehavior() { {'Click Me'} {'✕'} {'Left Placement'} {'This popover appears on the left with custom offset values.'} } ``` ### Modality In some cases, you might want the popover to be modal. This means that it'll: - trap focus within its content - block scrolling on the body - disable pointer interactions outside the popover - hide content behind the popover from screen readers To make the popover modal, set the `modal` prop to `true`. When `modal={true}`, we set the `portalled` attribute to `true` as well. **Example: modal** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component Modal() { {'Click Me'} {'✕'} {'Quick Actions'} {'Press Escape or click outside to close this popover.'} } ``` ### Anchor Use `Popover.Anchor` to position the popover relative to a different element than the trigger. **Example: anchor** ```ripple import { Popover } from 'ark-ripple/popover'; import button from 'styles/button.module.css'; import field from 'styles/field.module.css'; import styles from 'styles/popover.module.css'; export component Anchor() { {'Click Me'} {'✕'} {'Confirm Action'} {'Focus is trapped inside this modal popover until dismissed.'} } ``` ### Same Width Use `positioning.sameWidth` to make the popover match the width of its trigger element. **Example: same-width** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component SameWidth() { {'Click Me'} {'✕'} {'Title'} {'Description'} } ``` ### Dialog Integration When rendering a popover inside a dialog, you have two options for proper layering: 1. **Keep the Portal with `lazyMount` and `unmountOnExit`** - This ensures the popover is properly unmounted when the dialog closes, preventing stale DOM nodes. 2. **Remove the Portal** - Render the popover inline within the dialog content. This works well but may have z-index considerations. **Example: with-dialog** ```ripple import { Dialog } from 'ark-ripple/dialog'; import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import dialog from 'styles/dialog.module.css'; import styles from 'styles/popover.module.css'; export component WithDialog() { {'Click Me'} {'✕'} {'Matched Width'} {'This popover matches the width of its trigger element.'} } ``` ### Nested Popovers can be nested within each other. Each nested popover maintains its own open state and positioning. **Example: nested** ```ripple import { Popover } from 'ark-ripple/popover'; import { Portal } from 'ark-ripple/portal'; import button from 'styles/button.module.css'; import styles from 'styles/popover.module.css'; export component Nested() { {'Open Dialog'} {'✕'} {'Edit Profile'} {'Update your profile information below.'} {'More Options'} {'✕'} {'Additional Settings'} {'This popover renders correctly above the dialog.'} } ``` ## Guides ### Available Size The following css variables are exposed to the `Popover.Positioner` which you can use to style the `Popover.Content` ```css /* width of the popover trigger */ --reference-width: {'Click Me'} {'Settings'} {'Manage your preferences and account settings.'} {'Advanced'} {'Advanced Settings'} {'Configure advanced options for power users.'} ; /* width of the available viewport */ --available-width: ; /* height of the available viewport */ --available-height: ; ``` For example, if you want to make sure the maximum height doesn't exceed the available height, use the following css: ```css [data-scope='popover'][data-part='content'] { max-height: calc(var(--available-height) - 100px); } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `autoFocus` | `boolean` | No | Whether to automatically set focus on the first focusable content within the popover when opened. | | `closeOnEscape` | `boolean` | No | Whether to close the popover when the escape key is pressed. | | `closeOnInteractOutside` | `boolean` | No | Whether to close the popover when the user clicks outside of the popover. | | `defaultOpen` | `boolean` | No | The initial open state of the popover when rendered. Use when you don't need to control the open state of the popover. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ anchor: string trigger: string content: string title: string description: string closeTrigger: string positioner: string arrow: string }>` | No | The ids of the elements in the popover. Useful for composition. | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `initialFocusEl` | `() => HTMLElement | null` | No | The element to focus on when the popover is opened. | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `modal` | `boolean` | No | Whether the popover should be modal. When set to `true`: - interaction with outside elements will be disabled - only popover content will be visible to screen readers - scrolling is blocked - focus is trapped within the popover | | `onEscapeKeyDown` | `(event: KeyboardEvent) => void` | No | Function called when the escape key is pressed | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component | | `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component | | `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function invoked when the popover opens or closes | | `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component | | `onRequestDismiss` | `(event: LayerDismissEvent) => void` | No | Function called when this layer is closed due to a parent layer being closed | | `open` | `boolean` | No | The controlled open state of the popover | | `persistentElements` | `(() => Element | null)[]` | No | Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event | | `portalled` | `boolean` | No | Whether the popover is portalled. This will proxy the tabbing behavior regardless of the DOM position of the popover content. | | `positioning` | `PositioningOptions` | No | The user provided options used to position the popover content | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Anchor Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Arrow Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Arrow CSS Variables:** | Variable | Description | |----------|-------------| | `--arrow-size` | The size of the arrow | | `--arrow-size-half` | Half the size of the arrow | | `--arrow-background` | Use this variable to style the arrow background | | `--arrow-offset` | The offset position of the arrow | **ArrowTip Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **CloseTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | popover | | `[data-part]` | content | | `[data-state]` | "open" | "closed" | | `[data-nested]` | popover | | `[data-has-nested]` | popover | | `[data-expanded]` | Present when expanded | | `[data-placement]` | The placement of the content | **Content CSS Variables:** | Variable | Description | |----------|-------------| | `--layer-index` | The index of the dismissable in the layer stack | | `--nested-layer-count` | The number of nested popovers | **Description Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Indicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | popover | | `[data-part]` | indicator | | `[data-state]` | "open" | "closed" | **Positioner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Positioner CSS Variables:** | Variable | Description | |----------|-------------| | `--reference-width` | The width of the reference element | | `--reference-height` | The height of the root | | `--available-width` | The available width in viewport | | `--available-height` | The available height in viewport | | `--x` | The x position for transform | | `--y` | The y position for transform | | `--z-index` | The z-index value | | `--transform-origin` | The transform origin for animations | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UsePopoverReturn` | Yes | | | `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame | | `lazyMount` | `boolean` | No | Whether to enable lazy mounting | | `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state | | `present` | `boolean` | No | Whether the node is present (controlled by the user) | | `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. | | `unmountOnExit` | `boolean` | No | Whether to unmount on exit. | **Title Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Trigger Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | popover | | `[data-part]` | trigger | | `[data-placement]` | The placement of the trigger | | `[data-state]` | "open" | "closed" | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `portalled` | `boolean` | Whether the popover is portalled. | | `open` | `boolean` | Whether the popover is open | | `setOpen` | `(open: boolean) => void` | Function to open or close the popover | | `reposition` | `(options?: Partial ) => void` | Function to reposition the popover | ## Accessibility ### Keyboard Support **`Space`** Description: Opens/closes the popover. **`Enter`** Description: Opens/closes the popover. **`Tab`** Description: Moves focus to the next focusable element within the content.
Note: If there are no focusable elements, focus is moved to the next focusable element after the trigger. **`Shift + Tab`** Description: Moves focus to the previous focusable element within the content
Note: If there are no focusable elements, focus is moved to the trigger. **`Esc`** Description: Closes the popover and moves focus to the trigger. # QR Code ## Anatomy ```tsx``` ## Examples **Example: basic** ```ripple import { QrCode } from 'ark-ripple/qr-code'; import styles from 'styles/qr-code.module.css'; export component Basic() { } ``` ### With Overlay You can also add a logo or overlay to the QR code. This is useful when you want to brand the QR code. **Example: overlay** ```ripple import { QrCode } from 'ark-ripple/qr-code'; import styles from 'styles/qr-code.module.css'; export component Overlay() { } ``` ### Error Correction In cases where the link is too long or the logo overlay covers a significant area, the error correction level can be increased. Use the `encoding.ecc` or `encoding.boostEcc` property to set the error correction level: - `L`: Allows recovery of up to 7% data loss (default) - `M`: Allows recovery of up to 15% data loss - `Q`: Allows recovery of up to 25% data loss - `H`: Allows recovery of up to 30% data loss **Example: error-correction** ```ripple import { QrCode } from 'ark-ripple/qr-code'; import { RadioGroup } from 'ark-ripple/radio-group'; import { track } from 'ripple'; import styles from 'styles/qr-code.module.css'; import radio from 'styles/radio-group.module.css'; export component ErrorCorrection() { let errorLevel = track('L');
} ``` ### Root Provider An alternative way to control the QR code is to use the `RootProvider` component and the `useQrCode` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { QrCode, useQrCode } from 'ark-ripple/qr-code'; import styles from 'styles/qr-code.module.css'; export component RootProvider() { const qrCode = useQrCode({ value: 'http://ark-ui.com' });{ @errorLevel = e.value; }} > for (const level of ['L', 'M', 'Q', 'H']; key level) {} {level} } ``` ### Download Use the `QrCode.DownloadTrigger` component to allow users to download the QR code as an image. Specify the `fileName` and `mimeType` props for the downloaded file. **Example: download** ```ripple import { QrCode } from 'ark-ripple/qr-code'; import button from 'styles/button.module.css'; import styles from 'styles/qr-code.module.css'; export component Download() {} ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `defaultValue` | `string` | No | The initial value to encode when rendered. Use when you don't need to control the value of the qr code. | | `encoding` | `QrCodeGenerateOptions` | No | The qr code encoding options. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string; frame: string }>` | No | The element ids. | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Callback fired when the value changes. | | `pixelSize` | `number` | No | The pixel size of the qr code. | | `value` | `string` | No | The controlled value to encode. | **Root CSS Variables:** | Variable | Description | |----------|-------------| | `--qrcode-pixel-size` | The size of the Root | | `--qrcode-width` | The width of the Root | | `--qrcode-height` | The height of the Root | **DownloadTrigger Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `fileName` | `string` | Yes | The name of the file. | | `mimeType` | `DataUrlType` | Yes | The mime type of the image. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `quality` | `number` | No | The quality of the image. | **Frame Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Overlay Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Pattern Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseQrCodeReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `value` | `string` | The value to encode. | | `setValue` | `(value: string) => void` | Set the value to encode. | | `getDataUrl` | `(type: DataUrlType, quality?: number) => Promise {'Download'} ` | Returns the data URL of the qr code. | # Radio Group ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { RadioGroup } from 'ark-ripple/radio-group'; import styles from 'styles/radio-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component Basic() { } ``` ### Initial Value To set the radio group's initial value, set the `defaultValue` prop to the value of the radio item to be selected by default. **Example: initial-value** ```ripple import { RadioGroup } from 'ark-ripple/radio-group'; import styles from 'styles/radio-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component InitialValue() { {'Framework'} for (const framework of frameworks; key framework) {} {framework} } ``` ### Controlled For a controlled Radio Group, the state is managed using the `value` prop, and updates when the `onValueChange` event handler is called: **Example: controlled** ```ripple import { RadioGroup } from 'ark-ripple/radio-group'; import { track } from 'ripple'; import styles from 'styles/radio-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component Controlled() { let value = track {'Framework'} for (const framework of frameworks; key framework) {} {framework} (null); { @value = e.value; }} > } ``` ### Root Provider An alternative way to control the radio group is to use the `RootProvider` component and the `useRadioGroup` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { RadioGroup, useRadioGroup } from 'ark-ripple/radio-group'; import button from 'styles/button.module.css'; import styles from 'styles/radio-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component RootProvider() { const radioGroup = useRadioGroup({ defaultValue: 'React' });{'Framework'} for (const framework of frameworks; key framework) {} {framework} } ``` ### Disabled To make a radio group disabled, set the `disabled` prop to `true`. **Example: disabled** ```ripple import { RadioGroup } from 'ark-ripple/radio-group'; import styles from 'styles/radio-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component Disabled() {{'Framework'} for (const framework of frameworks; key framework) {} {framework} } ``` ## Guides ### asChild The `RadioGroup.Item` component renders as a `label` element by default. This ensures proper form semantics and accessibility, as radio groups are form controls that require labels to provide meaningful context for users. When using the `asChild` prop, you must **render a `label` element** as the direct child of `RadioGroup.Item` to maintain valid HTML structure and accessibility compliance. ```tsx // INCORRECT usage ❌ {'Framework'} for (const framework of frameworks; key framework) {} {framework} // CORRECT usage ✅ ``` ### Hidden Input The `RadioGroup.ItemHiddenInput` component renders a hidden HTML input element that enables proper form submission and integration with native form behaviors. This component is essential for the radio group to function correctly as it: - Provides the underlying input element that browsers use for form submission - Enables integration with form libraries and validation systems - Ensures the radio group works with native form reset functionality ```tsx // INCORRECT usage ❌ // CORRECT usage ✅ ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `defaultValue` | `string` | No | The initial value of the checked radio when rendered. Use when you don't need to control the value of the radio group. | | `disabled` | `boolean` | No | If `true`, the radio group will be disabled | | `form` | `string` | No | The associate form of the underlying input. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string label: string indicator: string item: (value: string) => string itemLabel: (value: string) => string itemControl: (value: string) => string itemHiddenInput: (value: string) => string }>` | No | The ids of the elements in the radio. Useful for composition. | | `invalid` | `boolean` | No | If `true`, the radio group is marked as invalid. | | `name` | `string` | No | The name of the input fields in the radio (Useful for form submission). | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Function called once a radio is checked | | `orientation` | `'horizontal' | 'vertical'` | No | Orientation of the radio group | | `readOnly` | `boolean` | No | Whether the radio group is read-only | | `required` | `boolean` | No | If `true`, the radio group is marked as required. | | `value` | `string` | No | The controlled value of the radio group | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | radio-group | | `[data-part]` | root | | `[data-orientation]` | The orientation of the radio-group | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | **Indicator Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Indicator Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | radio-group | | `[data-part]` | indicator | | `[data-disabled]` | Present when disabled | | `[data-orientation]` | The orientation of the indicator | **Indicator CSS Variables:** | Variable | Description | |----------|-------------| | `--transition-property` | The transition property value for the Indicator | | `--left` | The left position value | | `--top` | The top position value | | `--width` | The width of the element | | `--height` | The height of the element | **ItemControl Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **ItemControl Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | radio-group | | `[data-part]` | item-control | | `[data-active]` | Present when active or pressed | **ItemHiddenInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `string` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `disabled` | `boolean` | No | | | `invalid` | `boolean` | No | | **ItemText Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | radio-group | | `[data-part]` | label | | `[data-orientation]` | The orientation of the label | | `[data-disabled]` | Present when disabled | | `[data-invalid]` | Present when invalid | | `[data-required]` | Present when required | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseRadioGroupReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `value` | `string` | The current value of the radio group | | `setValue` | `(value: string) => void` | Function to set the value of the radio group | | `clearValue` | `VoidFunction` | Function to clear the value of the radio group | | `focus` | `VoidFunction` | Function to focus the radio group | | `getItemState` | `(props: ItemProps) => ItemState` | Returns the state details of a radio input | ## Accessibility Complies with the [Radio WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/). ### Keyboard Support **`Tab`** Description: Moves focus to either the checked radio item or the first radio item in the group. **`Space`** Description: When focus is on an unchecked radio item, checks it. **`ArrowDown`** Description: Moves focus and checks the next radio item in the group. **`ArrowRight`** Description: Moves focus and checks the next radio item in the group. **`ArrowUp`** Description: Moves focus to the previous radio item in the group. **`ArrowLeft`** Description: Moves focus to the previous radio item in the group. # Rating Group ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { RatingGroup } from 'ark-ripple/rating-group'; import { Star } from 'lucide-ripple'; import styles from 'styles/rating-group.module.css'; export component Basic() { } ``` ### Controlled When using the `RatingGroup` component, you can use the `value` and `onValueChange` props to control the state. **Example: controlled** ```ripple import { RatingGroup } from 'ark-ripple/rating-group'; import { Star } from 'lucide-ripple'; import { track } from 'ripple'; import styles from 'styles/rating-group.module.css'; export component Controlled() { let value = track(0); {'Label'} component children({ context }) { for (const item of @context.items; key item) { } } component children({ context }) { } { @value = details.value; }} > } ``` ### Root Provider An alternative way to control the rating group is to use the `RootProvider` component and the `useRatingGroup` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { RatingGroup, useRatingGroup } from 'ark-ripple/rating-group'; import { Star } from 'lucide-ripple'; import styles from 'styles/rating-group.module.css'; export component RootProvider() { const ratingGroup = useRatingGroup({ count: 5, defaultValue: 3 });{'Label'} component children({ context }) { for (const item of @context.items; key item) { } } component children({ context }) { } } ``` ### Field The `Field` component helps manage form-related state and accessibility attributes of a rating group. It includes handling ARIA labels, helper text, and error text to ensure proper accessibility. **Example: with-field** ```ripple import { Field } from 'ark-ripple/field'; import { RatingGroup } from 'ark-ripple/rating-group'; import { Star } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/rating-group.module.css'; export component WithField() {{'Label'} component children({ context }) { for (const item of @context.items; key item) { } } component children({ context }) { } } ``` ### Half Rating Allow `0.5` value steps by setting the `allowHalf` prop to `true`. Ensure to render the correct icon if the `half` value is set in the Rating components render callback. **Example: half-star** ```ripple import { RatingGroup } from 'ark-ripple/rating-group'; import { Star } from 'lucide-ripple'; import styles from 'styles/rating-group.module.css'; export component HalfStar() { {'Label'} component children({ context }) { for (const item of @context.items; key item) { } } component children({ context }) { } {'Additional Info'} {'Error Info'} } ``` ### Forms To use the rating group within forms, pass the prop `name`. It will render a hidden input and ensure the value changes get propagated to the form correctly. **Example: form-usage** ```ripple import { RatingGroup } from 'ark-ripple/rating-group'; import { Star } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/rating-group.module.css'; export component FormUsage() { } ``` ### Disabled To make the rating group disabled, set the `disabled` prop to `true`. **Example: disabled** ```ripple import { RatingGroup } from 'ark-ripple/rating-group'; import { Star } from 'lucide-ripple'; import styles from 'styles/rating-group.module.css'; export component Disabled() { {'Label'} component children({ context }) { for (const item of @context.items; key item) { } } component children({ context }) { } } ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `allowHalf` | `boolean` | No | Whether to allow half stars. | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `autoFocus` | `boolean` | No | Whether to autofocus the rating. | | `count` | `number` | No | The total number of ratings. | | `defaultValue` | `number` | No | The initial value of the rating when rendered. Use when you don't need to control the value of the rating. | | `disabled` | `boolean` | No | Whether the rating is disabled. | | `form` | `string` | No | The associate form of the underlying input element. | | `id` | `string` | No | The unique identifier of the machine. | | `ids` | `Partial<{ root: string label: string hiddenInput: string control: string item: (id: string) => string }>` | No | The ids of the elements in the rating. Useful for composition. | | `name` | `string` | No | The name attribute of the rating element (used in forms). | | `onHoverChange` | `(details: HoverChangeDetails) => void` | No | Function to be called when the rating value is hovered. | | `onValueChange` | `(details: ValueChangeDetails) => void` | No | Function to be called when the rating value changes. | | `readOnly` | `boolean` | No | Whether the rating is readonly. | | `required` | `boolean` | No | Whether the rating is required. | | `translations` | `IntlTranslations` | No | Specifies the localized strings that identifies the accessibility elements and their states | | `value` | `number` | No | The controlled value of the rating | **Control Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Control Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | rating-group | | `[data-part]` | control | | `[data-readonly]` | Present when read-only | | `[data-disabled]` | Present when disabled | **HiddenInput Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Item Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `index` | `number` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Item Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | rating-group | | `[data-part]` | item | | `[data-disabled]` | Present when disabled | | `[data-readonly]` | Present when read-only | | `[data-checked]` | Present when checked | | `[data-highlighted]` | Present when highlighted | | `[data-half]` | | **Label Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Label Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | rating-group | | `[data-part]` | label | | `[data-disabled]` | Present when disabled | | `[data-required]` | Present when required | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseRatingGroupReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `setValue` | `(value: number) => void` | Sets the value of the rating group | | `clearValue` | `VoidFunction` | Clears the value of the rating group | | `hovering` | `boolean` | Whether the rating group is being hovered | | `value` | `number` | The current value of the rating group | | `hoveredValue` | `number` | The value of the currently hovered rating | | `count` | `number` | The total number of ratings | | `items` | `number[]` | The array of rating values. Returns an array of numbers from 1 to the max value. | | `getItemState` | `(props: ItemProps) => ItemState` | Returns the state of a rating item | ## Accessibility ### Keyboard Support **`ArrowRight`** Description: Moves focus to the next star, increasing the rating value based on the `allowHalf` property. **`ArrowLeft`** Description: Moves focus to the previous star, decreasing the rating value based on the `allowHalf` property. **`Enter`** Description: Selects the focused star in the rating group. # Scroll Area ## Anatomy ```tsx {'Label'} component children({ context }) { for (const item of @context.items; key item) { } } component children({ context }) { } ``` ## Required style It's important to note that the scroll area requires the following styles on the `ScrollArea.Viewport` element to hide the native scrollbar: ```css [data-scope='scroll-area'][data-part='viewport'] { scrollbar-width: none; &::-webkit-scrollbar { display: none; } } ``` ## Examples ### Basic Create a basic scrollable area with custom scrollbar. **Example: basic** ```ripple import { ScrollArea } from 'ark-ripple/scroll-area'; import styles from 'styles/scroll-area.module.css'; export component Basic() { } ``` ### Horizontal Configure the scroll area for horizontal scrolling only. **Example: horizontal** ```ripple import { ScrollArea } from 'ark-ripple/scroll-area'; import styles from 'styles/scroll-area.module.css'; export component Horizontal() { {'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'}
} ``` ### Both Directions Enable scrolling in both horizontal and vertical directions. **Example: both-directions** ```ripple import { ScrollArea } from 'ark-ripple/scroll-area'; import styles from 'styles/scroll-area.module.css'; export component BothDirections() { {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'}
} ``` ### Nested Scroll areas can be nested within each other for complex layouts. **Example: nested** ```ripple import { ScrollArea } from 'ark-ripple/scroll-area'; import styles from 'styles/scroll-area.module.css'; export component Nested() { {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'}
} ``` ## API Reference ### Props **Component API Reference** **Root Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `ids` | `Partial<{ root: string; viewport: string; content: string; scrollbar: string; thumb: string }>` | No | The ids of the scroll area elements | **Root Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | scroll-area | | `[data-part]` | root | | `[data-overflow-x]` | Present when the content overflows the x-axis | | `[data-overflow-y]` | Present when the content overflows the y-axis | **Root CSS Variables:** | Variable | Description | |----------|-------------| | `--corner-width` | The width of the Root | | `--corner-height` | The height of the Root | | `--thumb-width` | The width of the slider thumb | | `--thumb-height` | The height of the slider thumb | **Content Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Content Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | scroll-area | | `[data-part]` | content | | `[data-overflow-x]` | Present when the content overflows the x-axis | | `[data-overflow-y]` | Present when the content overflows the y-axis | **Corner Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Corner Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | scroll-area | | `[data-part]` | corner | | `[data-hover]` | Present when hovered | | `[data-state]` | "hidden" | "visible" | | `[data-overflow-x]` | Present when the content overflows the x-axis | | `[data-overflow-y]` | Present when the content overflows the y-axis | **RootProvider Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `value` | `UseScrollAreaReturn` | Yes | | | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Scrollbar Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | | `orientation` | `Orientation` | No | | **Scrollbar Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | scroll-area | | `[data-part]` | scrollbar | | `[data-orientation]` | The orientation of the scrollbar | | `[data-scrolling]` | Present when scrolling | | `[data-hover]` | Present when hovered | | `[data-dragging]` | Present when in the dragging state | | `[data-overflow-x]` | Present when the content overflows the x-axis | | `[data-overflow-y]` | Present when the content overflows the y-axis | **Thumb Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Thumb Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | scroll-area | | `[data-part]` | thumb | | `[data-orientation]` | The orientation of the thumb | | `[data-hover]` | Present when hovered | | `[data-dragging]` | Present when in the dragging state | **Viewport Props:** | Prop | Type | Required | Description | |------|------|----------|-------------| | `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. | **Viewport Data Attributes:** | Attribute | Value | |-----------|-------| | `[data-scope]` | scroll-area | | `[data-part]` | viewport | | `[data-at-top]` | Present when scrolled to the top edge | | `[data-at-bottom]` | Present when scrolled to the bottom edge | | `[data-at-left]` | Present when scrolled to the left edge | | `[data-at-right]` | Present when scrolled to the right edge | | `[data-overflow-x]` | Present when the content overflows the x-axis | | `[data-overflow-y]` | Present when the content overflows the y-axis | **Viewport CSS Variables:** | Variable | Description | |----------|-------------| | `--scroll-area-overflow-x-start` | The distance from the horizontal start edge in pixels | | `--scroll-area-overflow-x-end` | The distance from the horizontal end edge in pixels | | `--scroll-area-overflow-y-start` | The distance from the vertical start edge in pixels | | `--scroll-area-overflow-y-end` | The distance from the vertical end edge in pixels | ### Context **API:** | Property | Type | Description | |----------|------|-------------| | `isAtTop` | `boolean` | Whether the scroll area is at the top | | `isAtBottom` | `boolean` | Whether the scroll area is at the bottom | | `isAtLeft` | `boolean` | Whether the scroll area is at the left | | `isAtRight` | `boolean` | Whether the scroll area is at the right | | `hasOverflowX` | `boolean` | Whether the scroll area has horizontal overflow | | `hasOverflowY` | `boolean` | Whether the scroll area has vertical overflow | | `getScrollProgress` | `() => Point` | Get the scroll progress as values between 0 and 1 | | `scrollToEdge` | `(details: ScrollToEdgeDetails) => void` | Scroll to the edge of the scroll area | | `scrollTo` | `(details: ScrollToDetails) => void` | Scroll to specific coordinates | | `getScrollbarState` | `(props: ScrollbarProps) => ScrollbarState` | Returns the state of the scrollbar | # Segment Group ## Anatomy ```tsx {'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.'}
{'This is a nested scroll area. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'}
``` ## Examples **Example: basic** ```ripple import { SegmentGroup } from 'ark-ripple/segment-group'; import styles from 'styles/segment-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component Basic() { } ``` ### Controlled To create a controlled SegmentGroup component, manage the current selected segment using the `value` prop and update it when the `onValueChange` event handler is called: **Example: controlled** ```ripple import { SegmentGroup } from 'ark-ripple/segment-group'; import { track } from 'ripple'; import styles from 'styles/segment-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component Controlled() { let value = track for (const framework of frameworks; key framework) { } {framework} (null); { @value = e.value; }} > } ``` ### Root Provider An alternative way to control the segment group is to use the `RootProvider` component and the `useSegmentGroup` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { SegmentGroup, useSegmentGroup } from 'ark-ripple/segment-group'; import styles from 'styles/segment-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component RootProvider() { const segmentGroup = useSegmentGroup({ defaultValue: 'React' });for (const framework of frameworks; key framework) { } {framework} } ``` ### Disabled To disable a segment, simply pass the `disabled` prop to the `SegmentGroup.Item` component: **Example: disabled** ```ripple import { SegmentGroup } from 'ark-ripple/segment-group'; import styles from 'styles/segment-group.module.css'; const frameworks = ['React', 'Solid', 'Vue']; export component Disabled() {for (const framework of frameworks; key framework) { } {framework} } ``` ## API Reference for (const framework of frameworks; key framework) { } {framework} ## Accessibility Complies with the [Radio WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/). ### Keyboard Support **`Tab`** Description: Moves focus to either the checked radio item or the first radio item in the group. **`Space`** Description: When focus is on an unchecked radio item, checks it. **`ArrowDown`** Description: Moves focus and checks the next radio item in the group. **`ArrowRight`** Description: Moves focus and checks the next radio item in the group. **`ArrowUp`** Description: Moves focus to the previous radio item in the group. **`ArrowLeft`** Description: Moves focus to the previous radio item in the group. # Select ## Anatomy ```tsx ``` ## Examples **Example: basic** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const frameworks = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, { label: 'Svelte', value: 'svelte' }, ], }, ); export component Basic() { } ``` ### Controlled Use the `value` and `onValueChange` props to control the selected items. **Example: controlled** ```ripple import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/select.module.css'; interface Item { label: string; value: string; disabled?: boolean | undefined; } const collection = createListCollection {'Framework'} {'Frameworks'} for (const item of frameworks.items; key item.value) {} {item.label} {'✓'} - ( { items: [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, { label: 'Svelte', value: 'svelte', disabled: true }, ], }, ); export component Controlled() { let value = track
([]); const setValue = (v: string[]) => { @value = v; }; setValue(e.value)} > } ``` ### Root Provider An alternative way to control the select is to use the `RootProvider` component and the `useSelect` hook. This way you can access the state and methods from outside the component. **Example: root-provider** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection, useSelect } from 'ark-ripple/select'; import { ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const frameworks = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, { label: 'Svelte', value: 'svelte' }, ], }, ); export component RootProvider() { const select = useSelect({ collection: frameworks });{'Framework'} {'Frameworks'} for (const item of collection.items; key item.value) {} {item.label} {'✓'} } ``` ### Multiple To enable `multiple` item selection: **Example: multiple** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const frameworks = createListCollection( { items: [ { label: 'React', value: 'react' }, { label: 'Solid', value: 'solid' }, { label: 'Vue', value: 'vue' }, { label: 'Svelte', value: 'svelte', disabled: true }, ], }, ); export component Multiple() {{'Framework'} {'Frameworks'} for (const item of frameworks.items; key item.value) {} {item.label} {'✓'} } ``` ### Grouping Grouping related options can be useful for organizing options into categories. - Use the `groupBy` prop to configure the grouping of the items. - Use the `collection.group()` method to get the grouped items. - Use the `Select.ItemGroup` and `Select.ItemGroupLabel` components to render the grouped items. **Example: grouping** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const frameworks = createListCollection( { items: [ { label: 'React', value: 'react', type: 'JS' }, { label: 'Solid', value: 'solid', type: 'JS' }, { label: 'Vue', value: 'vue', type: 'JS' }, { label: 'Panda', value: 'panda', type: 'CSS' }, { label: 'Tailwind', value: 'tailwind', type: 'CSS' }, ], groupBy: (item: any) => item.type, }, ); export component Grouping() { {'Framework'} {'Frameworks'} for (const item of frameworks.items; key item.value) {} {item.label} {'✓'} } ``` ### Field Use `Field` to manage form state, ARIA labels, helper text, and error text. **Example: with-field** ```ripple import { Select, createListCollection } from 'ark-ripple/select'; import { Field } from 'ark-ripple/field'; import { ChevronsUpDown } from 'lucide-ripple'; import field from 'styles/field.module.css'; import styles from 'styles/select.module.css'; const collection = createListCollection({ items: ['React', 'Solid', 'Vue', 'Svelte'] }); export component WithField() { {'Framework'} for (const [type, group] of frameworks.group(); key type) { } {type} for (const item of group; key item.value) {} {item.label} {'✓'} } ``` ### Form Usage Here's an example of integrating the `Select` component with a form library. **Example: form-library** ```ripple import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown, X } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/select.module.css'; const collection = createListCollection( { items: ['React', 'Solid', 'Vue', 'Svelte'], }, ); export component FormLibrary() { let value = track {'Label'} for (const item of collection.items; key item) { } {item} {'✓'} {'Additional Info'} {'Error Info'} (['React']); const setValue = (v: string[]) => { @value = v; }; const handleSubmit = (e: any) => { e.preventDefault(); window.alert(JSON.stringify(new FormData(e.currentTarget).getAll('framework'))); }; // TODO: Add form library instead of native form } ``` ### Async Loading Here's an example of how to load the items asynchronously when the select is opened. **Example: async** ```ripple import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/select.module.css'; function loadData() { return new Promise ((resolve) => { setTimeout(() => resolve(['React', 'Solid', 'Vue', 'Svelte', 'Angular', 'Ember']), 500); }); } export component Async() { let data = track (null); let loading = track(false); let error = track (null); const collection = track(() => createListCollection ({ items: @data ?? [] })); const setData = (v: string[] | null) => { @data = v; }; const setLoading = (v: boolean) => { @loading = v; }; const setError = (v: Error | null) => { @error = v; }; const handleOpenChange = (details: any) => { if (details.open && @data === null) { setLoading(true); setError(null); loadData().then((data) => setData(data)).catch((err) => setError(err)).finally( () => setLoading(false), ); } }; } ``` ### Lazy Mount Use `lazyMount` and `unmountOnExit` to control when content is mounted, improving performance. **Example: lazy-mount** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const collection = createListCollection( { items: ['React', 'Solid', 'Vue', 'Svelte', 'Angular', 'Alpine'], }, ); export component LazyMount() { {'Framework'} if (@loading) { {'Loading...'}} else if (@error) {{'Error: '} {@error.message}} else { for (const item of @collection.items; key item) {} } {item} {'✓'} } ``` ### Select on Highlight Here's an example of automatically selecting items when they are highlighted (hovered or navigated to with keyboard). **Example: select-on-highlight** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection, useSelect } from 'ark-ripple/select'; import { ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const collection = createListCollection( { items: ['React', 'Solid', 'Vue', 'Svelte'], }, ); export component SelectOnHighlight() { const select = useSelect( { collection, onHighlightChange(details: any) { if (details.highlightedValue) { @select.selectValue(details.highlightedValue); } }, }, ); {'Framework'} {'Clear'} {'Frameworks'} for (const item of collection.items; key item) {} {item} {'✓'} } ``` ### Max Selection Here's an example of limiting the number of items that can be selected in a multiple select. **Example: max-selected** ```ripple import { Portal } from 'ark-ripple/portal'; import { track } from 'ripple'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown, X } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const items = ['React', 'Solid', 'Vue', 'Svelte']; const MAX_SELECTION = 2; const hasReachedMax = (value: string[]) => value.length >= MAX_SELECTION; export component MaxSelected() { let value = track {'Framework'} {'Clear'} {'Frameworks'} for (const item of collection.items; key item) {} {item} {'✓'} ([]); const setValue = (v: string[]) => { @value = v; }; const collection = track( () => createListCollection( { items: items.map( (item) => ({ label: item, value: item, disabled: hasReachedMax(@value) && !@value.includes(item), }), ), }, ), ); const handleValueChange = (details: Select.ValueChangeDetails) => { if (hasReachedMax(@value) && details.value.length > @value.length) return; setValue(details.value); }; } ``` ### Select All Use `selectAll()` from the select context to select all items at once. **Example: select-all** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection, useSelectContext } from 'ark-ripple/select'; import { ChevronsUpDown } from 'lucide-ripple'; import button from 'styles/button.module.css'; import styles from 'styles/select.module.css'; component SelectAllButton() { const select = useSelectContext(); {'Framework'} {'Frameworks'} for (const item of @collection.items; key item.value) {} {item.label} {'✓'} component children({ context }) { } } export component SelectAll() { const collection = createListCollection({ items: ['React', 'Solid', 'Vue', 'Svelte'] });} ``` ### Overflow For selects with many items, use `positioning.fitViewport` to ensure the dropdown fits within the viewport. Combine with a max-height on the content to enable scrolling. **Example: overflow** ```ripple import { Portal } from 'ark-ripple/portal'; import { Select, createListCollection } from 'ark-ripple/select'; import { ChevronsUpDown } from 'lucide-ripple'; import styles from 'styles/select.module.css'; const collection = createListCollection( { items: [ 'Name 1', 'Name 2', 'Name 3', 'Name 4', 'Name 5', 'Name 6', 'Name 7', 'Name 8', 'Name 9', 'Name 10', 'Name 11', 'Name 12', 'Name 13', 'Name 14', ], }, ); export component Overflow() { {'Framework'} {'Clear'} for (const item of collection.items; key item) { } {item} {'✓'} } ``` ## Guides ### Nested Usage When using the Select component within a `Popover` or `Dialog`, avoid rendering its content within a `Portal` or `Teleport`. This ensures the Select's content stays within the Popover/Dialog's DOM hierarchy rather than being portalled to the document body, maintaining proper interaction and accessibility behavior. ### Hidden Select The `Select.HiddenSelect` component renders a native HTML ` {'Framework'} {'Clear'} {'Names'} for (const item of collection.items; key item) {} {item} {'✓'}