Show More
Free · MITHeadless, 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
npx astrocraft-ui add utility/show-morePlain-CSS theme — no build step
npx astrocraft-ui add utility/show-more --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add utility/show-more --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add utility/show-more --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add utility/show-more --theme tailwind --bridge lumosLive 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.
| Component | Slots | Variants | Runtime state | Native 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.
---
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>
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.