Skip to main content
astrocraft-ui/ components · 101

Show More

Free · MIT

Headless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.

Install

Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).

Unstyled — behavior only, you write the CSS

Install command
npx astrocraft-ui add utility/show-more

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add utility/show-more --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add utility/show-more --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add utility/show-more --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add utility/show-more --theme tailwind --bridge lumos

Live demo

ShowMore

The clamp is applied by the script, not the markup: with JavaScript off you get all of the text rather than a cut-off paragraph with no way to open it. The button only appears when the text actually overflows, which is why it has to measure — and it measures again when the column narrows. Resize the window and watch it appear and disappear.

The two libraries share a behavior layer and nothing else. They will drift in styling and must not drift in behavior, so four modules are kept byte-identical and a script diffs them; the rest is read as a reference diff rather than copied. That is also why a new anchored overlay extends _anchor.ts rather than the frozen _popover.ts: the freeze is what stops a behavior fix landing in one library and not the other, and it is worth more than the small duplication it costs.

Attribute surface

With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.

ComponentSlotsVariantsRuntime stateNative state
`ShowMore.astro``show-more` `show-more-content` `show-more-label-less` `show-more-label-more` `show-more-trigger`—`data-expanded``[aria-expanded]` `[hidden]`

Source

What the command copies — 2 files, alias-free so the copy lands standing alone. This is the whole component, shown without its comments; the files you install keep them, with the reasoning behind every rule.

src/components/ui/utility/show-more/ShowMore.astro
---
import "../../../../styles/structure.css";

import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"div"> & {
  lines?: number;
  expanded?: boolean;
  label?: string;
  lessLabel?: string;
};

const {
  lines = 3,
  expanded = false,
  label = "Show more",
  lessLabel = "Show less",
  class: className,
  ...rest
} = Astro.props;

const contentId = `show-more-${crypto.randomUUID().slice(0, 8)}`;
---

<div
  class={className}
  data-slot="show-more"
  data-lines={lines}
  data-expanded={expanded ? "true" : "false"}
  {...rest}
>
  <div id={contentId} data-slot="show-more-content"><slot /></div>
  <button
    type="button"
    hidden
    aria-expanded={expanded ? "true" : "false"}
    aria-controls={contentId}
    data-slot="show-more-trigger"
  >
    <span data-slot="show-more-label-more"><slot name="more">{label}</slot></span>
    <span data-slot="show-more-label-less"><slot name="less">{lessLabel}</slot></span>
  </button>
</div>

<script>
  import { onReadyOnce } from "../../_once";

  function wire(root: HTMLElement) {
    const content = root.querySelector<HTMLElement>('[data-slot="show-more-content"]');
    const button = root.querySelector<HTMLElement>('[data-slot="show-more-trigger"]');
    if (!content || !button) return;
    const lines = Number(root.dataset.lines) || 3;

    const clamp = (on: boolean) => {
      content.style.overflow = on ? "hidden" : "";
      content.style.maxHeight = on ? `${lines}lh` : "";
    };

    const measure = () => {
      if (root.dataset.expanded === "true") return;
      clamp(false);
      const full = content.scrollHeight;
      clamp(true);
      const overflows = full > content.clientHeight + 1;
      button.hidden = !overflows;
      if (!overflows) clamp(false);
    };

    button.addEventListener("click", () => {
      const open = root.dataset.expanded !== "true";
      root.dataset.expanded = String(open);
      button.setAttribute("aria-expanded", String(open));
      clamp(!open);
    });

    measure();

    let lastWidth = -1;
    new ResizeObserver(() => {
      const width = content.clientWidth;
      if (width === lastWidth) return;
      lastWidth = width;
      measure();
    }).observe(content);
  }

  onReadyOnce('[data-slot="show-more"]', wire);
</script>
src/components/ui/utility/show-more/index.ts
import ShowMore from "./ShowMore.astro";

export { ShowMore };
export default ShowMore;

What you get

The component source, copied into your project by npx astrocraft-ui add utility/show-more — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.