Custom Roles
Example
Custom roles and permissions
Powerful primitives to fully customize your app's authorization story.




Product Member
Administrator
Editor
QA Tester
Props
Prop | Datatype | Possible Values |
---|---|---|
darkmode | Boolean | true |
title | String | |
subtitle | String | |
titles | String |
Notes
- Hard set width and height, adjust as needed
- You may have to edit the font styling, padding, and margin due to your sites existing stylesheets.
Code
import React, { useState, useEffect, useRef } from 'react';import './custom_roles.css'
const CustomRoles = ({ darkmode = false, title = 'Custom roles and permissions', subtitle = "Powerful primitives to fully customize your app's authorization story.", titles = ['Product Member', 'Administrator', 'Editor', 'QA Tester']}) => {
const [photoIndex, setPhotoIndex] = useState(0); const [currentTitleIndex, setCurrentTitleIndex] = useState(0); const titlesListRef = useRef(null); const titlesListWrapRef = useRef(null); const selectedIndicatorRef = useRef(null); const [targetTitleWidth, setTargetTitleWidth] = useState(0);
useEffect(() => { const interval = setInterval(() => { setCurrentTitleIndex(prevIndex => (prevIndex + 1) % titles.length); setPhotoIndex(prevIndex => (prevIndex + 1) % 4) }, 2000);
return () => clearInterval(interval); }, [titles.length]);
useEffect(() => { if (titlesListRef.current && titlesListWrapRef.current) { const titlesElements = titlesListRef.current.children; if (titlesElements[currentTitleIndex]) { const wrapWidth = titlesListWrapRef.current.offsetWidth; const titleElement = titlesElements[currentTitleIndex]; const titleElementWidth = titleElement.offsetWidth; const titleOffset = titleElement.offsetLeft + titleElementWidth / 2; const translateX = -titleOffset + wrapWidth / 2;
titlesListRef.current.style.transform = `translateX(${translateX}px)`; setTargetTitleWidth(titleElementWidth + 10); setTimeout(() => { if (selectedIndicatorRef.current) { selectedIndicatorRef.current.style.width = `${titleElementWidth + 10}px`; } }, 500); } } }, [currentTitleIndex, titles.length, targetTitleWidth]);
return ( <div className="custom_roles" style={{ backgroundColor: darkmode ? '#232323' : '#fff' }} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > <div className="cr_copy_wrap"> <svg width="0" height="0" aria-hidden="true"><defs><pattern id="wave" width="12" height="3" patternUnits="userSpaceOnUse"><path fill="none" stroke="white" stroke-opacity="0.1" d="M-6 0c3 2 6 0 6 0s3-2 6 0 6 0 6 0 3-2 6 0M-6 3c3 2 6 0 6 0s3-2 6 0 6 0 6 0 3-2 6 0"></path></pattern></defs></svg> <p className="cr_title" style={{ color: darkmode ? '#fff' : '#000' }}>{title}</p> <p className="cr_subtitle" style={{ color: darkmode ? '#9395A1' : '#000' }}>{subtitle}</p> </div> <div className="cr_profiles_wrap"> <div className="cr_profile_grid"> <div className="cr_profile" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de' }}></div> <div className="cr_profile cr_profile_add_photo" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de', mixBlendMode: photoIndex == 0 ? 'normal' : 'luminosity', opacity: photoIndex == 0 ? '1' : '.7', boxShadow: photoIndex == 0 ? '0 3px 5px 1px rgba(0, 0, 0, .1)' : '0 3px 5px -1px rgba(0, 0, 0, .1)' }}> <div className="cr_profile_w_photo"> <img src="/assets/custom_roles/person-1@3x.webp" className="cr_profile_photo" /> <svg class="cr_profile_svg" aria-hidden="true"><rect width="100%" height="100%" fill="url(#wave)"></rect></svg> </div> </div> <div className="cr_profile" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de' }}></div> <div className="cr_profile cr_profile_add_photo" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de', mixBlendMode: photoIndex == 1 ? 'normal' : 'luminosity', opacity: photoIndex == 1 ? '1' : '.7', boxShadow: photoIndex == 1 ? '0 3px 5px 1px rgba(0, 0, 0, .1)' : '0 3px 5px -1px rgba(0, 0, 0, .1)' }}> <div className="cr_profile_w_photo"> <img src="/assets/custom_roles/person-2@3x.webp" className="cr_profile_photo" /> <svg class="cr_profile_svg" aria-hidden="true"><rect width="100%" height="100%" fill="url(#wave)"></rect></svg> </div> </div> <div className="cr_profile" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de' }}></div> <div className="cr_profile cr_profile_add_photo" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de', mixBlendMode: photoIndex == 3 ? 'normal' : 'luminosity', opacity: photoIndex == 3 ? '1' : '.7', boxShadow: photoIndex == 3 ? '0 3px 5px 1px rgba(0, 0, 0, .1)' : '0 3px 5px -1px rgba(0, 0, 0, .1)' }}> <div className="cr_profile_w_photo"> <img src="/assets/custom_roles/person-3@3x.webp" className="cr_profile_photo" /> <svg class="cr_profile_svg" aria-hidden="true"><rect width="100%" height="100%" fill="url(#wave)"></rect></svg> </div> </div> <div className="cr_profile" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de' }}></div> <div className="cr_profile cr_profile_add_photo" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de', mixBlendMode: photoIndex == 2 ? 'normal' : 'luminosity', opacity: photoIndex == 2 ? '1' : '.7', boxShadow: photoIndex == 2 ? '0 3px 5px 1px rgba(0, 0, 0, .1)' : '0 3px 5px -1px rgba(0, 0, 0, .1)' }}> <div className="cr_profile_w_photo"> <img src="/assets/custom_roles/person-4@3x.webp" className="cr_profile_photo" /> <svg class="cr_profile_svg" aria-hidden="true"><rect width="100%" height="100%" fill="url(#wave)"></rect></svg> </div> </div> <div className="cr_profile" style={{ backgroundColor: darkmode ? '#eee' : '#fff', border: darkmode ? '1px solid #2b2b2b' : '1px solid #d9d9de' }}></div> </div> </div> <div className="cr_titles_list_wrap" ref={titlesListWrapRef} style={{ position: 'relative' }}> <div className="cr_titles_floating_rect_wrap"> <div className="cr_titles_floating_rect" ref={selectedIndicatorRef} style={{ width: `${targetTitleWidth}px` }}></div> </div> <div className="cr_titles_list" ref={titlesListRef}> {titles.map((title, i) => <p key={i} className={`cr_titles_title cr_titles_title_${i}`} style={{ color: currentTitleIndex == i ? '#232323' : 'rgb(183, 184, 194)' }}>{title}</p> )} </div> </div> </div> );};
export { CustomRoles };
.custom_roles { width: 400px; min-width: 280px; height: 528px; border-radius: 16px; position: relative; box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 1px 1px 0px, rgba(34, 42, 53, 0.04) 0px 4px 6px 0px, rgba(47, 48, 55, 0.05) 0px 24px 68px 0px, rgba(0, 0, 0, 0.04) 0px 2px 3px 0px;}
.cr_copy_wrap { padding: 24px 24px 0px 24px;}
.cr_title { font-weight: bold; line-height: 24px; font-size: 13px; margin-top: 0px !important;}
.cr_subtitle { margin-top: 8px !important; line-height: 20px; font-size: 13px}
.cr_profiles_wrap { display: flex; flex-direction: column; align-items: center;}
.cr_profile_grid { display: flex; width: 256px; flex-wrap: wrap; gap: 8px; margin-top: 20px; margin-bottom: 10px;}
.cr_profile { margin: 0px !important; border: 1px solid #d9d9de; width: 80px; height: 92px; border-radius: 8px; background-color: #fff;}
.cr_profile_add_photo { box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .1); padding: 4px; -webkit-transition: all 1s ease-out; -moz-transition: all 1s ease-out; -o-transition: all 1s ease-out; transition: all 1s ease-out;}
.cr_profile_w_photo { border-radius: 4px; padding: 0px; background-image: radial-gradient(241.22% 160.71% at 49.27% -9.52%, rgba(108, 71, 255, 0.3), rgba(255, 249, 99, 0.24) 41.24%, rgba(56, 218, 253, 0.18) 62.34%, rgba(98, 72, 246, 0) 91.89%); height: 100%; position: relative;}
.cr_profile_photo{ border: 1px solid rgb(217, 217, 222); border-radius: 4px; -webkit-transition: all .2s ease-out; -moz-transition: all .2s ease-out; -o-transition: all .2s ease-out; transition: all .2s ease-out; mix-blend-mode: multiply;}
.cr_profile_svg { height: 100% !important; width: 100%; top: 0px; z-index: 10; border-radius: 4px; margin: 0px !important; position: absolute;}
.cr_titles_list_wrap { margin-top: 25px !important; width: 100%; position: relative; overflow: hidden;}
.cr_titles_list { margin: 0px !important; display: flex; gap: 12px; width: max-content; position: relative; -webkit-transition: all .2s ease-out; -moz-transition: all .2s ease-out; -o-transition: all .2s ease-out; transition: all .2s ease-out;}
.cr_titles_title { margin: 0px !important; padding: 4px 8px;}
.cr_titles_floating_rect_wrap { position: absolute; width: 100%; height: max-content;}
.cr_titles_floating_rect { transition: width 0.5s ease, transform 0.5s ease; height: 37px; margin: 0px auto; border-radius: 10000px; border: 1px solid rgb(217, 217, 222); background-color: rgb(250, 250, 251);}