Skip to main content

Overview

ElementOptions is a compound component for building inline configuration popovers attached to elements. It provides form controls (Select, Toggle, Slider, ColorPicker, Input) and helper hooks for updating element properties.
Element Options example

Features

  • Compound components — Root, Trigger, Content, Group, Label, Separator
  • Form controls — Select, Toggle, Slider, ColorPicker, Input
  • Helper hooksuseElementOptions, useUpdateElementProps
  • Radix UI — Built on Radix Popover for accessibility and positioning

Installation

npm install @yoopta/ui
# or
yarn add @yoopta/ui

Basic Usage

import { ElementOptions, useElementOptions, useUpdateElementProps } from '@yoopta/ui/element-options';

// In your element render component
function MyElement({ attributes, children, element, blockId }) {
  return (
    <div {...attributes} className="group relative">
      <ElementOptions.Root blockId={blockId} element={element}>
        <ElementOptions.Trigger className="absolute right-0 top-0 opacity-0 group-hover:opacity-100" />
        <MyElementOptions />
      </ElementOptions.Root>
      {children}
    </div>
  );
}

// Options content component
function MyElementOptions() {
  const { element } = useElementOptions();
  const updateProps = useUpdateElementProps<{ theme: string }>();

  return (
    <ElementOptions.Content side="bottom" align="end">
      <ElementOptions.Group>
        <ElementOptions.Label>Theme</ElementOptions.Label>
        <ElementOptions.Select
          value={element.props?.theme ?? 'default'}
          options={[
            { value: 'default', label: 'Default' },
            { value: 'info', label: 'Info' },
          ]}
          onValueChange={(value) => updateProps({ theme: value })}
        />
      </ElementOptions.Group>
    </ElementOptions.Content>
  );
}

API Reference

ElementOptions.Root

Root component that provides context and manages popover state.
<ElementOptions.Root blockId={blockId} element={element}>
  {children}
</ElementOptions.Root>
Props:
PropTypeDescription
blockIdstringBlock ID containing element
elementSlateElementThe element being configured
childrenReactNodeTrigger and Content
classNamestringCustom CSS classes
styleCSSPropertiesCustom inline styles

ElementOptions.Trigger

Button that opens the options popover.
<ElementOptions.Trigger className="opacity-0 group-hover:opacity-100">
  <SettingsIcon />
</ElementOptions.Trigger>
Props:
PropTypeDescription
childrenReactNodeCustom icon (defaults to MoreHorizontal)
classNamestringCustom CSS classes
styleCSSPropertiesCustom inline styles

ElementOptions.Content

Floating content panel containing form controls.
<ElementOptions.Content side="bottom" align="end" sideOffset={8}>
  {/* Groups and controls here */}
</ElementOptions.Content>
Props:
PropTypeDefaultDescription
side'top' | 'right' | 'bottom' | 'left''bottom'Placement side
align'start' | 'center' | 'end''end'Alignment
sideOffsetnumber4Offset from trigger
alignOffsetnumber0Alignment offset
classNamestringCustom CSS classes

ElementOptions.Group

Groups related controls together.
<ElementOptions.Group>
  <ElementOptions.Label>Settings</ElementOptions.Label>
  {/* Controls here */}
</ElementOptions.Group>

ElementOptions.Label

Label for a group of controls.
<ElementOptions.Label>Theme</ElementOptions.Label>

ElementOptions.Separator

Visual separator between groups.
<ElementOptions.Separator />

ElementOptions.Select

Dropdown select for choosing from predefined options.
<ElementOptions.Select
  value={currentValue}
  options={[
    { value: 'solid', label: 'Solid' },
    { value: 'dashed', label: 'Dashed', icon: <DashedIcon /> },
  ]}
  onValueChange={(value) => updateProps({ style: value })}
  renderOption={(option) => <span>{option.icon} {option.label}</span>}
  renderValue={(option) => <span>{option?.label}</span>}
/>
Props:
PropTypeDescription
valueTCurrent selected value
optionsSelectOption<T>[]Available options
onValueChange(value: T) => voidCalled when selection changes
placeholderstringPlaceholder text
renderOption(option: SelectOption<T>) => ReactNodeCustom option renderer
renderValue(option?: SelectOption<T>) => ReactNodeCustom value renderer
SelectOption type:
type SelectOption<T> = {
  value: T;
  label: string;
  icon?: ReactNode;
  color?: string;
};

ElementOptions.Toggle

Boolean toggle switch.
<ElementOptions.Toggle
  checked={isEnabled}
  onCheckedChange={(checked) => updateProps({ enabled: checked })}
  label="Enable feature"
/>
Props:
PropTypeDescription
checkedbooleanCurrent state
onCheckedChange(checked: boolean) => voidCalled when toggled
labelstringOptional label text

ElementOptions.Slider

Numeric slider for range values.
<ElementOptions.Slider
  value={50}
  onValueChange={(value) => updateProps({ opacity: value })}
  min={0}
  max={100}
  step={1}
/>
Props:
PropTypeDefaultDescription
valuenumberCurrent value
onValueChange(value: number) => voidCalled on change
minnumber0Minimum value
maxnumber100Maximum value
stepnumber1Step increment

ElementOptions.ColorPicker

Color picker with presets and hex input.
<ElementOptions.ColorPicker
  value="#3B82F6"
  onChange={(color) => updateProps({ color })}
  presetColors={['#EF4444', '#22C55E', '#3B82F6', '#8B5CF6']}
/>
Props:
PropTypeDescription
valuestringCurrent color (hex)
onChange(color: string) => voidCalled on change
presetColorsstring[]Preset color swatches

ElementOptions.Input

Text input for string values.
<ElementOptions.Input
  value={title}
  onChange={(value) => updateProps({ title: value })}
  placeholder="Enter title..."
  type="text"
/>
Props:
PropTypeDefaultDescription
valuestringCurrent value
onChange(value: string) => voidCalled on change
placeholderstringPlaceholder text
type'text' | 'number' | 'url''text'Input type

Hooks

useElementOptions()

Access element context within Content.
const { blockId, element, editor, isOpen, setIsOpen } = useElementOptions();
Returns:
PropertyTypeDescription
blockIdstringBlock ID
elementSlateElementCurrent element
editorYooEditorEditor instance
isOpenbooleanPopover open state
setIsOpen(open: boolean) => voidToggle popover

useUpdateElementProps<T>()

Helper hook for updating element properties.
const updateProps = useUpdateElementProps<{ theme: string; color: string }>();

// Usage
updateProps({ theme: 'info' });
updateProps({ color: '#3B82F6' });

Examples

Callout Theme Selector

const CALLOUT_THEMES = [
  { value: 'default', label: 'Default', icon: <MessageSquare className="h-4 w-4" /> },
  { value: 'info', label: 'Info', icon: <Info className="h-4 w-4 text-blue-500" /> },
  { value: 'success', label: 'Success', icon: <CheckCircle className="h-4 w-4 text-green-500" /> },
  { value: 'warning', label: 'Warning', icon: <AlertCircle className="h-4 w-4 text-yellow-500" /> },
  { value: 'error', label: 'Error', icon: <XCircle className="h-4 w-4 text-red-500" /> },
];

function CalloutElementOptions() {
  const { element } = useElementOptions();
  const updateProps = useUpdateElementProps<{ theme: string }>();

  return (
    <ElementOptions.Content side="bottom" align="end" sideOffset={8}>
      <ElementOptions.Group>
        <ElementOptions.Label>Theme</ElementOptions.Label>
        <ElementOptions.Select
          value={element.props?.theme ?? 'default'}
          options={CALLOUT_THEMES}
          onValueChange={(value) => updateProps({ theme: value })}
          renderOption={(option) => (
            <span className="flex items-center gap-2">
              {option.icon}
              {option.label}
            </span>
          )}
        />
      </ElementOptions.Group>
    </ElementOptions.Content>
  );
}

Divider with Style and Color

const DIVIDER_THEMES = [
  { value: 'solid', label: 'Solid' },
  { value: 'dashed', label: 'Dashed' },
  { value: 'dotted', label: 'Dotted' },
  { value: 'gradient', label: 'Gradient' },
];

const DIVIDER_COLORS = ['#E5E7EB', '#6B7280', '#EF4444', '#22C55E', '#3B82F6'];

function DividerElementOptions() {
  const { element } = useElementOptions();
  const updateProps = useUpdateElementProps<{ theme: string; color: string }>();

  return (
    <ElementOptions.Content side="bottom" align="end" sideOffset={8}>
      <ElementOptions.Group>
        <ElementOptions.Label>Style</ElementOptions.Label>
        <ElementOptions.Select
          value={element.props?.theme ?? 'solid'}
          options={DIVIDER_THEMES}
          onValueChange={(value) => updateProps({ theme: value })}
        />
      </ElementOptions.Group>

      <ElementOptions.Separator />

      <ElementOptions.Group>
        <ElementOptions.Label>Color</ElementOptions.Label>
        <ElementOptions.ColorPicker
          value={element.props?.color ?? '#E5E7EB'}
          onChange={(color) => updateProps({ color })}
          presetColors={DIVIDER_COLORS}
        />
      </ElementOptions.Group>
    </ElementOptions.Content>
  );
}

Table Appearance Toggles

function TableElementOptions() {
  const { element } = useElementOptions();
  const updateProps = useUpdateElementProps<{
    bordered: boolean;
    compact: boolean;
    scrollable: boolean;
  }>();
  const props = element.props ?? {};

  return (
    <ElementOptions.Content side="bottom" align="end" sideOffset={8}>
      <ElementOptions.Group>
        <ElementOptions.Label>Appearance</ElementOptions.Label>

        <div className="flex items-center justify-between px-2">
          <span>Bordered</span>
          <ElementOptions.Toggle
            checked={props.bordered ?? true}
            onCheckedChange={(checked) => updateProps({ bordered: checked })}
          />
        </div>

        <div className="flex items-center justify-between px-2">
          <span>Compact</span>
          <ElementOptions.Toggle
            checked={props.compact ?? false}
            onCheckedChange={(checked) => updateProps({ compact: checked })}
          />
        </div>

        <div className="flex items-center justify-between px-2">
          <span>Scrollable</span>
          <ElementOptions.Toggle
            checked={props.scrollable ?? true}
            onCheckedChange={(checked) => updateProps({ scrollable: checked })}
          />
        </div>
      </ElementOptions.Group>
    </ElementOptions.Content>
  );
}

Table of Contents with Input and Toggles

const DEPTH_OPTIONS = [
  { value: '1', label: 'H1 only' },
  { value: '2', label: 'H1 – H2' },
  { value: '3', label: 'H1 – H3' },
];

function TocElementOptions() {
  const { element } = useElementOptions();
  const updateProps = useUpdateElementProps<{
    title: string;
    depth: number;
    showNumbers: boolean;
    collapsible: boolean;
  }>();
  const props = element.props ?? {};

  return (
    <ElementOptions.Content side="bottom" align="end" sideOffset={8}>
      <ElementOptions.Group>
        <ElementOptions.Label>Title</ElementOptions.Label>
        <ElementOptions.Input
          value={props.title ?? 'Table of Contents'}
          onChange={(value) => updateProps({ title: value })}
          placeholder="Title..."
        />
      </ElementOptions.Group>

      <ElementOptions.Separator />

      <ElementOptions.Group>
        <ElementOptions.Label>Depth</ElementOptions.Label>
        <ElementOptions.Select
          value={String(props.depth ?? 3)}
          options={DEPTH_OPTIONS}
          onValueChange={(value) => updateProps({ depth: Number(value) })}
        />
      </ElementOptions.Group>

      <ElementOptions.Separator />

      <ElementOptions.Group>
        <div className="flex items-center justify-between px-2">
          <span>Numbered</span>
          <ElementOptions.Toggle
            checked={props.showNumbers ?? false}
            onCheckedChange={(checked) => updateProps({ showNumbers: checked })}
          />
        </div>
        <div className="flex items-center justify-between px-2">
          <span>Collapsible</span>
          <ElementOptions.Toggle
            checked={props.collapsible ?? false}
            onCheckedChange={(checked) => updateProps({ collapsible: checked })}
          />
        </div>
      </ElementOptions.Group>
    </ElementOptions.Content>
  );
}

Styling

CSS Variables

:root {
  --yoopta-ui-element-options-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --yoopta-ui-element-options-content-min-width: 180px;
  --yoopta-ui-element-options-content-padding: 8px;
  --yoopta-ui-element-options-content-radius: 0.5rem;
  --yoopta-ui-element-options-control-padding-y: 6px;
  --yoopta-ui-element-options-control-padding-x: 10px;
  --yoopta-ui-element-options-control-radius: 6px;
  --yoopta-ui-element-options-toggle-width: 36px;
  --yoopta-ui-element-options-toggle-height: 20px;
}

Custom Styling

Apply Tailwind or custom classes to components:
<ElementOptions.Content className="min-w-[200px] rounded-lg border bg-popover p-2 shadow-md">
  <ElementOptions.Group className="flex flex-col gap-1">
    <ElementOptions.Label className="px-2 text-xs font-medium text-muted-foreground">
      Theme
    </ElementOptions.Label>
    <ElementOptions.Select
      className="flex h-8 w-full items-center justify-between rounded-md border px-3 text-sm"
      {...props}
    />
  </ElementOptions.Group>
</ElementOptions.Content>

Best Practices

// Good: Separate component for options
function MyElementOptions() {
  const { element } = useElementOptions();
  const updateProps = useUpdateElementProps();
  // ...
}

// In render:
<ElementOptions.Root blockId={blockId} element={element}>
  <ElementOptions.Trigger />
  <MyElementOptions />
</ElementOptions.Root>
<div className="group relative">
  <ElementOptions.Root blockId={blockId} element={element}>
    <ElementOptions.Trigger
      className="absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity"
    />
    <MyElementOptions />
  </ElementOptions.Root>
  {children}
</div>
// Always provide defaults when reading props
const theme = element.props?.theme ?? 'default';
const enabled = element.props?.enabled ?? false;