← Build log

Expandable Text Systems: Handling Long Content Without Truncation

When User-Generated Content Breaks Your Clean Interface Design

By: Jeremy (TPM learning that content varies more than you expect)


Here’s a problem every TPM encounters but rarely plans for: What happens when user-generated content doesn’t fit your carefully designed interface?

Building VoiceCatcher, I designed a clean session history interface with consistent spacing and predictable layouts. Then users started creating hour-long voice recordings with 10,000+ character transcripts. Suddenly my “clean interface” became a wall of unreadable text that destroyed the entire user experience.

This is the story of how we went from truncating transcripts at 120 characters (losing critical information) to building an expandable text system that handles any content length gracefully. Because the best interfaces adapt to content, not the other way around.

The Setup: When Perfect Design Meets Real Content

The context: Our history window showed a list of voice sessions with:

  • Session timestamp
  • Short transcript preview
  • Action buttons (play, copy, delete)

I designed this assuming transcripts would be sentence-length snippets—maybe 50-100 characters. The interface looked perfect in mockups.

Then real usage happened:

  • Meeting recordings: 45-minute sessions with thousands of words
  • Interview transcripts: Hour-long conversations
  • Lecture notes: Dense academic content
  • Brainstorming sessions: Stream-of-consciousness rambling

My “preview” interface couldn’t handle any of these real-world use cases.

The Problem: Truncation Destroys Content Value

Attempt 1: Fixed Character Truncation

The approach: Limit previews to 120 characters with ”…” ellipsis.

function truncateText(text: string, maxLength: number = 120): string {
  return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
}

The result: Useless previews. For a 2,000-word transcript, showing the first 120 characters provided zero value for identifying or finding content.

User feedback: “I can’t tell what any of these sessions are about from the previews.”

Attempt 2: Smart Truncation

The approach: Truncate at sentence boundaries, show first complete sentences.

The problem: Real speech doesn’t follow clean sentence structures. Voice recordings have:

  • Long, rambling sentences that go on for paragraphs
  • Sentence fragments and interruptions
  • Lists and enumeration without clear breaks
  • “Um,” “uh,” and filler words that break sentence detection

The realization: Truncation fundamentally conflicts with the value proposition of voice transcription. You’re literally hiding the content users created the tool to capture.

The Solution: Progressive Disclosure for Text Content

The Design Principle

Instead of hiding content, make content exploration feel natural and controlled.

The Implementation: 3-Line Preview System

interface ExpandableTextProps {
  text: string;
  maxLines?: number;
  className?: string;
}

const ExpandableText: React.FC<ExpandableTextProps> = ({
  text,
  maxLines = 3,
  className = ''
}) => {
  const [isExpanded, setIsExpanded] = useState(false);
  const [showToggle, setShowToggle] = useState(false);

  // Check if content exceeds max lines
  useEffect(() => {
    // ... line height calculation logic
  }, [text, maxLines]);

  return (
    <div className={className}>
      <div
        className={isExpanded ? 'expanded' : 'collapsed'}
        style={{
          maxHeight: isExpanded ? 'none' : `${maxLines * 1.5}rem`,
          overflow: 'hidden',
          transition: 'max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
        }}
      >
        {text}
      </div>

      {showToggle && (
        <button
          onClick={() => setIsExpanded(!isExpanded)}
          className="show-more-button"
        >
          {isExpanded ? 'Show less' : 'Show more'}
        </button>
      )}
    </div>
  );
};

The UX Strategy

  1. Default view: Show exactly 3 lines of content
  2. Clear indication: Fade gradient if content continues
  3. Progressive disclosure: “Show more” reveals full content
  4. Reversible action: “Show less” returns to preview
  5. Performance optimization: Only render visible content initially

The TPM Framework: Content-Adaptive Interface Design

1. Design for Content Variance, Not Content Ideals

Never design assuming content will fit your preferred format.

The Content Audit Prompt:

For this interface showing [content type]:
1. What's the shortest realistic content example?
2. What's the longest realistic content example?
3. What's the most complex/irregular content structure?
4. How does content value correlate with content length?
5. What happens to user workflow if we hide/truncate content?

2. Progressive Disclosure as Product Strategy

Don’t think of long content as a problem to solve—think of it as an opportunity to provide depth when users want it.

The Progressive Disclosure Prompt:

I have content that varies from [shortest] to [longest] in this interface.
Design a progressive disclosure system that:
1. Provides value in the collapsed state
2. Makes expansion feel natural and reversible
3. Maintains interface cleanliness
4. Performs well with large content
5. Works across different content types

3. Performance as UX Constraint

Content expansion systems must feel instant, even with large content.

The Performance Prompt:

I need to display expandable text content up to [max size].
Optimize for:
1. Initial render performance (collapsed state)
2. Expansion animation smoothness
3. Memory usage with multiple expanded items
4. Scroll position preservation
5. Mobile device compatibility

The Technical Implementation: Performance-First Expansion

CSS-First Approach

.expandable-text {
  position: relative;
  transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  contain: layout style; /* Performance optimization */
}

.expandable-text.collapsed {
  max-height: 4.5rem; /* 3 lines at 1.5rem line-height */
  overflow: hidden;
}

.expandable-text.collapsed::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 1.5rem;
  background: linear-gradient(transparent, var(--background-color));
  pointer-events: none;
}

.expandable-text.expanded {
  max-height: none;
}

Why CSS-First Works

  • Performance: Browser-native animations are smoother than JavaScript
  • Accessibility: Works with screen readers and keyboard navigation
  • Maintenance: Less complex state management
  • Reliability: Degrades gracefully if JavaScript fails

Smart Measurement Logic

const useTextMeasurement = (text: string, maxLines: number) => {
  const [exceeds, setExceeds] = useState(false);
  const measureRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!measureRef.current) return;

    const element = measureRef.current;
    const lineHeight = parseInt(getComputedStyle(element).lineHeight);
    const maxHeight = lineHeight * maxLines;

    setExceeds(element.scrollHeight > maxHeight);
  }, [text, maxLines]);

  return { exceeds, measureRef };
};

The Business Impact: When Content Drives Adoption

Before: Truncation System

  • Users couldn’t identify sessions from previews
  • Important content was completely hidden
  • History browsing became tedious search process
  • Long recordings felt “broken” or “incomplete”

After: Expandable System

  • Users could quickly scan session content
  • Full transcripts accessible when needed
  • History browsing became efficient content navigation
  • Long recordings felt like rich, valuable content

The Adoption Metrics

  • History engagement: 300% increase in history window usage
  • Content discovery: Users found relevant past content faster
  • Feature confidence: Users recorded longer sessions knowing content remained accessible
  • User satisfaction: Elimination of “lost content” complaints

Advanced: Content-Aware UX Optimizations

Intelligent Default States

const getOptimalPreviewLines = (contentLength: number): number => {
  if (contentLength < 200) return 2; // Short content needs less preview
  if (contentLength < 1000) return 3; // Medium content gets standard preview
  return 4; // Long content gets more preview
};

Contextual Show/Hide Behavior

const ExpandableText = ({ text, context }) => {
  // In search results: start expanded if text contains search term
  // In list view: start collapsed for clean scanning
  // In detail view: start expanded for full content consumption

  const initialExpanded = context === 'search' && containsSearchTerm(text);
  const [isExpanded, setIsExpanded] = useState(initialExpanded);

  // ... rest of component
};

Performance Optimizations for Scale

// Virtual scrolling for lists with many expandable items
const VirtualizedExpandableList = ({ items }) => {
  return (
    <VirtualList
      height={600}
      itemCount={items.length}
      itemSize={getItemSize} // Dynamic sizing based on expansion state
      itemData={items}
    >
      {({ index, style, data }) => (
        <div style={style}>
          <ExpandableText
            text={data[index].transcript}
            // Only fully render if in viewport
            lazy={!isInViewport(index)}
          />
        </div>
      )}
    </VirtualList>
  );
};

The Psychology: Why Truncation Breaks User Trust

Cognitive Completeness Expectation

Users expect to access the full value of content they created. Truncation signals that the system is hiding something they own.

VoiceCatcher example: When users recorded 30-minute meetings, seeing only “Well, I think we should…” in the preview created anxiety that content was lost or incomplete.

Progressive Disclosure Reduces Cognitive Load

Paradox: Showing users they can access more content actually makes interfaces feel cleaner than hiding content entirely.

User feedback: “I like that I can see there’s more content without having to see all of it at once.”

Control Increases Perceived Value

Users value content more when they control how much they see. The ability to expand/collapse makes content feel actively curated rather than accidentally truncated.

Common TPM Content Display Mistakes (That I Made)

Mistake 1: Designing for Ideal Content

Wrong thinking: “Users will create short, well-formatted content.” Reality: Real content is messy, long, and unpredictable.

Fix: Design for the worst-case content first, then optimize for typical content.

Mistake 2: Treating Content Length as UI Problem

Wrong thinking: “Long content breaks our design, so we’ll limit it.” Reality: Long content often represents high user engagement and value.

Fix: Build interfaces that scale with content value, not against it.

Mistake 3: Hidden Truncation Logic

Wrong thinking: “Users won’t notice smart truncation.” Reality: Users always notice when content is missing, even if they can’t articulate why.

Fix: Make content limitation explicit and user-controlled.

Mistake 4: Performance vs. UX False Choice

Wrong thinking: “We can’t show long content because it will be slow.” Reality: Users will wait for content they want, but won’t wait for content they don’t need.

Fix: Use progressive loading and smart defaults to optimize for typical usage while supporting edge cases.

Advanced: The Content-First Design Philosophy

Content Drives Layout, Not Vice Versa

Instead of forcing content into predetermined layouts, build layouts that adapt to content characteristics.

const adaptiveLayout = {
  short: 'single-line', // < 50 chars
  medium: 'multi-line', // 50-500 chars
  long: 'expandable', // 500-2000 chars
  document: 'modal-view', // > 2000 chars
};

Value-Based Progressive Disclosure

Show content based on user value, not just character count.

const getPreviewStrategy = (content: TranscriptContent) => {
  const { length, keywords, searchMatch, userInteraction } = content;

  // High-value content gets more preview space
  if (searchMatch || keywords.length > 3) return 'expanded-preview';
  if (userInteraction.recent) return 'standard-preview';
  return 'minimal-preview';
};

Accessibility-First Expansion

const ExpandableText = ({ text, ...props }) => {
  return (
    <div>
      <div
        id={`content-${id}`}
        aria-expanded={isExpanded}
        role="region"
        aria-label={`Transcript content, ${isExpanded ? 'expanded' : 'collapsed'}`}
      >
        {text}
      </div>

      <button
        aria-controls={`content-${id}`}
        aria-expanded={isExpanded}
        onClick={() => setIsExpanded(!isExpanded)}
      >
        {isExpanded ? 'Show less content' : 'Show full transcript'}
      </button>
    </div>
  );
};

The Prompts That Enable Content-Adaptive Design

Content Variance Analysis Prompt

I'm building [interface] that displays [content type].
Analyze content variance:
1. Minimum/maximum realistic content length
2. Typical content structure and patterns
3. High-value vs. low-value content characteristics
4. User scanning vs. deep-reading scenarios
5. Performance constraints at scale

Design a display system that adapts to this variance.

Progressive Disclosure Strategy Prompt

Create a progressive disclosure system for [content type] with these requirements:
- Preview provides genuine value for content assessment
- Expansion feels natural and reversible
- Performance remains smooth with [max content size]
- Design maintains visual consistency across content lengths
- Accessibility support for all interaction modes

Include CSS, interaction patterns, and performance optimizations.

Performance Optimization Prompt

I have expandable text content that can be up to [size] with [number] items on screen.
Optimize for:
1. Smooth expansion animations
2. Memory efficiency with multiple expanded items
3. Scroll position preservation during expansion
4. Mobile device performance
5. Screen reader compatibility

Provide implementation strategies and measurement approaches.

Lessons Learned: Content as Product Feature

Long Content Indicates High User Engagement

Users who create lengthy content are power users who get significant value from your product. Optimizing for them often improves the experience for everyone.

Metric discovery: Sessions with 1000+ character transcripts had 5x higher user retention than short sessions.

Progressive Disclosure Increases Overall Engagement

When users can control content depth, they explore more content overall.

Before: Users looked at 2-3 history items on average After: Users looked at 8-10 history items, expanding 40% of them

Performance Investment Enables Product Differentiation

Smooth handling of long content became a competitive advantage—other voice tools either truncate aggressively or perform poorly with substantial content.

The Meta-Lesson: Design for Content Reality, Not Content Ideals

The biggest insight from building VoiceCatcher’s expandable text system: User-generated content will always exceed your design assumptions.

The question isn’t “How do we make content fit our interface?” but “How do we make our interface valuable regardless of content characteristics?”

Our expandable text system didn’t just solve a UI problem—it enabled users to create longer, more valuable content because they knew it would remain accessible and organized.

Practical Implementation for TPMs

Content Audit Before Design

Before designing any content display interface, spend time with real content examples. Especially the extreme cases.

Build for Content Growth

Assume content length and complexity will increase over time as users become more engaged with your product.

Performance as Product Quality

Smooth content interaction isn’t a technical nice-to-have—it’s a product quality indicator that affects user perception.

Measure Content Engagement

Track how users interact with content at different lengths. This data informs future interface decisions.

The Bottom Line

The best content interfaces don’t constrain content—they make any amount of content feel manageable and valuable.

VoiceCatcher’s expandable text system transformed hour-long transcripts from “overwhelming walls of text” into “rich content I can navigate efficiently.” The system didn’t just display content—it made content feel valuable and organized.

As TPMs, we need to recognize that content display choices directly affect how much value users extract from our products. Content-adaptive interfaces don’t just look better—they enable users to create and consume more value.

The expandable text system became one of our most praised features not because it was technically complex, but because it respected the content users created while maintaining interface usability.

That’s the difference between displaying content and enabling content value.


Jeremy is a technical product manager turned solo developer, building VoiceCatcher as a local-first voice transcription tool. Follow the journey at [project repo] or connect on [social links].

Next in this series: “Custom Title Bars and Window Management in Electron” - How removing the browser title bar led to implementing custom window controls and drag functionality while maintaining native platform behavior.