← All Articles A Product of Kinsa Creative

Truncating Text in CSS

Limit Text to a Single Line

First of all, consider whether you really have to limit the text to a single line. It is a bad solution to most problems. It hides information from the user — information which is necessary in most cases to make a decision. Better solutions include creating a design that responds to varying content or addressing the problem at a content level, or both.

Making the full text available on a hover is better than making the user click through, but it still requires interaction to make a decision.

white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

The container element may also need overflow: hidden; applied to it (if it is within, for example, a grid column that is auto width) or otherwise have a fixed width for this to work properly.

Text Overflow In Tailwind CSS

Text overflow control is built into Tailwind CSS via the truncate class.

Limit Text to Multiple Lines

For multiple lines of text, CSS offers the line-clamp attribute, which can be used to limit text to a specific number of lines.

display: -webkit-box;
line-clamp: 3; /* Number of lines */
-webkit-box-orient: vertical;
overflow: hidden;

Line Clamp In Tailwind CSS

Line clamping is built into Tailwind CSS via the line-clamp-* class.

Complex Example: A React Component with Buttons to Show/Hide the Full Text

This component uses Tailwind CSS for styling.

It truncates text to a certain number of lines and then overlays a button in the bottom right corner with a transparent to opaque fade (since it is covering the ellipsis).

The button is keyboard and screenreader accessible with visual focus indicators via focus ring styling for keyboard readers and clear button text for screen reader support.

import { useCallback, useEffect, useRef, useState } from "react";

interface ExpandableTextProps {
    text: string;
    maxLines?: number;
    id?: string; // For better accessibility
}

const ExpandableText = ({ text, maxLines = 3, id }: ExpandableTextProps) => {
    const [isExpanded, setIsExpanded] = useState(false);
    const [isClamped, setIsClamped] = useState(false);
    const [focusedByKeyboard, setFocusedByKeyboard] = useState(false);
    const textRef = useRef<HTMLDivElement>(null);
    const componentId =
        id || `expandable-text-${Math.random().toString(36).substring(2, 9)}`;

    const checkIfClamped = useCallback(() => {
        if (textRef.current && !isExpanded) {
            const element = textRef.current;
            setIsClamped(element.scrollHeight > element.clientHeight);
        }
    }, [isExpanded]);

    useEffect(() => {
        checkIfClamped();
    }, [text, maxLines, checkIfClamped]);

    useEffect(() => {
        const handleResize = () => {
            checkIfClamped();
        };

        window.addEventListener("resize", handleResize);

        return () => {
            window.removeEventListener("resize", handleResize);
        };
    }, [checkIfClamped]);

    const handleToggle = () => {
        if (isExpanded) {
            setIsExpanded(false);
            setTimeout(checkIfClamped, 0);
        } else {
            setIsExpanded(true);
        }
    };

    return (
        <div className="relative">
            <div
                ref={textRef}
                className={isExpanded ? "" : `line-clamp-${maxLines}`}
                id={`${componentId}-content`}
                aria-expanded={isExpanded}
            >
                {text}
            </div>
            {(isClamped || isExpanded) && (
                <button
                    type="button"
                    className={` ${
                        isExpanded
                            ? "mt-2 text-xs font-bold text-blue-600 hover:text-blue-800"
                            : "absolute bottom-0 right-0 bg-[linear-gradient(to_right,transparent_0px,white_48px)] pl-[54px] text-xs font-bold text-blue-600 hover:text-blue-800"
                    } ${focusedByKeyboard ? "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" : "focus:outline-none"} `}
                    onClick={handleToggle}
                    onMouseDown={() => setFocusedByKeyboard(false)}
                    onKeyDown={() => setFocusedByKeyboard(true)}
                    aria-controls={`${componentId}-content`}
                    aria-expanded={isExpanded}
                >
                    {isExpanded ? "Show Less" : "Show More"}
                </button>
            )}
        </div>
    );
};

To use it:

<ExpandableText text={"My very long text here."} maxLines={3} />

Feedback?

Email us at enquiries@kinsa.cc.