Quick reference guide for all JavaScript components in SaladUI, their states, events, and special features.
Core Classes
Component (core/component.js)
Base class for all interactive components.
Properties:
el- Root DOM elementhook- LiveView hook contextstateMachine- State machine instanceoptions- Parsed fromdata-optionsallParts- Array of queryable parts
Methods:
getComponentConfig()- Override to define component behavior (must return a fresh object each call)transition(event, params)- Trigger state transitiongetPart(name)- Get single part by namegetAllParts(name)- Get all parts with namegetPartId(name)- Get or generate ID for partpushEvent(event, data)- Send event to serverhandleCommand(command, params)- Handle server commandssetupComponentEvents()- Override for custom event setup, called once bysetupEvents()teardownComponentEvents()- Override to undosetupComponentEvents(), called fromremoveAllEvents()afterMount()- Override for logic that needs live listeners, called once right aftersetupEvents()beforeDestroy()- Override for cleanup, called before listeners are removeddestroy()- Cleanup and remove listeners
See Component Lifecycle for the full contract between these hooks (which pair with which, and why).
StateMachine (core/state-machine.js)
State management with transitions.
Methods:
transition(event, params)- Execute transitiondetermineNextState(transition, params)- Resolve next stateexecuteTransition(prev, next, params)- Execute full transitionexecuteStateHandler(state, type, params)- Execute enter/exit handlers
Properties:
state- Current statepreviousState- Previous statestateConfig- State machine configuration
ComponentRegistry (core/factory.js)
Component registration and factory.
Methods:
register(type, ComponentClass)- Register component typecreate(type, el, hookContext)- Create component instance
SaladUIHook (core/hook.js)
Phoenix LiveView hook for component integration.
Lifecycle:
mounted()- Initialize componentupdated()- Reinitialize on DOM updatedestroyed()- Cleanup component
Interactive Components
Accordion (components/accordion.js)
Collapsible sections with keyboard navigation.
States:
idle- Normal state
Options:
allowMultiple- Allow multiple items open (default: false)defaultValue- Initially opened items
Events:
item-opened- Item was openeditem-closed- Item was closed
Parts:
root- Containeritem- Accordion itemitem-trigger- Item header/buttonitem-content- Item content panel
ARIA:
- Triggers have
role="button"andaria-expanded - Content has
role="region"andaria-labelledby
Command (components/command.js)
Command palette with search and keyboard navigation.
States:
idle- Normal statesearching- User is searching
Options:
filter- Custom filter functionplaceholder- Search placeholder text
Events:
select- Item was selectedsearch- Search query changed
Parts:
root- Containerinput- Search inputlist- Results listitem- Result itemempty- Empty state message
Keyboard:
↓- Navigate down↑- Navigate upEnter- Select itemEscape- Clear or close
Dialog (components/dialog.js)
Modal dialog with focus trap.
States:
closed- Dialog is closedopen- Dialog is open
Options:
closeOnOutsideClick- Close on overlay click (default: true)animations- Animation configurations
Events:
open- Dialog openedclose- Dialog closed
Parts:
root- Containertrigger- Open buttoncontent- Dialog containercontent-panel- Dialog paneloverlay- Backdropclose-trigger- Close buttontitle- Dialog titledescription- Dialog description
Features:
- Focus trap when open
- Escape key to close
- Click outside to close (optional)
ARIA:
role="dialog"on contentaria-modal="true"when openaria-labelledbyandaria-describedby
DropdownMenu (components/dropdown_menu.js)
Context menu with keyboard navigation.
States:
closed- Menu closedopen- Menu open
Options:
positioning- Menu positioning config
Events:
open- Menu openedclose- Menu closedselect- Item selected
Parts:
root- Containertrigger- Menu triggercontent- Menu panelitem- Menu itemseparator- Visual separatorlabel- Section label
Keyboard:
↓- Navigate down↑- Navigate upEnter/Space- Select itemEscape- Close menu
ARIA:
role="menu"on contentrole="menuitem"on itemsrole="separator"on separators
HoverCard (components/hover-card.js)
Contextual information on hover.
States:
closed- Card hiddenopen- Card visible
Options:
openDelay- Delay before opening (default: 200ms)closeDelay- Delay before closing (default: 300ms)
Events:
open- Card openedclose- Card closed
Parts:
root- Containertrigger- Hover targetcontent- Card content panel
Features:
- Delay timers for open/close
- Mouse enter/leave detection
- Positioning relative to trigger
Menu (components/menu.js)
Base menu component with navigation.
States:
closed- Menu closedopen- Menu open
Features:
- Keyboard navigation (arrow keys)
- Typeahead search
- Nested submenus
- Mouse and keyboard interaction
Parts:
root- Containertrigger- Menu triggercontent- Menu panelitem- Menu itemsubmenu- Nested submenuseparator- Visual divider
Popover (components/popover.js)
Floating content panel.
States:
closed- Popover closedopen- Popover open
Options:
positioning- Positioning configurationcloseOnOutsideClick- Close on outside click
Events:
open- Popover openedclose- Popover closed
Parts:
root- Containertrigger- Open buttoncontent- Popover panelclose-trigger- Close button
Features:
- Auto-positioning
- Click outside detection
- Escape to close
RadioGroup (components/radio_group.js)
Radio button group with keyboard navigation.
States:
idle- Normal state
Options:
defaultValue- Initially selected valueorientation-horizontalorvertical
Events:
value-changed- Selection changed
Parts:
root- Containeritem- Radio buttonindicator- Visual indicator
Keyboard:
↓/→- Next item↑/←- Previous itemSpace- Select item
ARIA:
role="radiogroup"on rootrole="radio"on itemsaria-checkedon selected item
ScrollArea (components/scroll_area.js)
Custom scrollbar component.
Features:
- Custom scrollbar styling
- Horizontal and vertical scrolling
- Auto-hide scrollbars
Parts:
root- Containerviewport- Scrollable areascrollbar- Scrollbar trackthumb- Scrollbar thumb
Select (components/select.js)
Dropdown select with search and keyboard navigation.
States:
closed- Select closedopen- Select open
Options:
searchable- Enable search (default: false)clearable- Show clear button (default: false)multiple- Multiple selection (default: false)placeholder- Placeholder text
Events:
open- Select openedclose- Select closedvalue-changed- Selection changedsearch- Search query changed
Parts:
root- Containertrigger- Select buttonvalue- Selected value displaycontent- Options panelsearch-input- Search input (if searchable)item- Select optionclear-button- Clear button (if clearable)
Keyboard:
↓- Navigate down↑- Navigate upEnter/Space- Select itemEscape- Close- Type to search (if searchable)
ARIA:
role="combobox"on triggerrole="listbox"on contentrole="option"on itemsaria-selectedon selected items
Slider (components/slider.js)
Range input slider.
States:
idle- Not being draggeddragging- Currently dragging
Options:
min- Minimum value (default: 0)max- Maximum value (default: 100)step- Step increment (default: 1)orientation-horizontalorverticalmultiple- Multiple thumbs
Events:
value-changed- Value changeddrag-start- Drag starteddrag-end- Drag ended
Parts:
root- Containertrack- Slider trackrange- Filled rangethumb- Draggable handle
Keyboard:
←/↓- Decrease value→/↑- Increase valueHome- Minimum valueEnd- Maximum valuePageUp/PageDown- Large increment
ARIA:
role="slider"on thumbaria-valuemin,aria-valuemax,aria-valuenowaria-orientation
Switch (components/switch.js)
Toggle switch component.
States:
unchecked- Switch offchecked- Switch on
Options:
defaultChecked- Initial state
Events:
checked- State changed
Parts:
root- Containerthumb- Switch thumbinput- Hidden input
Keyboard:
Space/Enter- Toggle
ARIA:
role="switch"on rootaria-checkedreflects state
Tabs (components/tabs.js)
Tabbed content interface.
States:
- Per tab:
activeorinactive
Options:
defaultValue- Initially active taborientation-horizontalorvertical
Events:
tab-changed- Active tab changed
Parts:
root- Containerlist- Tab list containertrigger- Tab buttoncontent- Tab panel
Keyboard:
←/→- Navigate tabs (horizontal)↑/↓- Navigate tabs (vertical)Home- First tabEnd- Last tab
ARIA:
role="tablist"on listrole="tab"on triggersrole="tabpanel"on contentaria-selectedon active tabaria-controlslinks trigger to panel
Tooltip (components/tooltip.js)
Hover tooltip with delay.
States:
closed- Tooltip hiddenopen- Tooltip visible
Options:
delay- Open delay (default: 200ms)closeDelay- Close delay (default: 0ms)
Events:
open- Tooltip openedclose- Tooltip closed
Parts:
root- Containertrigger- Hover targetcontent- Tooltip content
Features:
- Delay timers
- Auto-positioning
- Mouse tracking
ARIA:
role="tooltip"on contentaria-describedbyon trigger
Collapsible (components/collapsible.js)
Expandable content panel.
States:
collapsed- Content hiddenexpanded- Content visible
Options:
defaultOpen- Initially expanded
Events:
open- Content expandedclose- Content collapsed
Parts:
root- Containertrigger- Toggle buttoncontent- Collapsible content
Keyboard:
Space/Enter- Toggle
ARIA:
role="button"on triggeraria-expandedreflects statearia-controlslinks trigger to content
Chart (components/chart.js)
Chart.js integration component.
States:
idle- Normal stateloading- Loading data
Options:
- Chart.js configuration options
Events:
chart-created- Chart initializeddata-updated- Data refreshed
Commands:
update- Update chart dataresize- Resize chartdestroy- Destroy chart instance
Features:
- Wraps Chart.js library
- Dynamic data updates
- Responsive sizing
Utility Classes
FocusTrap (core/focus-trap.js)
Traps focus within an element.
Methods:
activate()- Enable focus trapdeactivate()- Disable focus trapdestroy()- Cleanup
ClickOutsideMonitor (core/click-outside.js)
Detects clicks outside elements.
Constructor:
new ClickOutsideMonitor(elements, callback)Methods:
start()- Start monitoringstop()- Stop monitoringdestroy()- Cleanup
Positioning Utilities (core/utils.js)
Functions:
animateTransition(config, element)- Apply CSS transitionsqueryDOM(root, filter)- Query DOM with filter function
Common Patterns
Basic Component Setup
import Component from "../core/component";
import SaladUI from "../index";
class MyComponent extends Component {
constructor(el, hookContext) {
super(el, { hookContext, initialState: "idle" });
}
getComponentConfig() {
return {
stateMachine: { /* ... */ },
events: { /* ... */ },
hiddenConfig: { /* ... */ },
ariaConfig: { /* ... */ }
};
}
}
SaladUI.register("my-component", MyComponent);
export default MyComponent;With Focus Trap
import FocusTrap from "../core/focus-trap";
onOpenEnter() {
if (!this.focusTrap) {
this.focusTrap = new FocusTrap(this.contentPanel);
}
this.focusTrap.activate();
}
onClosedEnter() {
this.focusTrap?.deactivate();
}
beforeDestroy() {
this.focusTrap?.destroy();
this.focusTrap = null;
}With Click Outside Detection
import ClickOutsideMonitor from "../core/click-outside";
setupComponentEvents() {
super.setupComponentEvents();
if (this.options.closeOnOutsideClick) {
this.clickOutsideMonitor = new ClickOutsideMonitor(
[this.contentPanel],
() => this.transition("close")
);
}
}
// Pair teardown with setup — not beforeDestroy(). teardownComponentEvents()
// also runs if setupEvents() is ever re-invoked, so this is the only place
// guaranteed to undo what setupComponentEvents() created. See
// docs/component_lifecycle.md#common-pitfalls for what goes wrong if this
// cleanup lives in beforeDestroy() instead.
teardownComponentEvents() {
super.teardownComponentEvents();
this.clickOutsideMonitor?.destroy();
this.clickOutsideMonitor = null;
}
onOpenEnter() {
// start()/stop() just pause/resume the monitor — they don't create or
// destroy it.
this.clickOutsideMonitor?.start();
}
onClosedEnter() {
this.clickOutsideMonitor?.stop();
}With Keyboard Navigation
constructor(el, hookContext) {
super(el, { hookContext });
this.currentIndex = 0;
this.items = [];
}
setupComponentEvents() {
super.setupComponentEvents();
this.items = this.getAllParts("item");
}
getEventConfig() {
return {
open: {
keyMap: {
ArrowDown: () => this.navigateNext(),
ArrowUp: () => this.navigatePrev(),
Enter: () => this.selectCurrent(),
Escape: "close"
}
}
};
}
navigateNext() {
this.currentIndex = Math.min(
this.currentIndex + 1,
this.items.length - 1
);
this.focusCurrentItem();
}
navigatePrev() {
this.currentIndex = Math.max(this.currentIndex - 1, 0);
this.focusCurrentItem();
}
focusCurrentItem() {
this.items[this.currentIndex]?.focus();
}Testing Components
Manual Testing
Use the storybook application:
cd storybook
mix phx.server
# Visit http://localhost:4000
Console Testing
// In browser console
const dialog = document.querySelector("[data-component='dialog']");
// Access component instance (if exposed)
const component = dialog._saladui_component;
// Or send commands
dialog.dispatchEvent(new CustomEvent("salad_ui:command", {
detail: { command: "open", params: {} }
}));Debug Mode
Add logging to components:
onStateChanged(prev, next) {
console.log(`State: ${prev} → ${next}`);
return super.onStateChanged(prev, next);
}
transition(event, params) {
console.log(`Transition: ${event}`, params);
return super.transition(event, params);
}Performance Tips
- Cache part references in constructor
- Use event delegation where possible
- Debounce expensive operations (search, positioning)
- Pair setup/teardown: undo
setupComponentEvents()inteardownComponentEvents(), and other cleanup inbeforeDestroy(), to prevent memory leaks - Minimize DOM queries in event handlers
- Use requestAnimationFrame for animations
- Lazy initialize expensive features (focus trap, positioning)
Common Pitfalls
- Forgetting to register component - Always call
SaladUI.register() - Not cleaning up listeners - Anything created in
setupComponentEvents()needs a matchingteardownComponentEvents(); other cleanup goes inbeforeDestroy() - Querying parts too early - Parts may not be available in constructor
- Missing ARIA attributes - Define complete
ariaConfig - Calling
setupEvents()yourself - It's called exactly once by the factory; useafterMount()if you need logic to run after listeners are live (see Component Lifecycle for the bug this caused historically) - Not handling LiveView updates - Component is recreated on updates
- Circular event loops - Be careful with pushEvent triggering updates
- Missing error handling - Validate data in
parseOptions()