iOS List View
A re-creation of the iOS List View built with React Aria Components, Framer Motion, and Tailwind CSS with support for swipe gestures, layout animations, and multiple selection mode.
Example#
Emma Johnson
9:40 AM
Meeting Reminder: Project Kickoff
Dear Devon, This is a friendly reminder of the upcoming project kickoff meeting scheduled for tomorrow at 9am. The meeting will be held in [location]. It's essential that all team members attend to ensure a successful start to the project. Please come prepared with any necessary materials or information relevant to the project. If you have any questions or need further clarification, don't hesitate to reach out to me. Looking forward to seeing you at the meeting. Best regards, Emma
support@company.com
8:23 AM
Important Account Update
Dear Devon, We hope this email finds you well. We are writing to inform you about an important update regarding your account with us. As part of our ongoing efforts to enhance security, we have implemented a new two-factor authentication process. To ensure continued access to your account, please follow the instructions provided in the attached document to set up the two-factor authentication feature. If you have any questions or need assistance, please don't hesitate to contact our support team. Thank you for your cooperation. Best regards, The [Company] Team
Liam Thompson
Yesterday
Promotion Announcement
Dear Devon, We are pleased to inform you that based on your exceptional performance, dedication, and contributions to the company, you have been promoted to the position of [new position]. This promotion is a recognition of your hard work and the value you bring to our organization. Please accept our heartfelt congratulations on this well-deserved achievement. We believe that you will excel in your new role and contribute to the continued success of our team. If you have any questions or need any support during this transition, please don't hesitate to contact the HR department. Best regards, The HR Team
events@company.com
Yesterday
Invitation to Exclusive Networking Event
Dear Devon, You are cordially invited to our upcoming exclusive networking event, where industry leaders, professionals, and enthusiasts gather to exchange ideas and forge valuable connections. This event will take place on [date] at [venue], starting at [time]. Please RSVP by [RSVP date] to secure your spot. We anticipate a high demand for attendance, so we encourage you to respond promptly. We look forward to welcoming you to this exciting event! Best regards, The [Company] Events Team
sales@company.com
Friday
Thank You for Your Recent Purchase
Dear Devon, Thank you for your recent purchase from our online store. We appreciate your business and are delighted to let you know that your order has been successfully processed and is now being prepared for shipment. You will receive a confirmation email with tracking details as soon as your package is dispatched. If you have any questions regarding your order or need further assistance, please don't hesitate to reach out to our customer support team. Once again, thank you for choosing us as your preferred shopping destination. Best regards, The [Company] Team
Jane Doe
Friday
New Project Proposal
Hi Devon, I've attached a new project proposal for your review. Please let me know what you think. Thanks, Jane
Susan Smith
Friday
Status Update
Hi Devon, I'm just sending a quick status update on the project we're working on together. I'm on track to meet my deadlines, and I'll keep you updated on my progress. Thanks, Susan
Michael Jones
Thursday
Question about the presentation
Hi Devon, I had a question about the presentation you gave last week. I was wondering if you could send me the slides so I can review them in more detail. Thanks, Michael
Customer Service
Thursday
Order Confirmation
Hi Devon, We just wanted to confirm that your order has been shipped. Your order number is 1234567890, and it should arrive at your home address within 2-3 business days. Thanks for your purchase! Customer Service
Your Bank
Wednesday
Account Statement
Hi Devon, We're writing to you today to provide you with your monthly account statement. As you can see, your account balance is currently $1,000.00. Please let us know if you have any questions. Thanks, Your Bank
hr@company2.com
Tuesday
Employee Benefits Update
Dear Devon, We wanted to inform you about the recent updates to our employee benefits package. We have enhanced the healthcare coverage options and added additional wellness programs to support your well-being. Please review the attached document for detailed information on the updated benefits. If you have any questions or need further assistance, feel free to contact the HR department. Best regards, The HR Team
import {Button, GridList, GridListItem} from 'react-aria-components';
import type {Selection, SelectionMode} from 'react-aria-components';
import {animate, AnimatePresence, motion, useIsPresent, useMotionTemplate, useMotionValue, useMotionValueEvent} from 'framer-motion';
import {useRef, useState} from 'react';
import type {CSSProperties} from 'react';
const MotionItem = motion(GridListItem);
const inertiaTransition = {
type: 'inertia' as const,
bounceStiffness: 300,
bounceDamping: 40,
timeConstant: 300
};
function SwipableList() {
let [items, setItems] = useState(messages.emails);
let [selectedKeys, setSelectedKeys] = useState<Selection>(new Set());
let [selectionMode, setSelectionMode] = useState<SelectionMode>('none');
let onDelete = () => {
setItems(
items.filter((i) => selectedKeys !== 'all' && !selectedKeys.has(i.id))
);
setSelectedKeys(new Set());
setSelectionMode('none');
};
return (
<div className="flex flex-col h-full max-h-[500px] sm:w-[400px] -mx-[14px] sm:mx-0">
{/* Toolbar */}
<div className="flex pb-4 justify-between">
<Button
className="text-blue-600 text-lg outline-none bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring disabled:text-gray-400"
style={{ opacity: selectionMode === 'none' ? 0 : 1 }}
isDisabled={selectedKeys !== 'all' && selectedKeys.size === 0}
onPress={onDelete}
>
Delete
</Button>
<Button
className="text-blue-600 text-lg outline-none bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring"
onPress={() => {
setSelectionMode((m) => (m === 'none' ? 'multiple' : 'none'));
setSelectedKeys(new Set());
}}
>
{selectionMode === 'none' ? 'Edit' : 'Cancel'}
</Button>
</div>
<GridList
className="relative flex-1 overflow-auto"
aria-label="Inbox"
onAction={selectionMode === 'none' ? () => {} : undefined}
selectionMode={selectionMode}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
<AnimatePresence>
{items.map((item) => (
<ListItem
key={item.id}
id={item.id}
textValue={[item.sender, item.date, item.subject, item.message]
.join('\n')}
onRemove={() => setItems(items.filter((i) => i !== item))}
>
<div className="flex flex-col text-md cursor-default">
<div className="flex justify-between">
<p className="font-bold text-lg m-0">{item.sender}</p>
<p className="text-gray-500 m-0">{item.date}</p>
</div>
<p className="m-0">{item.subject}</p>
<p className="line-clamp-2 text-gray-500 dark:text-gray-400 m-0">
{item.message}
</p>
</div>
</ListItem>
))}
</AnimatePresence>
</GridList>
</div>
);
}
function ListItem({ id, children, textValue, onRemove }) {
let ref = useRef(null);
let x = useMotionValue(0);
let isPresent = useIsPresent();
let xPx = useMotionTemplate` px`;
// Align the text in the remove button to the left if the
// user has swiped at least 80% of the width.
let [align, setAlign] = useState('end');
useMotionValueEvent(x, 'change', (x) => {
let a = x < -ref.current?.offsetWidth * 0.8 ? 'start' : 'end';
setAlign(a);
});
return (
<MotionItem
id={id}
textValue={textValue}
className="outline-none group relative overflow-clip border-t border-0 border-solid last:border-b border-gray-200 dark:border-gray-800 pressed:bg-gray-200 dark:pressed:bg-gray-800 selected:bg-gray-200 dark:selected:bg-gray-800 focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2"
layout
transition={{ duration: 0.25 }}
exit={{ opacity: 0 }}
// Take item out of the flow if it is being removed.
style={{ position: isPresent ? 'relative' : 'absolute' }}
>
{/* @ts-ignore - Framer Motion's types don't handle functions properly. */}
{({ selectionMode, isSelected }) => (
// Content of the item can be swiped to reveal the delete button, or fully swiped to delete.
<motion.div
ref={ref}
style={{ x, '--x': xPx } as CSSProperties}
className="flex items-center"
drag={selectionMode === 'none' ? 'x' : undefined}
dragConstraints={{ right: 0 }}
onDragEnd={(e, { offset }) => {
// If the user dragged past 80% of the width, remove the item
// otherwise animate back to the nearest snap point.
let v = offset.x > -20 ? 0 : -100;
if (x.get() < -ref.current.offsetWidth * 0.8) {
v = -ref.current.offsetWidth;
onRemove();
}
animate(x, v, { ...inertiaTransition, min: v, max: v });
}}
onDragStart={() => {
// Cancel react-aria press event when dragging starts.
document.dispatchEvent(new PointerEvent('pointercancel'));
}}
>
{selectionMode === 'multiple' && (
<SelectionCheckmark isSelected={isSelected} />
)}
<motion.div
layout
layoutDependency={selectionMode}
transition={{ duration: 0.25 }}
className="relative flex items-center px-4 py-2 z-10"
>
{children}
</motion.div>
{selectionMode === 'none' && (
<Button
className="bg-red-600 pressed:bg-red-700 cursor-default text-lg outline-none border-none transition-colors text-white flex items-center absolute top-0 left-[100%] py-2 h-full z-0 isolate focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2"
style={{
// Calculate the size of the button based on the drag position,
// which is stored in a CSS variable above.
width: 'max(100px, calc(-1 * var(--x)))',
justifyContent: align
}}
onPress={onRemove}
// Move the button into view when it is focused with the keyboard
// (e.g. via the arrow keys).
onFocus={() => x.set(-100)}
onBlur={() => x.set(0)}
>
<motion.span
initial={false}
className="px-4"
animate={{
// Whenever the alignment changes, perform a keyframe animation
// between the previous position and new position. This is done
// by calculating a transform for the previous alignment and
// animating it back to zero.
transform: align === 'start'
? ['translateX(calc(-100% - var(--x)))', 'translateX(0)']
: ['translateX(calc(100% + var(--x)))', 'translateX(0)']
}}
>
Delete
</motion.span>
</Button>
)}
</motion.div>
)}
</MotionItem>
);
}
function SelectionCheckmark({ isSelected }) {
return (
<motion.svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6 flex-shrink-0 ml-4"
initial={{ x: -40 }}
animate={{ x: 0 }}
transition={{ duration: 0.25 }}
>
{!isSelected && (
<circle
r={9}
cx={12}
cy={12}
stroke="currentColor"
fill="none"
strokeWidth={1}
className="text-gray-400"
/>
)}
{isSelected && (
<path
className="text-blue-600"
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clipRule="evenodd"
/>
)}
</motion.svg>
);
}
import {
Button,
GridList,
GridListItem
} from 'react-aria-components';
import type {
Selection,
SelectionMode
} from 'react-aria-components';
import {
animate,
AnimatePresence,
motion,
useIsPresent,
useMotionTemplate,
useMotionValue,
useMotionValueEvent
} from 'framer-motion';
import {useRef, useState} from 'react';
import type {CSSProperties} from 'react';
const MotionItem = motion(GridListItem);
const inertiaTransition = {
type: 'inertia' as const,
bounceStiffness: 300,
bounceDamping: 40,
timeConstant: 300
};
function SwipableList() {
let [items, setItems] = useState(messages.emails);
let [selectedKeys, setSelectedKeys] = useState<Selection>(
new Set()
);
let [selectionMode, setSelectionMode] = useState<
SelectionMode
>('none');
let onDelete = () => {
setItems(
items.filter((i) =>
selectedKeys !== 'all' && !selectedKeys.has(i.id)
)
);
setSelectedKeys(new Set());
setSelectionMode('none');
};
return (
<div className="flex flex-col h-full max-h-[500px] sm:w-[400px] -mx-[14px] sm:mx-0">
{/* Toolbar */}
<div className="flex pb-4 justify-between">
<Button
className="text-blue-600 text-lg outline-none bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring disabled:text-gray-400"
style={{
opacity: selectionMode === 'none' ? 0 : 1
}}
isDisabled={selectedKeys !== 'all' &&
selectedKeys.size === 0}
onPress={onDelete}
>
Delete
</Button>
<Button
className="text-blue-600 text-lg outline-none bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring"
onPress={() => {
setSelectionMode((
m
) => (m === 'none' ? 'multiple' : 'none'));
setSelectedKeys(new Set());
}}
>
{selectionMode === 'none' ? 'Edit' : 'Cancel'}
</Button>
</div>
<GridList
className="relative flex-1 overflow-auto"
aria-label="Inbox"
onAction={selectionMode === 'none'
? () => {}
: undefined}
selectionMode={selectionMode}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
<AnimatePresence>
{items.map((item) => (
<ListItem
key={item.id}
id={item.id}
textValue={[
item.sender,
item.date,
item.subject,
item.message
].join('\n')}
onRemove={() =>
setItems(items.filter((i) => i !== item))}
>
<div className="flex flex-col text-md cursor-default">
<div className="flex justify-between">
<p className="font-bold text-lg m-0">
{item.sender}
</p>
<p className="text-gray-500 m-0">
{item.date}
</p>
</div>
<p className="m-0">{item.subject}</p>
<p className="line-clamp-2 text-gray-500 dark:text-gray-400 m-0">
{item.message}
</p>
</div>
</ListItem>
))}
</AnimatePresence>
</GridList>
</div>
);
}
function ListItem({ id, children, textValue, onRemove }) {
let ref = useRef(null);
let x = useMotionValue(0);
let isPresent = useIsPresent();
let xPx = useMotionTemplate` px`;
// Align the text in the remove button to the left if the
// user has swiped at least 80% of the width.
let [align, setAlign] = useState('end');
useMotionValueEvent(x, 'change', (x) => {
let a = x < -ref.current?.offsetWidth * 0.8
? 'start'
: 'end';
setAlign(a);
});
return (
<MotionItem
id={id}
textValue={textValue}
className="outline-none group relative overflow-clip border-t border-0 border-solid last:border-b border-gray-200 dark:border-gray-800 pressed:bg-gray-200 dark:pressed:bg-gray-800 selected:bg-gray-200 dark:selected:bg-gray-800 focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2"
layout
transition={{ duration: 0.25 }}
exit={{ opacity: 0 }}
// Take item out of the flow if it is being removed.
style={{
position: isPresent ? 'relative' : 'absolute'
}}
>
{/* @ts-ignore - Framer Motion's types don't handle functions properly. */}
{({ selectionMode, isSelected }) => (
// Content of the item can be swiped to reveal the delete button, or fully swiped to delete.
<motion.div
ref={ref}
style={{ x, '--x': xPx } as CSSProperties}
className="flex items-center"
drag={selectionMode === 'none' ? 'x' : undefined}
dragConstraints={{ right: 0 }}
onDragEnd={(e, { offset }) => {
// If the user dragged past 80% of the width, remove the item
// otherwise animate back to the nearest snap point.
let v = offset.x > -20 ? 0 : -100;
if (x.get() < -ref.current.offsetWidth * 0.8) {
v = -ref.current.offsetWidth;
onRemove();
}
animate(x, v, {
...inertiaTransition,
min: v,
max: v
});
}}
onDragStart={() => {
// Cancel react-aria press event when dragging starts.
document.dispatchEvent(
new PointerEvent('pointercancel')
);
}}
>
{selectionMode === 'multiple' && (
<SelectionCheckmark isSelected={isSelected} />
)}
<motion.div
layout
layoutDependency={selectionMode}
transition={{ duration: 0.25 }}
className="relative flex items-center px-4 py-2 z-10"
>
{children}
</motion.div>
{selectionMode === 'none' && (
<Button
className="bg-red-600 pressed:bg-red-700 cursor-default text-lg outline-none border-none transition-colors text-white flex items-center absolute top-0 left-[100%] py-2 h-full z-0 isolate focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2"
style={{
// Calculate the size of the button based on the drag position,
// which is stored in a CSS variable above.
width: 'max(100px, calc(-1 * var(--x)))',
justifyContent: align
}}
onPress={onRemove}
// Move the button into view when it is focused with the keyboard
// (e.g. via the arrow keys).
onFocus={() => x.set(-100)}
onBlur={() => x.set(0)}
>
<motion.span
initial={false}
className="px-4"
animate={{
// Whenever the alignment changes, perform a keyframe animation
// between the previous position and new position. This is done
// by calculating a transform for the previous alignment and
// animating it back to zero.
transform: align === 'start'
? [
'translateX(calc(-100% - var(--x)))',
'translateX(0)'
]
: [
'translateX(calc(100% + var(--x)))',
'translateX(0)'
]
}}
>
Delete
</motion.span>
</Button>
)}
</motion.div>
)}
</MotionItem>
);
}
function SelectionCheckmark({ isSelected }) {
return (
<motion.svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6 flex-shrink-0 ml-4"
initial={{ x: -40 }}
animate={{ x: 0 }}
transition={{ duration: 0.25 }}
>
{!isSelected && (
<circle
r={9}
cx={12}
cy={12}
stroke="currentColor"
fill="none"
strokeWidth={1}
className="text-gray-400"
/>
)}
{isSelected && (
<path
className="text-blue-600"
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clipRule="evenodd"
/>
)}
</motion.svg>
);
}
import {
Button,
GridList,
GridListItem
} from 'react-aria-components';
import type {
Selection,
SelectionMode
} from 'react-aria-components';
import {
animate,
AnimatePresence,
motion,
useIsPresent,
useMotionTemplate,
useMotionValue,
useMotionValueEvent
} from 'framer-motion';
import {
useRef,
useState
} from 'react';
import type {CSSProperties} from 'react';
const MotionItem =
motion(GridListItem);
const inertiaTransition =
{
type:
'inertia' as const,
bounceStiffness: 300,
bounceDamping: 40,
timeConstant: 300
};
function SwipableList() {
let [items, setItems] =
useState(
messages.emails
);
let [
selectedKeys,
setSelectedKeys
] = useState<
Selection
>(new Set());
let [
selectionMode,
setSelectionMode
] = useState<
SelectionMode
>('none');
let onDelete = () => {
setItems(
items.filter((i) =>
selectedKeys !==
'all' &&
!selectedKeys
.has(i.id)
)
);
setSelectedKeys(
new Set()
);
setSelectionMode(
'none'
);
};
return (
<div className="flex flex-col h-full max-h-[500px] sm:w-[400px] -mx-[14px] sm:mx-0">
{/* Toolbar */}
<div className="flex pb-4 justify-between">
<Button
className="text-blue-600 text-lg outline-none bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring disabled:text-gray-400"
style={{
opacity:
selectionMode ===
'none'
? 0
: 1
}}
isDisabled={selectedKeys !==
'all' &&
selectedKeys
.size ===
0}
onPress={onDelete}
>
Delete
</Button>
<Button
className="text-blue-600 text-lg outline-none bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring"
onPress={() => {
setSelectionMode(
(
m
) => (m ===
'none'
? 'multiple'
: 'none')
);
setSelectedKeys(
new Set()
);
}}
>
{selectionMode ===
'none'
? 'Edit'
: 'Cancel'}
</Button>
</div>
<GridList
className="relative flex-1 overflow-auto"
aria-label="Inbox"
onAction={selectionMode ===
'none'
? () => {}
: undefined}
selectionMode={selectionMode}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
<AnimatePresence>
{items.map((
item
) => (
<ListItem
key={item
.id}
id={item
.id}
textValue={[
item
.sender,
item
.date,
item
.subject,
item
.message
].join(
'\n'
)}
onRemove={() =>
setItems(
items
.filter(
(
i
) =>
i !==
item
)
)}
>
<div className="flex flex-col text-md cursor-default">
<div className="flex justify-between">
<p className="font-bold text-lg m-0">
{item
.sender}
</p>
<p className="text-gray-500 m-0">
{item
.date}
</p>
</div>
<p className="m-0">
{item
.subject}
</p>
<p className="line-clamp-2 text-gray-500 dark:text-gray-400 m-0">
{item
.message}
</p>
</div>
</ListItem>
))}
</AnimatePresence>
</GridList>
</div>
);
}
function ListItem(
{
id,
children,
textValue,
onRemove
}
) {
let ref = useRef(null);
let x = useMotionValue(
0
);
let isPresent =
useIsPresent();
let xPx =
useMotionTemplate` px`;
// Align the text in the remove button to the left if the
// user has swiped at least 80% of the width.
let [align, setAlign] =
useState('end');
useMotionValueEvent(
x,
'change',
(x) => {
let a =
x <
-ref.current
?.offsetWidth *
0.8
? 'start'
: 'end';
setAlign(a);
}
);
return (
<MotionItem
id={id}
textValue={textValue}
className="outline-none group relative overflow-clip border-t border-0 border-solid last:border-b border-gray-200 dark:border-gray-800 pressed:bg-gray-200 dark:pressed:bg-gray-800 selected:bg-gray-200 dark:selected:bg-gray-800 focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2"
layout
transition={{
duration: 0.25
}}
exit={{
opacity: 0
}}
// Take item out of the flow if it is being removed.
style={{
position:
isPresent
? 'relative'
: 'absolute'
}}
>
{/* @ts-ignore - Framer Motion's types don't handle functions properly. */}
{(
{
selectionMode,
isSelected
}
) => (
// Content of the item can be swiped to reveal the delete button, or fully swiped to delete.
<motion.div
ref={ref}
style={{
x,
'--x': xPx
} as CSSProperties}
className="flex items-center"
drag={selectionMode ===
'none'
? 'x'
: undefined}
dragConstraints={{
right: 0
}}
onDragEnd={(
e,
{ offset }
) => {
// If the user dragged past 80% of the width, remove the item
// otherwise animate back to the nearest snap point.
let v =
offset.x >
-20
? 0
: -100;
if (
x.get() <
-ref
.current
.offsetWidth *
0.8
) {
v = -ref
.current
.offsetWidth;
onRemove();
}
animate(
x,
v,
{
...inertiaTransition,
min: v,
max: v
}
);
}}
onDragStart={() => {
// Cancel react-aria press event when dragging starts.
document
.dispatchEvent(
new PointerEvent(
'pointercancel'
)
);
}}
>
{selectionMode ===
'multiple' &&
(
<SelectionCheckmark
isSelected={isSelected}
/>
)}
<motion.div
layout
layoutDependency={selectionMode}
transition={{
duration:
0.25
}}
className="relative flex items-center px-4 py-2 z-10"
>
{children}
</motion.div>
{selectionMode ===
'none' && (
<Button
className="bg-red-600 pressed:bg-red-700 cursor-default text-lg outline-none border-none transition-colors text-white flex items-center absolute top-0 left-[100%] py-2 h-full z-0 isolate focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2"
style={{
// Calculate the size of the button based on the drag position,
// which is stored in a CSS variable above.
width:
'max(100px, calc(-1 * var(--x)))',
justifyContent:
align
}}
onPress={onRemove}
// Move the button into view when it is focused with the keyboard
// (e.g. via the arrow keys).
onFocus={() =>
x.set(
-100
)}
onBlur={() =>
x.set(0)}
>
<motion.span
initial={false}
className="px-4"
animate={{
// Whenever the alignment changes, perform a keyframe animation
// between the previous position and new position. This is done
// by calculating a transform for the previous alignment and
// animating it back to zero.
transform:
align ===
'start'
? [
'translateX(calc(-100% - var(--x)))',
'translateX(0)'
]
: [
'translateX(calc(100% + var(--x)))',
'translateX(0)'
]
}}
>
Delete
</motion.span>
</Button>
)}
</motion.div>
)}
</MotionItem>
);
}
function SelectionCheckmark(
{ isSelected }
) {
return (
<motion.svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6 flex-shrink-0 ml-4"
initial={{
x: -40
}}
animate={{ x: 0 }}
transition={{
duration: 0.25
}}
>
{!isSelected && (
<circle
r={9}
cx={12}
cy={12}
stroke="currentColor"
fill="none"
strokeWidth={1}
className="text-gray-400"
/>
)}
{isSelected && (
<path
className="text-blue-600"
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clipRule="evenodd"
/>
)}
</motion.svg>
);
}
Tailwind config#
This example uses the tailwindcss-react-aria-components plugin. Add it to your tailwind.config.js
:
module.exports = {
// ...
plugins: [
require('tailwindcss-react-aria-components')
]
};
module.exports = {
// ...
plugins: [
require('tailwindcss-react-aria-components')
]
};
module.exports = {
// ...
plugins: [
require(
'tailwindcss-react-aria-components'
)
]
};