LineClamp

Folds long content to a set number of lines, with a button to show the rest.

On my first day at the new school I met a girl on the roof holding an old camera. She said it could photograph tomorrow. Every day after that we climbed the rusted ladder once class let out. She pressed the shutter and I kept the time. The tomorrows in her pictures were never wrong: spilt milk, a late train, a sudden rain. Then one evening something appeared in the viewfinder that should not have been there, and for the first time she did not press the shutter.
<script setup lang="ts">
  import { LineClamp } from '@hina-ui/vue'
</script>

<template>
  <LineClamp class="w-full max-w-md">
    On my first day at the new school I met a girl on the roof holding an old camera. She said it
    could photograph tomorrow. Every day after that we climbed the rusted ladder once class let out.
    She pressed the shutter and I kept the time. The tomorrows in her pictures were never wrong:
    spilt milk, a late train, a sudden rain. Then one evening something appeared in the viewfinder
    that should not have been there, and for the first time she did not press the shutter.
  </LineClamp>
</template>
vue

Usage

import { LineClamp } from '@hina-ui/vue'
ts

Put the content in the default slot. Anything past three lines is folded away, the folded edge fades out at the bottom and a “Show all” button appears below; once expanded the button reads “Show less”. Content that fits within the line count gets no button.

<LineClamp>{{ synopsis }}</LineClamp>
vue

The width comes from the container or from class. To mask spoilers use Spoiler; to open and close a whole region use Collapsible.

Examples

Line count

lines sets how many lines stay visible when folded, 3 by default. Fractions are rounded down and anything below 1 counts as 1.

Every autumn a secondhand bookseller sails north along the canal, selling a boatload of books to the towns on its banks. This year the boat has an uninvited passenger: a girl looking for a book that, by her own account, does not exist. She remembers every page of it but cannot give its title or its author. They strike a deal: she keeps his catalogue in order along the way, and he takes her to the library at the end of the canal. At every town where they moor, a reader recognises a passage she recites, and each tells a different ending.
Every autumn a secondhand bookseller sails north along the canal, selling a boatload of books to the towns on its banks. This year the boat has an uninvited passenger: a girl looking for a book that, by her own account, does not exist. She remembers every page of it but cannot give its title or its author. They strike a deal: she keeps his catalogue in order along the way, and he takes her to the library at the end of the canal. At every town where they moor, a reader recognises a passage she recites, and each tells a different ending.
<script setup lang="ts">
  import { LineClamp, Stack } from '@hina-ui/vue'

  const synopsis =
    'Every autumn a secondhand bookseller sails north along the canal, selling a boatload of books to the towns on its banks. This year the boat has an uninvited passenger: a girl looking for a book that, by her own account, does not exist. She remembers every page of it but cannot give its title or its author. They strike a deal: she keeps his catalogue in order along the way, and he takes her to the library at the end of the canal. At every town where they moor, a reader recognises a passage she recites, and each tells a different ending.'
</script>

<template>
  <Stack class="w-full max-w-md" gap="lg">
    <LineClamp :lines="2">{{ synopsis }}</LineClamp>
    <LineClamp :lines="4">{{ synopsis }}</LineClamp>
  </Stack>
</template>
vue

Block content

The default slot takes block content such as Text paragraphs or Prose. Lines are counted across paragraphs, at the line height of the text inside.

Volume one collects the first twelve chapters of the serial, from the first day at the new school to that year's culture festival. The dialogue was revised throughout for the book and three spreads were redrawn.

Two unpublished short stories close the volume. One tells where the old camera came from, in the words of the photography club's adviser. The other is a summer side story in which the two wait on the roof all night to photograph a meteor shower.

First printings include a postcard of the cover's line art. The digital edition has no extras, and its text matches the print edition.

<script setup lang="ts">
  import { LineClamp, Text } from '@hina-ui/vue'
</script>

<template>
  <LineClamp :lines="4" class="w-full max-w-md">
    <Text size="sm" tone="muted">
      Volume one collects the first twelve chapters of the serial, from the first day at the new
      school to that year's culture festival. The dialogue was revised throughout for the book and
      three spreads were redrawn.
    </Text>
    <Text size="sm" tone="muted">
      Two unpublished short stories close the volume. One tells where the old camera came from, in
      the words of the photography club's adviser. The other is a summer side story in which the two
      wait on the roof all night to photograph a meteor shower.
    </Text>
    <Text size="sm" tone="muted">
      First printings include a postcard of the cover's line art. The digital edition has no extras,
      and its text matches the print edition.
    </Text>
  </LineClamp>
</template>
vue

Controlled

v-model:expanded binds the expanded state; other controls on the page can expand or fold the same content.

Only one observer is left at the weather station on the summit. At six every morning he copies the night's wind speed and pressure into the log, then radios them down to the town below. The first snow of winter closes the mountain road, and the voice at the other end of the radio is replaced by a stranger's, claiming to speak from the same station thirty years ago.
<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, LineClamp, Stack } from '@hina-ui/vue'

  const expanded = ref(false)
</script>

<template>
  <Stack class="w-full max-w-md">
    <Button
      variant="outline"
      tone="neutral"
      size="sm"
      class="self-start"
      @click="expanded = !expanded"
    >
      {{ expanded ? 'Fold the synopsis' : 'Expand the synopsis' }}
    </Button>
    <LineClamp v-model:expanded="expanded" :lines="2">
      Only one observer is left at the weather station on the summit. At six every morning he copies
      the night's wind speed and pressure into the log, then radios them down to the town below. The
      first snow of winter closes the mountain road, and the voice at the other end of the radio is
      replaced by a stranger's, claiming to speak from the same station thirty years ago.
    </LineClamp>
  </Stack>
</template>
vue

Custom labels

The button text defaults to lineClamp.expand and lineClamp.collapse of the UI locale. expand-label and collapse-label replace it for a single instance.

It took me until volume three to notice that the offhand weather notes in the first two were all groundwork. The author never spells it out; the same radio call simply repeats three times, in three different seasons. When I closed the book I went back through volume one, and this time every page read like a different book. Recommended for readers who like a story that takes its time.
<script setup lang="ts">
  import { LineClamp } from '@hina-ui/vue'
</script>

<template>
  <LineClamp
    expand-label="Read the full review"
    collapse-label="Fold the review"
    class="w-full max-w-md"
  >
    It took me until volume three to notice that the offhand weather notes in the first two were all
    groundwork. The author never spells it out; the same radio call simply repeats three times, in
    three different seasons. When I closed the book I went back through volume one, and this time
    every page read like a different book. Recommended for readers who like a story that takes its
    time.
  </LineClamp>
</template>
vue

Behaviour

  • The folded height comes from CSS line clamping. The server always outputs the button, and CSS shows the button and the fade according to whether the content overflows, so the first frame is already final and layout and appearance are the same before and after activation.
  • Browsers without scroll-driven animations (animation-timeline) treat the content as overflowing before activation: the button shows, the fade does not, and the last line ends in an ellipsis; activation then corrects it from the measurement.
  • When expanded starts as true, “Show less” shows before activation; after activation the button is removed if the content does not exceed the line count.
  • After activation the component measures again when the container width, the content or lines changes, and once more when fonts finish loading. The button shows or hides with the result, including “Show less” in the expanded state.
  • Expanding and folding animate the height, and the fade at the bottom fades out and in with it; toggling again mid-transition turns back from the current height. With reduced motion enabled in the system, no transition plays.
  • The fade is a mask and does not depend on the background colour; it covers at most 1.5 lines and is shortened in proportion for small line counts. The mask is removed once the expanded state settles.
  • After folding through the button, the component scrolls back into view when the transition ends.
  • Folding only clips what is shown; the full content stays in the DOM.

Accessibility

  • The button is a native button: it takes focus and responds to both Enter and Space.
  • aria-expanded reflects whether the content is expanded, and aria-controls points at the content region.
  • The clipped text is not hidden from assistive technology.
  • When the content fits within the line count, the button is not shown and is left out of the focus order and the accessibility tree.

API

Props

Prop
Type
Default
Description
lines
number
3
Lines kept when folded; rounded down, at least 1
expanded
boolean
false
Whether it is expanded, takes v-model:expanded
expandLabel
string
From the locale
Text of the button that expands
collapseLabel
string
From the locale
Text of the button that folds
class
string
—
Classes appended to the root

Attributes it does not declare land on the root element.

Slots

Slot
Description
default
The folded content

Events

Event
Payload
Description
update:expanded
expanded: boolean
The expanded state changed