Learn how to create a reusable React hook that detects text truncation in UI elements, enabling intelligent tooltips and responsive design adjustments
The Truncation Detection Problem
In modern UIs, we often truncate text with CSS when it exceeds container bounds:
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
This is cool and all, but sometimes we need to keep those truncated strings helpful and communicative for our users. To that end, it would be helpful to know whether our string has been truncated or not. This knowledge could open such opportunities as
This knowledge could open such opurtunites as:
Showing tooltips only when content is truncated
Dynamically adjusting layouts
Providing expand/collapse functionality
Can we actually detect that?
Yes, yes we can!
A very rudimentary attempt could be made by checking element dimensions:
const isTruncated = element.scrollWidth > element.clientWidth
While this works OK, it has several limitations:
Doesn’t respond to window resizing
Requires “manual” DOM access
Definitely lacks React lifecycle awareness
Doesn’t handle edge cases (like flex containers)
To make this work the best with React, we definitely could use a hook.
Solution
For this to work, we need a hook with:
Type safety with generics
ResizeObserver for responsiveness
Simple API
import { RefObject, useEffect, useRef, useState } from 'react'
interface UseDetectedTruncation
{
ref: RefObject
isTruncated: boolean
}export const useDetectedTruncation = <
RefType extends HTMLElement,
>(): UseDetectedTruncation => {
const [isTruncated, setIsTruncated] = useState(false)
const elementRef = useRef(null)
const checkTruncation = () => {
const element = elementRef.current
if (!element) return
// Check both width and height for multi-line truncation
const isWidthTruncated = element.scrollWidth > element.clientWidth
const isHeightTruncated = element.scrollHeight > element.clientHeight
setIsTruncated(isWidthTruncated || isHeightTruncated)
}
useEffect(() => {
const element = elementRef.current
if (!element) return
// Initial check
checkTruncation()
// Set up observation
const resizeObserver = new ResizeObserver(checkTruncation)
resizeObserver.observe(element)
// MutationObserver for content changes
const mutationObserver = new MutationObserver(checkTruncation)
mutationObserver.observe(element, {
childList: true,
subtree: true,
characterData: true,
})
return () => {
resizeObserver.disconnect()
mutationObserver.disconnect()
}
}, [])
return { ref: elementRef, isTruncated }
}
Practical Usage
Here’s how to create a smart tooltip component using our hook:
import { Tooltip, type TooltipProps } from '@your-ui-library'
import { twMerge } from 'tailwind-merge'
interface SmartTooltipProps extends React.HTMLAttributes {
tooltipProps: Omit
content: string
}
export const SmartTooltip = ({
tooltipProps,
content,
children,
className,
...props
}: SmartTooltipProps) => {
const { isTruncated, ref } = useDetectedTruncation()
return (
{children || content}
)
}
Performance Considerations
Debounce Observations: For frequently resizing elements, consider debouncing the checks:
const debouncedCheck = useDebounce(checkTruncation, 100)
2. Selective Observation: Only observe necessary attributes:
resizeObserver.observe(element, { box: 'content-box' })
3. Cleanup: Properly disconnect observers in the cleanup function to prevent memory leaks.
Testing Strategies
Verify the hook works in different scenarios:
Static truncated text
Dynamically loaded content
Responsive layout changes
Multi-line truncation (line-clamp)
Nested scrolling containers
describe('useDetectedTruncation', () => {
it('detects horizontal truncation', () => {
const { result } = renderHook(() => useDetectedTruncation())
render(
Long text that should truncate
,
)
expect(result.current.isTruncated).toBe(true)
})
it('ignores non-truncated content', () => {
const { result } = renderHook(() => useDetectedTruncation())
render(
Short text
,
)
expect(result.current.isTruncated).toBe(false)
})
})
Going forward
To make it even sexier, we could consider adding the following features:
https://medium.com/media/d37d1c060816395a578aba437cc12a89/href
Conclusion
The useDetectedTruncation hook provides a clean, reusable solution for a common UI challenge. By encapsulating the detection logic, we can:
Create more accessible interfaces
Build smarter components
Reduce unnecessary tooltip clutter
Make our UIs more responsive to content changes.
Originally published at https://www.pawelkrystkiewicz.pl on November 20, 2024.