Testimonial Swipe
Paired stories and portraits in a fanned deck with swipe navigation and timed progress.
Install the component, then paste your configured usage. Your settings are included in the code below.
curl -fsS https://arclenui.dev/arclen-ui.mjs -o arclen-ui.mjs && node arclen-ui.mjs add testimonial-swipe --origin https://arclenui.dev --settings '{"sizing":"fill","height":720,"initialStory":1,"headings":["A clearer start","One shared rhythm","Room for the details","A better handoff","Built to keep moving"],"quotes":["There is more room for the work when the next step is clear.","The best change was a quieter Monday. Everyone already knew where to begin.","Our ideas stayed ambitious. Getting them into the world became much simpler.","There is a little more care in every handoff, and you can see it in the result.","We spend less time finding the next step and more time making it count."],"names":["Mara Ellis","Theo Park","Nia Solis","Jules Rowan","Ada Chen"],"roles":["Design director","Studio partner","Creative producer","Product designer","Independent founder"],"portrait1":"","portrait2":"","portrait3":"","portrait4":"","portrait5":"","stageColor":"#F0EDE7","panelColor":"#FBF8F4","textColor":"#1A140F","mutedColor":"#7A6E64","cornerRadius":36,"portraitScale":1,"showProgress":true,"progressColor":"#C8793A","autoPlay":true,"interval":6000,"duration":620,"typeface":"geist","headingScale":1,"quoteScale":1,"contentSpacing":1,"panelMaxWidth":1120,"panelPadding":44,"phonePadding":22,"frameWidth":8,"portraitRadius":28,"frameColor":"#FFFFFF","portraitX":50,"portraitY":50,"showAvatar":true,"showDragHint":true,"dragHint":"DRAG THE CARD","swipeEnabled":true}'import { TestimonialSwipe } from "@/components/testimonial-swipe";
export default function Example() {
return <TestimonialSwipe {...{
"sizing": "fill",
"height": 720,
"initialStory": 1,
"headings": [
"A clearer start",
"One shared rhythm",
"Room for the details",
"A better handoff",
"Built to keep moving"
],
"quotes": [
"There is more room for the work when the next step is clear.",
"The best change was a quieter Monday. Everyone already knew where to begin.",
"Our ideas stayed ambitious. Getting them into the world became much simpler.",
"There is a little more care in every handoff, and you can see it in the result.",
"We spend less time finding the next step and more time making it count."
],
"names": [
"Mara Ellis",
"Theo Park",
"Nia Solis",
"Jules Rowan",
"Ada Chen"
],
"roles": [
"Design director",
"Studio partner",
"Creative producer",
"Product designer",
"Independent founder"
],
"portrait1": "",
"portrait2": "",
"portrait3": "",
"portrait4": "",
"portrait5": "",
"stageColor": "#F0EDE7",
"panelColor": "#FBF8F4",
"textColor": "#1A140F",
"mutedColor": "#7A6E64",
"cornerRadius": 36,
"portraitScale": 1,
"showProgress": true,
"progressColor": "#C8793A",
"autoPlay": true,
"interval": 6000,
"duration": 620,
"typeface": "geist",
"headingScale": 1,
"quoteScale": 1,
"contentSpacing": 1,
"panelMaxWidth": 1120,
"panelPadding": 44,
"phonePadding": 22,
"frameWidth": 8,
"portraitRadius": 28,
"frameColor": "#FFFFFF",
"portraitX": 50,
"portraitY": 50,
"showAvatar": true,
"showDragHint": true,
"dragHint": "DRAG THE CARD",
"swipeEnabled": true
}} />;
}| Property | Type | Default |
|---|---|---|
sizingStage sizing Fill a caller with a defined height, or choose a fixed height. Short stages scroll within the component. Options: fill (Fill parent), fixed (Fixed height) | select | "fill" |
heightFixed height (px) Used when Stage sizing is Fixed height. 480–1200Step 10 | number | 720 |
initialStoryStarting story 1–5Step 1 | number | 1 |
headingsStory headings Entries pair by position. The quotation list sets the number of stories; missing companion entries use the matching example. Minimum 1 itemsMaximum 5 itemsUp to 80 characters per item | string-list | ["A clearer start","One shared rhythm","Room for the details","A better handoff","Built to keep moving"] |
quotesQuotations Entries pair by position. The quotation list sets the number of stories; missing companion entries use the matching example. Minimum 1 itemsMaximum 5 itemsUp to 360 characters per item | string-list | ["There is more room for the work when the next step is clear.","The best change was a quieter Monday. Everyone already knew where to begin.","Our ideas stayed ambitious. Getting them into the world became much simpler.","There is a little more care in every handoff, and you can see it in the result.","We spend less time finding the next step and more time making it count."] |
namesNames Entries pair by position. The quotation list sets the number of stories; missing companion entries use the matching example. Minimum 1 itemsMaximum 5 itemsUp to 80 characters per item | string-list | ["Mara Ellis","Theo Park","Nia Solis","Jules Rowan","Ada Chen"] |
rolesRoles Entries pair by position. The quotation list sets the number of stories; missing companion entries use the matching example. Minimum 1 itemsMaximum 5 itemsUp to 100 characters per item | string-list | ["Design director","Studio partner","Creative producer","Product designer","Independent founder"] |
portrait1Portrait 1 Leave empty for the bundled fictional AI-generated portrait photograph. Up to 12000000 characters | image | "" |
portrait2Portrait 2 Leave empty for the bundled fictional AI-generated portrait photograph. Up to 12000000 characters | image | "" |
portrait3Portrait 3 Leave empty for the bundled fictional AI-generated portrait photograph. Up to 12000000 characters | image | "" |
portrait4Portrait 4 Leave empty for the bundled fictional AI-generated portrait photograph. Up to 12000000 characters | image | "" |
portrait5Portrait 5 Leave empty for the bundled fictional AI-generated portrait photograph. Up to 12000000 characters | image | "" |
stageColorStage | color | "#F0EDE7" |
panelColorPanel | color | "#FBF8F4" |
textColorText | color | "#1A140F" |
mutedColorSecondary text | color | "#7A6E64" |
cornerRadiusPanel corners 0–48Step 1 | number | 36 |
portraitScalePortrait size Scales all portrait cards together while keeping their fan arrangement. 0.8–1.12Step 0.02 | number | 1 |
showProgressShow autoplay progress Show the decorative timer ring around Next. Autoplay timing also works with the ring hidden. | boolean | true |
progressColorProgress ring Accent colour of the autoplay progress arc. | color | "#C8793A" |
autoPlayAutoplay | boolean | true |
intervalAutoplay dwell (ms) The dwell starts after the main card settles. Focus, drag and resume begin a fresh dwell. 4000–15000Step 500 | number | 6000 |
durationTransition duration (ms) 180–900Step 20 | number | 620 |
typefaceTypeface Options: geist (Bundled Geist), system (System sans), serif (System serif) | select | "geist" |
headingScaleHeading scale 0.8–1.25Step 0.05 | number | 1 |
quoteScaleQuotation scale 0.85–1.25Step 0.05 | number | 1 |
contentSpacingContent spacing Scales the story gaps and the space between portraits and text on small screens. 0.75–1.5Step 0.05 | number | 1 |
panelMaxWidthPanel maximum width (px) 800–1440Step 20 | number | 1120 |
panelPaddingPanel padding (px) Used above 700px container width. 24–64Step 2 | number | 44 |
phonePaddingSmall panel padding (px) Used at 700px and below. Very narrow containers reduce this by 4px. 14–36Step 2 | number | 22 |
frameWidthPortrait frame (px) 0–14Step 1 | number | 8 |
portraitRadiusPortrait corners (px) 0–40Step 1 | number | 28 |
frameColorPortrait frame | color | "#FFFFFF" |
portraitXPortrait focal X (%) Horizontal crop position for every portrait and avatar. 0–100Step 1 | number | 50 |
portraitYPortrait focal Y (%) Vertical crop position for every portrait and avatar. 0–100Step 1 | number | 50 |
showAvatarShow identity portrait | boolean | true |
showDragHintShow drag hint | boolean | true |
dragHintDrag hint Up to 40 characters | text | "DRAG THE CARD" |
swipeEnabledEnable swipe Disable pointer dragging while keeping arrow buttons and keyboard navigation available. | boolean | true |
Testimonial Swipe
Paired stories beside a fanned portrait deck, with touch swipes, keyboard navigation and an autoplay progress ring. On small screens, the portraits sit above the story. The package includes its own Geist font and five fictional portrait photographs.
All bundled names, roles and quotations are fictional examples. The AI-generated photographs depict fictional people. Replace the examples with your own approved content before presenting them as customer testimonials.
Install and use
Add Testimonial Swipe from the catalogue’s install action, or copy its complete component directory into your project. Keep component.json, src, assets and the font licence together. The core has no runtime dependencies. The React wrapper uses your application’s React installation; it imports the component stylesheet itself.
import { TestimonialSwipe } from './components/testimonial-swipe/src/react';
export function Stories() {
return <TestimonialSwipe sizing="fixed" height={800} autoPlay={false} />;
}The React component accepts the manifest settings as regular props, plus className, style and paused. It renders the selected initial story on the server and enhances it after mount. Component is an alias for TestimonialSwipe.
Configure the deck
Use paired arrays for your content, and a portrait control for each story. This complete React example uses two fictional stories, a different colour scheme and smaller, unframed portraits:
import { TestimonialSwipe } from './components/testimonial-swipe/src/react';
export function StudioStories() {
return (
<TestimonialSwipe
sizing="fixed"
height={800}
autoPlay={false}
headings={['A calmer Monday', 'A shared next step']}
quotes={[
'A fictional studio finding more time for the work.',
'A fictional team making its next decision together.',
]}
names={['Mara Ellis', 'Theo Park']}
roles={['Studio partner', 'Design director']}
stageColor="#D8E4DC"
panelColor="#F8FAF7"
textColor="#213A32"
mutedColor="#536A60"
portraitScale={0.9}
frameWidth={0}
portraitRadius={18}
headingScale={0.95}
contentSpacing={1.1}
showAvatar={false}
dragHint="SWIPE TO EXPLORE"
/>
);
}The quotation list sets the number of stories, from one to five. Headings, names and roles pair by position; missing companion entries use the matching bundled example. initialStory is one-based and clamps to the available story count. Navigation wraps in both directions.
Each portrait1 through portrait5 control replaces one image. Empty values use the bundled WebP photographs, which are 960 × 1282. Supported replacements are HTTPS URLs, local relative paths, PNG/JPEG/WebP/GIF data URLs and application blob URLs. Keep blob URLs alive while the component uses them. Remote images are requested only when supplied by the application. The package makes no remote requests with its bundled defaults.
portraitX and portraitY adjust the shared crop position for cards and identity portraits. frameWidth, portraitRadius and frameColor style the card frame. portraitScale scales the complete fan, including its frame. Choose portraits with enough space around the face for cropping.
Typography and layout
typeface selects bundled Geist, system sans or system serif without downloading fonts. headingScale and quoteScale adjust the responsive text sizes independently. contentSpacing changes story spacing and the small-screen gap between the deck and copy. Long text expands the panel; check your actual copy at phone and desktop widths.
panelMaxWidth, panelPadding and phonePadding control the panel’s dimensions and inner spacing. Desktop padding applies above 700px container width; small-screen padding applies below it, with a further 4px reduction at 340px and below. cornerRadius controls panel corners, capped at 28px on small screens. Portrait corners have their own control.
The component responds to container width. sizing="fill" fills a parent with a defined height. Without a defined parent height, content supplies the natural height. sizing="fixed" uses height in pixels. Short containers scroll inside the component so content and controls remain reachable. For a fill layout, give the containing element a height such as style={{ height: 800 }}.
Stage, panel, text, secondary text, portrait frame and progress accent colours are independent. Choose readable contrast. Transparent colours expose the underlying surface and may need an explicit host background.
showAvatar hides the small identity portrait while retaining the person’s name and role. showDragHint hides the drag guidance; dragHint changes its text. Keep the hint short on narrow screens. Disabling swipeEnabled also hides the hint, removes the deck from the tab sequence and keeps navigation buttons and arrow keys available.
Interaction and motion
Swipe left to advance or right to go back. The current portrait leaves toward the left, the next portrait settles at the front, and the departing portrait rejoins the hidden back of the stack. Going back rearranges the fan toward the preceding portrait. Count, heading, quotation and identity enter with a short stagger. Navigation during the main card transition is ignored; another action is accepted after it settles.
Previous and Next wrap through every story. Left and Right arrow keys work while focus is inside the component. Touch dragging leaves vertical page scrolling available. Buttons provide the same navigation without dragging. Decorative portrait layers do not repeat announcements; manual changes announce the story number and name. Portraits are represented by their accompanying names rather than duplicate alternative text.
autoPlay, interval and duration control automatic playback, dwell and transition time. The next dwell starts after the main card settles. At the default 6000ms dwell, story changes are roughly 6.6 seconds apart. The decorative ring around Next shows the dwell. showProgress hides it and progressColor changes its accent; hiding the ring does not affect the timer.
Pause and Resume control autoplay while leaving manual navigation available. Hover alone does not pause it. Focus inside the component and active dragging suspend the dwell. Moving focus away starts a fresh dwell. Hidden documents, offscreen components and an externally paused controller suspend autoplay and active transitions. Resume starts a fresh dwell and resets the progress ring without replaying missed stories.
With reduced motion enabled, autoplay stops and manual changes are immediate. When the Web Animations API is unavailable, navigation changes content immediately; autoplay still uses its configured dwell. If Intersection Observer is unavailable, scroll and resize events check visibility. No continuous polling loop is used.
Server-rendered and unenhanced HTML contains the selected story and portrait deck with inactive controls. A single-story deck disables navigation and autoplay.
Vanilla lifecycle
Provide a host element such as <div id="stories"></div>, then mount the core and import its CSS:
import { mount, type Settings } from './components/testimonial-swipe/src/core';
import './components/testimonial-swipe/src/styles.css';
const host = document.querySelector<HTMLElement>('#stories');
if (!host) throw new Error('Missing stories host.');
const settings: Partial<Settings> = {
sizing: 'fixed',
height: 800,
autoPlay: false,
headingScale: 0.95,
progressColor: '#486857',
};
const controller = mount(host, settings);
controller.update({ ...settings, showAvatar: false });The HTML download includes its own local script, stylesheet and assets. Keep that directory structure intact when serving it from a subpath. The vanilla core and React wrapper use the same settings and validation.
update resolves a complete settings object against manifest defaults. To retain configured values, pass them again, as above. The current story is preserved where possible; changing initialStory selects that starting story. Invalid settings throw before changing a mounted component.
Call these lifecycle methods from your application as needed (fragment using the controller above):
controller.pause();
controller.resume();
controller.destroy();Destroy removes the instance’s markup, listeners, observers, timers and finite animations. It is safe to call more than once. Multiple mounts own independent state. The React wrapper destroys its controller when unmounted; its paused prop delegates to the same pause policy.
Settings and assets
The settings table is generated from component.json, including each control’s default and bounds. Unknown controls, invalid selections, out-of-range numbers, malformed story arrays and unsafe image sources throw an error.
The JavaScript, styles and fictional example copy are original Arclen UI work. The bundled synthetic photographs are original artwork generated for Arclen. Geist uses the SIL Open Font License 1.1. See NOTICE.md, PROVENANCE.md and assets/OFL-Geist.txt for asset details.
The preview runs the component’s own JavaScript. It respects reduced motion and pauses when off screen. The HTML bundle includes a poster fallback.
Every setting in this panel comes from the component manifest. Preview size, pause and replay stay in the inspector.