A component for truncating and revealing content with smooth motion and fade effects.
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Minus quisquam assumenda eligendi provident magni. error voluptatibus obcaecati ab qui necessitatibus.
<script setup lang="ts">
import { ref } from 'vue'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
import { ShowMore, ShowMoreItem, ShowMoreButton, ShowMoreContent } from '@/components/ui/show-more'
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core'
import { ChevronDown, ChevronUp } from 'lucide-vue-next'
// This is only for demonstration purposes to show the text on mobile and adjust the size to show or hide it.
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('sm')
// This is only for demonstration purposes to show the text on mobile and adjust the size to show or hide it.
const openItem = ref('none')
const shortText = {
preview: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit.',
expanded:
'Minus quisquam assumenda eligendi provident magni. error voluptatibus obcaecati ab qui necessitatibus.',
}
</script>
<template>
<ResizablePanelGroup direction="horizontal" class="rounded-lg border">
<!-- LEFT PANEL (CONTENT) -->
<ResizablePanel :default-size="60">
<!-- ShowMore Demo Content -->
<div class="size-full">
<ShowMore
:threshold="3"
type="single"
collapsible
v-model="openItem"
fade
:animation="{
duration: 0.5,
ease: 'backOut',
}"
>
<ShowMoreItem value="demo" v-slot="{ isTruncated }">
<div class="p-6">
<ShowMoreContent class="text-muted-foreground">
<!-- This is only for demonstration purposes to show the text on mobile and adjust the size to show or hide it. -->
<p v-if="isMobile">{{ shortText.preview }}</p>
<p v-else>{{ shortText.preview }} {{ shortText.expanded }}</p>
</ShowMoreContent>
<div v-if="isTruncated" class="flex items-center gap-4 mt-4">
<div class="flex-1 h-px bg-border"></div>
<ShowMoreButton
class="group h-8 px-4 text-xs font-semibold uppercase tracking-wide rounded-full border border-border flex items-center gap-2 [&>svg]:hidden"
>
<span v-if="openItem !== 'demo'" class="flex items-center gap-2">
Show More
<ChevronDown class="w-3 h-3" />
</span>
<span v-else class="flex items-center gap-2">
Show Less
<ChevronUp class="w-3 h-3" />
</span>
</ShowMoreButton>
<div class="flex-1 h-px bg-border"></div>
</div>
</div>
</ShowMoreItem>
</ShowMore>
</div>
</ResizablePanel>
<!-- HANDLE -->
<ResizableHandle />
<!-- RIGHT PANEL -->
<ResizablePanel :default-size="40">
<div class="h-full flex items-center justify-center text-xs text-muted-foreground">
horizontal Resize
</div>
</ResizablePanel>
</ResizablePanelGroup>
</template>The ShowMore component allows you to keep your interface clean by truncating long content and giving users the choice to expand it. It features height-based animations via motion-v and an optional fade effect for a premium feel.
Installation
pnpm dlx sulaf@latest add show-more
ShowMore vs. Accordion
While both components handle content expansion, they serve different architectural purposes:
- Accordion: A structural component where you manually define which part is the trigger (always visible) and which part is the content (collapsed). It's best for FAQ-style lists or structured menus.
- ShowMore: A high-level abstraction built on top of Accordion that handles automatic truncation. You provide the full content, and the component calculates how much to reveal based on a
threshold(lines or characters). It also supports built-in fade-out masks and conditional toggles.
Usage
Basic Usage
Wrap your content in ShowMoreContent and use ShowMoreButton to toggle the state.
<script setup lang="ts">
import { ShowMore, ShowMoreItem, ShowMoreButton, ShowMoreContent } from '@/components/ui/show-more'
</script>
<template>
<ShowMore :threshold="3">
<ShowMoreItem value="item-1">
<ShowMoreContent>
<!-- Your long content here -->
</ShowMoreContent>
<ShowMoreButton>Show More</ShowMoreButton>
</ShowMoreItem>
</ShowMore>
</template>Examples
Multi-item ShowMore
Use type="multiple" and bind to an array to manage the open state of multiple ShowMoreItems.
First Section
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Second Section
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Third Section
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
<script setup lang="ts">
import { ref } from 'vue'
import { ShowMore, ShowMoreItem, ShowMoreButton, ShowMoreContent } from '@/components/ui/show-more'
import { ChevronDown, ChevronUp } from 'lucide-vue-next'
const openItems = ref<string[]>([])
const longText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`
const items = [
{
value: 'item-1',
title: 'First Section',
content: longText,
},
{
value: 'item-2',
title: 'Second Section',
content: longText.repeat(2),
},
{
value: 'item-3',
title: 'Third Section',
content: longText.repeat(3),
},
]
</script>
<template>
<div class="w-full max-w-xl space-y-4">
<ShowMore :threshold="3" type="multiple" v-model="openItems" collapsible fade>
<ShowMoreItem
v-for="item in items"
:key="item.value"
:value="item.value"
v-slot="{ isTruncated }"
>
<h3 class="font-semibold text-lg mb-2">{{ item.title }}</h3>
<ShowMoreContent class="text-muted-foreground">
<p>{{ item.content }}</p>
</ShowMoreContent>
<div v-if="isTruncated" class="flex items-center gap-4 mt-4">
<div class="flex-1 h-px bg-border"></div>
<ShowMoreButton
class="group h-8 px-4 text-xs font-semibold uppercase tracking-wide rounded-full border border-border flex items-center gap-2 [&>svg]:hidden"
>
<span v-if="!openItems.includes(item.value)" class="flex items-center gap-2">
Show More
<ChevronDown class="w-3 h-3" />
</span>
<span v-else class="flex items-center gap-2">
Show Less
<ChevronUp class="w-3 h-3" />
</span>
</ShowMoreButton>
<div class="flex-1 h-px bg-border"></div>
</div>
</ShowMoreItem>
</ShowMore>
</div>
</template>Controlled ShowMore
Control the open state externally using v-model:open.
Controlled ShowMore
This is some very long content that demonstrates how the ShowMore component can be controlled externally. By binding a boolean ref to the 'v-model:open' prop, you can programmatically toggle the expanded state of the content. This is useful for scenarios where you might want to open or close the content based on user actions outside of the ShowMore component itself, or based on application logic. The text will expand and collapse smoothly with the configured animation.
<script setup lang="ts">
import { ref } from 'vue'
import { ShowMore, ShowMoreItem, ShowMoreButton, ShowMoreContent } from '@/components/ui/show-more'
import { Button } from '@/components/ui/button'
import { ChevronDown, ChevronUp } from 'lucide-vue-next'
const isShowMoreOpen = ref(false)
const longText = `This is some very long content that demonstrates how the ShowMore component can be controlled externally. By binding a boolean ref to the 'v-model:open' prop, you can programmatically toggle the expanded state of the content. This is useful for scenarios where you might want to open or close the content based on user actions outside of the ShowMore component itself, or based on application logic. The text will expand and collapse smoothly with the configured animation.`
</script>
<template>
<div class="w-full max-w-xl space-y-4">
<ShowMore :threshold="3" v-model:open="isShowMoreOpen" collapsible fade>
<ShowMoreItem value="controlled-item" v-slot="{ isTruncated }">
<h3 class="font-semibold text-lg mb-2">Controlled ShowMore</h3>
<ShowMoreContent class="text-muted-foreground">
<p>{{ longText }}</p>
</ShowMoreContent>
<div class="flex items-center gap-4 mt-4">
<div class="flex-1 h-px bg-border"></div>
<ShowMoreButton
class="group h-8 px-4 text-xs font-semibold uppercase tracking-wide rounded-full border border-border flex items-center gap-2 [&>svg]:hidden"
>
<span v-if="!isShowMoreOpen" class="flex items-center gap-2">
Show More
<ChevronDown class="w-3 h-3" />
</span>
<span v-else class="flex items-center gap-2">
Show Less
<ChevronUp class="w-3 h-3" />
</span>
</ShowMoreButton>
<div class="flex-1 h-px bg-border"></div>
</div>
</ShowMoreItem>
</ShowMore>
<Button @click="isShowMoreOpen = !isShowMoreOpen">
Toggle from outside (Currently: {{ isShowMoreOpen ? 'Open' : 'Closed' }})
</Button>
</div>
</template>Fade Effect
Add the fade prop to the root component to apply a gradient mask to the bottom of the collapsed content.
<ShowMore fade :threshold="3">
<!-- ... -->
</ShowMore>Custom Animation
You can customize the expansion duration and easing function.
<ShowMore
:animation="{
duration: 0.5,
ease: 'backOut'
}"
>
<!-- ... -->
</ShowMore>Want different defaults? You can modify the animation configuration directly in the types.ts file to create the perfect animation for your needs. Just update the default values there and they'll apply globally.
If you discover a particularly smooth or delightful animation configuration, consider sharing it! Open a pull request with your improved defaults so others can benefit from your work.
API Reference
ShowMore (Root)
The root component extends Reka UI's AccordionRoot and defines the core truncation logic.
| Prop | Type | Default | Description |
|---|---|---|---|
threshold | number | 3 | The number of lines or characters to show before truncating. |
truncationType | 'lines' | 'chars' | 'lines' | The method used to determine where to truncate. |
fade | boolean | false | Whether to apply a fade-out effect when collapsed. |
forceMount | boolean | true | Keeps content in the DOM for precise height measurements. |
showToggle | boolean | true | Whether to display the toggle button functionality. |
v-model:open | boolean | false | Reactive boolean state for two-way expansion control. |
lineHeight | string | '1.5rem' | CSS line-height used for height calculations. |
animation | ShowMoreAnimation | { duration: 0.2, ease: 'easeInOut' } | Motion configuration for expansion. |
value | string | - | Required. A unique identifier for the item. |
ShowMoreAnimation
import type { Easing } from 'motion-v'
type ShowMoreAnimation = {
duration?: number // Duration in seconds
ease?: Easing // Easing function (e.g., 'easeInOut', 'backOut')
}Accessibility
The ShowMore component is built on top of Reka UI's Accordion primitive, inheriting its accessibility features and following the WAI-ARIA Accordion pattern.
Keyboard Interactions
| Key | Action |
|---|---|
Space/Enter | Toggles the expanded/collapsed state of the content. |
ARIA Attributes
ShowMore(viaAccordionRoot) usesaria-multiselectablewhentype="multiple".ShowMoreItem(viaAccordionItem) hasaria-labelledbylinking it to its trigger.ShowMoreButton(viaAccordionTrigger) automatically managesaria-expandedandaria-controlsattributes.ShowMoreContent(viaAccordionContent) usesidandaria-labelledbyto link to its associated trigger.
Labeling
Ensure that the content within ShowMoreButton is descriptive and clearly indicates the action, such as "Show More" or "Read Full Article." For more complex scenarios, consider adding an aria-label to the ShowMoreButton.