A hierarchical combobox for selecting property paths within JSON objects.
const sampleSchema = { id: 'string', email: 'string', firstName: 'string', lastName: 'string', customAttributes: { department: 'string', jobTitle: 'string', officeLocation: 'string', }, }; function Example() { const [selectedValue, setSelectedValue] = React.useState(null); return ( <JsonPathSelector data={sampleSchema} selectedValue={selectedValue} onSelectionChange={setSelectedValue} /> ); }
| Prop | Type | Default |
|---|---|---|
data | JsonObject | Required |
selectedValue | string[] | null | undefined (controlled) |
defaultSelectedValue | string[] | null | null (uncontrolled) |
onSelectionChange | (selectedKeys: string[] | null) => void | |
placeholder | string | 'Select an attribute' |
label | string | 'Attributes' |
disabled | boolean | false |
align | 'start' | 'center' | 'end' | 'start' |
emptyMessage | string | 'No data to display' |
notFoundMessage | string | 'No attributes found' |
selectedPathFooter | boolean | true |
A simple selector with a flat object structure.
const userSchema = { email: 'string', firstName: 'string', lastName: 'string', username: 'string', }; function Example() { const [selectedValue, setSelectedValue] = React.useState(null); return ( <Flex direction="column" gap="2" width="320px"> <JsonPathSelector data={userSchema} selectedValue={selectedValue} onSelectionChange={setSelectedValue} /> {selectedValue && ( <Text size="2" color="gray"> Selected: {selectedValue.join('.')} </Text> )} </Flex> ); }
The component automatically handles nested objects, displaying them as expandable/collapsible tree nodes. It also supports complex property names with spaces, dots, and URNs.
const complexSchema = { userId: 'string', userEmail: 'string', profile: { firstName: 'string', lastName: 'string', avatarUrl: 'string', preferences: { notifications: { email: { frequency: 'string', categories: { security: 'boolean', marketing: 'boolean', product: 'boolean', }, }, push: { enabled: 'boolean', }, }, theme: 'string', }, }, employment: { companyName: 'string', department: 'string', position: { title: 'string', level: 'string', team: { name: 'string', location: { office: 'string', building: { name: 'string', address: { street: 'string', coordinates: { latitude: 'number', longitude: 'number', }, }, }, }, }, }, }, }; function Example() { const [selectedValue, setSelectedValue] = React.useState(null); return ( <Flex direction="column" gap="2" width="320px"> <JsonPathSelector data={complexSchema} selectedValue={selectedValue} onSelectionChange={setSelectedValue} /> {selectedValue && ( <Text size="2" color="gray"> Path: {selectedValue.join(' → ')} </Text> )} </Flex> ); }
While schemas (with type strings like 'string', 'number', etc.) are the recommended format for defining attribute structures, the component also works with real data values. This can be useful when you want to display actual data from your application.
const actualUserData = { id: 'usr_01HQZV3K7J8M9N2P3Q4R5S6T7V', email: 'john.doe@example.com', firstName: 'John', lastName: 'Doe', roles: ['admin', 'developer'], isActive: true, lastLoginAt: '2024-11-15T10:30:00Z', metadata: { department: 'Engineering', title: 'Senior Software Engineer', loginCount: 42, preferences: { theme: 'dark', notifications: true, }, }, }; function Example() { const [selectedValue, setSelectedValue] = React.useState(null); return ( <Flex direction="column" gap="2" width="320px"> <JsonPathSelector data={actualUserData} selectedValue={selectedValue} onSelectionChange={setSelectedValue} /> {selectedValue && ( <Text size="2" color="gray"> Selected: {selectedValue.join(' → ')} </Text> )} </Flex> ); }
When no data is available (null, undefined, or empty object), the JsonPathSelector displays an empty state message. When data exists but search returns no results, it shows a not found message. You can customize both messages using the emptyMessage and notFoundMessage props.
function Example() { const [selectedValue1, setSelectedValue1] = React.useState(null); const [selectedValue2, setSelectedValue2] = React.useState(null); const [selectedValue3, setSelectedValue3] = React.useState(null); const sampleSchema = { firstName: 'string', lastName: 'string', email: 'string', }; return ( <Flex direction="column" gap="4" width="320px"> <JsonPathSelector align="start" data={{}} placeholder="Default empty state" selectedValue={selectedValue1} onSelectionChange={setSelectedValue1} /> <JsonPathSelector align="start" data={{}} emptyMessage="No attributes configured yet" placeholder="Custom empty state" selectedValue={selectedValue2} onSelectionChange={setSelectedValue2} /> <JsonPathSelector align="start" data={sampleSchema} notFoundMessage="No matching attributes" placeholder="Custom search not found" selectedValue={selectedValue3} onSelectionChange={setSelectedValue3} /> </Flex> ); }
Disable the selector to prevent user interaction.
const sampleSchema = { id: 'string', status: 'string', createdAt: 'string', }; function Example() { const [selectedValue, setSelectedValue] = React.useState(['status']); const [emptySelectedValue, setEmptySelectedValue] = React.useState(null); return ( <Flex direction="column" gap="4" width="320px"> <Flex direction="column" gap="2"> <JsonPathSelector data={{}} selectedValue={emptySelectedValue} onSelectionChange={setEmptySelectedValue} disabled /> </Flex> <Flex direction="column" gap="2"> <JsonPathSelector data={sampleSchema} selectedValue={selectedValue} onSelectionChange={setSelectedValue} disabled /> </Flex> </Flex> ); }
This example stress tests the component with deeply nested data (5+ levels), many items at each level, and varying text lengths to demonstrate how it handles complex real-world scenarios.
const largeSchema = { // Short names 'id': 'string', 'uid': 'string', 'ref': 'string', // Medium length names 'emailAddress': 'string', 'displayName': 'string', 'username': 'string', 'phoneNumber': 'string', // Long and complex names 'urn:ietf:params:scim:schemas:core:2.0:User': 'string', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': 'string', 'urn:workos:directory:attribute:custom:department:engineering:team:platform': 'string', // Very long parent attribute with children 'urn:ietf:params:scim:schemas:extension:enterprise:workos:2.0:User:CustomAttributes': { customField1: 'string', customField2: 'string', customField3: 'string', nestedCustomAttributes: { attribute1: 'string', attribute2: 'string', veryLongAttributeNameThatDemonstratesTextWrappingAndOverflow: 'string', }, }, // Deep nesting with many items 'personalInformation': { basicDetails: { firstName: 'string', middleName: 'string', lastName: 'string', preferredName: 'string', suffix: 'string', title: 'string', dateOfBirth: 'string', placeOfBirth: 'string', nationality: 'string', maritalStatus: 'string', }, contactInformation: { primaryEmail: 'string', secondaryEmail: 'string', workEmail: 'string', mobilePhone: 'string', homePhone: 'string', workPhone: 'string', emergencyContact: { name: 'string', relationship: 'string', primaryPhone: 'string', secondaryPhone: 'string', email: 'string', address: { street: 'string', unit: 'string', city: 'string', state: 'string', postalCode: 'string', country: 'string', }, }, }, addresses: { homeAddress: { streetAddress: 'string', apartmentNumber: 'string', city: 'string', state: 'string', zipCode: 'string', country: 'string', coordinates: { latitude: 'number', longitude: 'number', accuracy: 'string', }, }, mailingAddress: { streetAddress: 'string', city: 'string', state: 'string', zipCode: 'string', country: 'string', }, previousAddresses: { address1: { street: 'string', city: 'string', state: 'string', from: 'string', to: 'string', }, address2: { street: 'string', city: 'string', state: 'string', from: 'string', to: 'string', }, }, }, }, 'employmentInformation': { currentEmployment: { 'companyName': 'string', 'urn:workos:company:id': 'string', 'position': { jobTitle: 'string', jobLevel: 'string', employmentType: 'string', department: { name: 'string', division: 'string', costCenter: 'string', team: { name: 'string', subTeam: 'string', location: { officeName: 'string', buildingName: 'string', floor: 'string', desk: 'string', address: { street: 'string', suite: 'string', city: 'string', state: 'string', postalCode: 'string', coordinates: { latitude: 'number', longitude: 'number', timezone: 'string', }, }, }, }, }, responsibilities: { primary: 'string', secondary: 'string', tertiary: 'string', }, manager: { name: 'string', email: 'string', title: 'string', department: 'string', }, }, 'compensation': { baseSalary: 'number', currency: 'string', paymentFrequency: 'string', bonus: { targetAmount: 'number', performanceMultiplier: 'number', }, }, 'startDate': 'string', 'employeeId': 'string', }, employmentHistory: { previousEmployer1: { companyName: 'string', position: 'string', startDate: 'string', endDate: 'string', reasonForLeaving: 'string', }, previousEmployer2: { companyName: 'string', position: 'string', startDate: 'string', endDate: 'string', reasonForLeaving: 'string', }, }, }, 'systemPreferences': { notificationSettings: { emailNotifications: { enabled: 'boolean', frequency: 'string', categories: { securityAlerts: 'boolean', systemUpdates: 'boolean', marketingEmails: 'boolean', productAnnouncements: 'boolean', teamMentions: 'boolean', directMessages: 'boolean', }, }, pushNotifications: { enabled: 'boolean', devices: { mobile: 'boolean', desktop: 'boolean', browser: 'boolean', }, }, }, displayPreferences: { theme: 'string', language: 'string', timezone: 'string', dateFormat: 'string', timeFormat: 'string', }, }, }; function Example() { const [selectedValue, setSelectedValue] = React.useState([ 'urn:ietf:params:scim:schemas:extension:enterprise:workos:2.0:User:CustomAttributes', 'nestedCustomAttributes', 'veryLongAttributeNameThatDemonstratesTextWrappingAndOverflow', ]); return ( <Flex direction="column" gap="4" width="320px"> <JsonPathSelector data={largeSchema} selectedValue={selectedValue} onSelectionChange={setSelectedValue} /> </Flex> ); }
The JsonPathSelector can be used as either a controlled or uncontrolled component.
const sampleSchema = { id: 'string', name: 'string', price: 'number', inStock: 'boolean', }; function Example() { // Controlled const [controlledValue, setControlledValue] = React.useState(['name']); // Uncontrolled const [lastUncontrolledValue, setLastUncontrolledValue] = React.useState(null); return ( <Flex direction="column" gap="4" width="320px"> <Flex direction="column" gap="2"> <Flex direction="row" align="center" gap="2" justify="between" width="100%" > <Text size="2" weight="medium"> Controlled </Text> <Button ghost size="1" onClick={() => setControlledValue(['price'])}> Select option </Button> </Flex> <JsonPathSelector data={sampleSchema} selectedValue={controlledValue} onSelectionChange={setControlledValue} /> </Flex> <Flex direction="column" gap="2"> <Text size="2" weight="medium"> Uncontrolled </Text> <JsonPathSelector data={sampleSchema} defaultSelectedValue={['id']} onSelectionChange={setLastUncontrolledValue} /> {lastUncontrolledValue && ( <Text size="1" color="gray"> Last selected: {lastUncontrolledValue.join('.')} </Text> )} </Flex> </Flex> ); }
Multiple selectors can be arranged in a grid layout for attribute mapping scenarios.
const idpSchema = { id: 'string', email: 'string', username: 'string', profile: { firstName: 'string', lastName: 'string', displayName: 'string', }, attributes: { department: 'string', title: 'string', location: 'string', }, }; const attributeMappings = [ { key: 'firstName', required: true }, { key: 'lastName', required: true }, { key: 'email', required: true }, { key: 'department', required: false }, { key: 'jobTitle', required: false }, ]; function Example() { const [mappings, setMappings] = React.useState({}); const [submitted, setSubmitted] = React.useState(null); const handleChange = (key, value) => { setMappings((prev) => ({ ...prev, [key]: value })); }; const handleSubmit = (e) => { e.preventDefault(); const result = Object.entries(mappings) .filter(([, value]) => value) .map(([key, value]) => `${key}: ${value.join('.')}`); setSubmitted(result.join(', ')); }; return ( <Flex direction="column" gap="4" width="100%"> <Box asChild style={{ border: '1px solid var(--gray-a5)', borderRadius: 'var(--radius-4)', }} > <form onSubmit={handleSubmit}> {/* Header */} <Box style={{ display: 'grid', gridTemplateColumns: '1fr 30px 1fr', gap: 'var(--space-3)', padding: 'var(--space-3)', backgroundColor: 'var(--gray-a2)', borderBottom: '1px solid var(--gray-a5)', }} > <Text size="2" weight="bold"> Attribute name </Text> <Box /> <Text size="2" weight="bold"> IdP field name </Text> </Box> {/* Rows */} <Box> {attributeMappings.map((attr, index) => ( <Box key={attr.key} style={{ display: 'grid', gridTemplateColumns: '1fr 30px 1fr', gap: 'var(--space-3)', padding: 'var(--space-3)', alignItems: 'center', borderBottom: index < attributeMappings.length - 1 ? '1px solid var(--gray-a5)' : 'none', }} > <Flex align="center" gap="2"> <Text size="2">{attr.key}</Text> {attr.required && <Badge>Required</Badge>} </Flex> <Flex align="center" justify="center"> <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M8.14645 3.14645C8.34171 2.95118 8.65829 2.95118 8.85355 3.14645L12.8536 7.14645C13.0488 7.34171 13.0488 7.65829 12.8536 7.85355L8.85355 11.8536C8.65829 12.0488 8.34171 12.0488 8.14645 11.8536C7.95118 11.6583 7.95118 11.3417 8.14645 11.1464L11.2929 8H2.5C2.22386 8 2 7.77614 2 7.5C2 7.22386 2.22386 7 2.5 7H11.2929L8.14645 3.85355C7.95118 3.65829 7.95118 3.34171 8.14645 3.14645Z" fill="var(--gray-a10)" fillRule="evenodd" clipRule="evenodd" /> </svg> </Flex> <JsonPathSelector align="end" data={idpSchema} placeholder="Select source attribute" selectedValue={mappings[attr.key] || null} onSelectionChange={(value) => handleChange(attr.key, value)} /> </Box> ))} </Box> <Box style={{ padding: 'var(--space-3)', borderTop: '1px solid var(--gray-a5)', }} > <Button type="submit">Save mappings</Button> </Box> </form> </Box> {submitted && ( <Callout.Root> <Callout.Text>Saved: {submitted}</Callout.Text> </Callout.Root> )} </Flex> ); }
A comprehensive example showing multiple selectors in a Dialog with react-hook-form integration, scrollable body, and fixed header.
() => { const [open, setOpen] = React.useState(false); const [savedValues, setSavedValues] = React.useState(null); const idpAttributeSchema = { id: 'string', email: 'string', username: 'string', firstName: 'string', lastName: 'string', profile: { displayName: 'string', title: 'string', department: 'string', manager: 'string', phoneNumber: 'string', }, employment: { employeeId: 'string', startDate: 'string', status: 'string', location: { office: 'string', building: 'string', floor: 'string', }, }, customAttributes: { costCenter: 'string', securityClearance: 'string', preferredLanguage: 'string', }, groups: 'array', }; const attributeMappings = [ { key: 'idpId', label: 'IdP ID', required: true }, { key: 'firstName', label: 'First Name', required: true }, { key: 'lastName', label: 'Last Name', required: true }, { key: 'email', label: 'Email', required: true }, { key: 'username', label: 'Username', required: true }, { key: 'jobTitle', label: 'Job Title', required: false }, { key: 'department', label: 'Department', required: false }, { key: 'groups', label: 'Groups', required: false }, ]; const { control, handleSubmit, formState: { errors }, } = useForm(); const onSubmit = (values) => { setSavedValues(values); setOpen(false); }; return ( <Flex direction="column" gap="4"> <Button onClick={() => setOpen(true)}>Edit attribute mapping</Button> {savedValues && ( <Callout.Root color="green"> <Callout.Text> Saved {Object.keys(savedValues).length} attribute mappings </Callout.Text> </Callout.Root> )} <Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Content size="5"> <Flex direction="column" maxHeight="calc(100vh - var(--dialog-content-padding) * 2 - var(--space-6) - max(var(--space-6), 6vh))" minHeight="400px" > <Flex asChild direction="column" flexGrow="1" minHeight="0"> <form onSubmit={handleSubmit(onSubmit)}> <Dialog.Title>Attribute mapping</Dialog.Title> <Dialog.Description mb="5"> Configure how attributes are mapped between the identity provider (IdP) and your application. Standard attributes such as name and email are required. </Dialog.Description> {Object.keys(errors).length > 0 && ( <Callout.Root color="red" mb="3"> <Callout.Text> Please fill in all required fields before saving. </Callout.Text> </Callout.Root> )} {/* Fixed Header */} <Box style={{ display: 'grid', gridTemplateColumns: '1fr 30px 1fr', gap: 'var(--space-3)', padding: 'var(--space-3)', backgroundColor: 'var(--gray-a2)', border: '1px solid var(--gray-a5)', borderRadius: 'var(--radius-4) var(--radius-4) 0 0', borderBottom: '1px solid var(--gray-a5)', }} > <Text size="2" weight="bold"> Attribute name </Text> <Box /> <Text size="2" weight="bold"> IdP field name </Text> </Box> {/* Scrollable Body */} <Flex asChild direction="column" flexGrow="1" minHeight="0" style={{ border: '1px solid var(--gray-a5)', borderTop: 'none', borderRadius: '0 0 var(--radius-4) var(--radius-4)', }} > <ScrollArea.Root> <ScrollArea.Viewport> <Box> {attributeMappings.map((attr, index) => ( <Box key={attr.key} style={{ display: 'grid', gridTemplateColumns: '1fr 30px 1fr', gap: 'var(--space-3)', padding: 'var(--space-3)', alignItems: 'center', borderBottom: index < attributeMappings.length - 1 ? '1px solid var(--gray-a5)' : 'none', minHeight: '44px', transition: 'background-color 0.1s', ...(errors[attr.key] && { backgroundColor: 'var(--red-a2)', }), }} > <Flex align="center" gap="2" wrap="wrap"> <Text size="2">{attr.label}</Text> {attr.required && <Badge>Required</Badge>} </Flex> <Flex align="center" justify="center"> <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M8.14645 3.14645C8.34171 2.95118 8.65829 2.95118 8.85355 3.14645L12.8536 7.14645C13.0488 7.34171 13.0488 7.65829 12.8536 7.85355L8.85355 11.8536C8.65829 12.0488 8.34171 12.0488 8.14645 11.8536C7.95118 11.6583 7.95118 11.3417 8.14645 11.1464L11.2929 8H2.5C2.22386 8 2 7.77614 2 7.5C2 7.22386 2.22386 7 2.5 7H11.2929L8.14645 3.85355C7.95118 3.65829 7.95118 3.34171 8.14645 3.14645Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd" /> </svg> </Flex> <Box> <Controller control={control} name={attr.key} defaultValue={null} rules={{ validate: (value) => { if (attr.required && !value) { return `${attr.label} is required`; } return true; }, }} render={({ field }) => ( <JsonPathSelector align="end" data={idpAttributeSchema} label={`Source attribute for ${attr.label}`} placeholder="Select source attribute" selectedValue={field.value} onSelectionChange={field.onChange} /> )} /> </Box> </Box> ))} </Box> </ScrollArea.Viewport> <ScrollArea.Scrollbar orientation="vertical" /> </ScrollArea.Root> </Flex> <Flex gap="2" justify="end" mt="5"> <Dialog.Close> <Button type="button">Cancel</Button> </Dialog.Close> <Button color="purple" type="submit"> Save changes </Button> </Flex> </form> </Flex> </Flex> </Dialog.Content> </Dialog.Root> </Flex> ); };