Text
Text
Body copy, descriptions and small print in three sizes and five tones, with an optional one-line truncate.
- Built on
<p>text-wrap: prettytext-overflow- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 0.9 kB
- Dependencies
- React
Two-step sign-in
Ask for a code from your authenticator app each time you sign in on a new device.
On since 3 March
Try its options
Loading its options.
Code
import { Text } from "@/components/equip/text";
export default function Demo() {
return (
<div style={{ display: "grid", gap: 6, width: 224 }}>
<Text weight="strong">Two-step sign-in</Text>
<Text tone="quiet">Ask for a code from your authenticator app each time you sign in on a new device.</Text>
<Text size="s" tone="ok">On since 3 March</Text>
</div>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:6px;width:224px">
<p class="eq-text" data-size="m" data-tone="default" data-weight="strong">Two-step sign-in</p>
<p class="eq-text" data-size="m" data-tone="quiet" data-weight="regular">Ask for a code from your authenticator app each time you sign in on a new device.</p>
<p class="eq-text" data-size="s" data-tone="ok" data-weight="regular">On since 3 March</p>
</div>Examples
Tones
Danger and ok are colour, so the words carry the meaning too: write Payment failed, not just red text.
Your export is ready to download.
Exports include every project you can open.
Last exported 2 hours ago by Priya Raman.
Payment failed. Update the card ending 4242 to keep your plan.
Domain verified. Mail from acme.dev will now be signed.
Code
import { Text } from "@/components/equip/text";
export default function Example() {
return (
<div style={{ display: "grid", gap: 8 }}>
<Text>Your export is ready to download.</Text>
<Text tone="quiet">Exports include every project you can open.</Text>
<Text tone="faint">Last exported 2 hours ago by Priya Raman.</Text>
<Text tone="danger">Payment failed. Update the card ending 4242 to keep your plan.</Text>
<Text tone="ok">Domain verified. Mail from acme.dev will now be signed.</Text>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px">
<p class="eq-text" data-size="m" data-tone="default" data-weight="regular">Your export is ready to download.</p>
<p class="eq-text" data-size="m" data-tone="quiet" data-weight="regular">Exports include every project you can open.</p>
<p class="eq-text" data-size="m" data-tone="faint" data-weight="regular">Last exported 2 hours ago by Priya Raman.</p>
<p class="eq-text" data-size="m" data-tone="danger" data-weight="regular">Payment failed. Update the card ending 4242 to keep your plan.</p>
<p class="eq-text" data-size="m" data-tone="ok" data-weight="regular">Domain verified. Mail from acme.dev will now be signed.</p>
</div>Sizes and weight
Preview deployments give every pull request its own address, so reviewers can try a change before it ships.
Preview deployments give every pull request its own address, so reviewers can try a change before it ships.
Preview deployments give every pull request its own address, so reviewers can try a change before it ships.
Previews are deleted 30 days after the branch is merged.
Code
import { Text } from "@/components/equip/text";
export default function Example() {
return (
<div style={{ display: "grid", gap: 12, maxWidth: 480 }}>
<Text size="l">Preview deployments give every pull request its own address, so reviewers can try a change before it ships.</Text>
<Text size="m">Preview deployments give every pull request its own address, so reviewers can try a change before it ships.</Text>
<Text size="s">Preview deployments give every pull request its own address, so reviewers can try a change before it ships.</Text>
<Text size="m" weight="strong">Previews are deleted 30 days after the branch is merged.</Text>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:12px;max-width:480px">
<p class="eq-text" data-size="l" data-tone="default" data-weight="regular">Preview deployments give every pull request its own address, so reviewers can try a change before it ships.</p>
<p class="eq-text" data-size="m" data-tone="default" data-weight="regular">Preview deployments give every pull request its own address, so reviewers can try a change before it ships.</p>
<p class="eq-text" data-size="s" data-tone="default" data-weight="regular">Preview deployments give every pull request its own address, so reviewers can try a change before it ships.</p>
<p class="eq-text" data-size="m" data-tone="default" data-weight="strong">Previews are deleted 30 days after the branch is merged.</p>
</div>Settings description
Email digest
A summary of comments and mentions, sent every Monday at 9:00.
Delete workspace
Removes every project, file and member. This cannot be undone.
Code
import { Text } from "@/components/equip/text";
import { Button } from "@/components/equip/button";
export default function Example() {
const row = {
display: "flex",
gap: 16,
alignItems: "center",
justifyContent: "space-between",
paddingBlock: 12,
borderBottom: "1px solid var(--eq-line)",
};
return (
<div style={{ maxWidth: 520 }}>
<div style={row}>
<div style={{ display: "grid", gap: 2 }}>
<Text weight="strong">Email digest</Text>
<Text size="s" tone="quiet">A summary of comments and mentions, sent every Monday at 9:00.</Text>
</div>
<Button variant="outline" size="s">Change</Button>
</div>
<div style={row}>
<div style={{ display: "grid", gap: 2 }}>
<Text weight="strong">Delete workspace</Text>
<Text size="s" tone="quiet">Removes every project, file and member. This cannot be undone.</Text>
</div>
<Button variant="danger" size="s">Delete</Button>
</div>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="max-width:520px"><div style="display:flex;gap:16px;align-items:center;justify-content:space-between;padding-block:12px;border-bottom:1px solid var(--eq-line)"><div style="display:grid;gap:2px"><p class="eq-text" data-size="m" data-tone="default" data-weight="strong">Email digest</p><p class="eq-text" data-size="s" data-tone="quiet" data-weight="regular">A summary of comments and mentions, sent every Monday at 9:00.</p></div><button type="button" class="eq-button" data-variant="outline" data-size="s"><span class="eq-button-label">Change</span></button></div><div style="display:flex;gap:16px;align-items:center;justify-content:space-between;padding-block:12px;border-bottom:1px solid var(--eq-line)"><div style="display:grid;gap:2px"><p class="eq-text" data-size="m" data-tone="default" data-weight="strong">Delete workspace</p><p class="eq-text" data-size="s" data-tone="quiet" data-weight="regular">Removes every project, file and member. This cannot be undone.</p></div><button type="button" class="eq-button" data-variant="danger" data-size="s"><span class="eq-button-label">Delete</span></button></div></div>Truncate
Truncated text keeps the whole string in the DOM; the title attribute lets a mouse user read it too.
quarterly-revenue-by-region-final-v3-approved.xlsx
Shared by Tomás Ortega
Code
import { Text } from "@/components/equip/text";
export default function Example() {
const name = "quarterly-revenue-by-region-final-v3-approved.xlsx";
return (
<div style={{ display: "grid", gap: 2, width: 220, padding: 12, border: "1px solid var(--eq-line)", borderRadius: "var(--eq-radius-m)" }}>
<Text weight="strong" truncate title={name}>
{name}
</Text>
<Text size="s" tone="faint">
Shared by Tomás Ortega
</Text>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:2px;width:220px;padding:12px;border:1px solid var(--eq-line);border-radius:var(--eq-radius-m)">
<p title="quarterly-revenue-by-region-final-v3-approved.xlsx" class="eq-text" data-size="m" data-tone="default" data-weight="strong" data-truncate="true">quarterly-revenue-by-region-final-v3-approved.xlsx</p>
<p class="eq-text" data-size="s" data-tone="faint" data-weight="regular">Shared by Tomás Ortega</p>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
as | "p" | "span" | "div" | "p" | The element. A paragraph by default; span inside a line or a flex row of inline items, div to hold other blocks. |
size | "s" | "m" | "l" | "m" | Reads --eq-text-s, --eq-text-m or --eq-text-l, with a line height that opens up as the size grows. |
tone | "default" | "quiet" | "faint" | "danger" | "ok" | "default" | Default is --eq-ink, quiet --eq-ink-2 for descriptions, faint --eq-ink-3 for metadata, danger and ok their signal colours. |
weight | "regular" | "strong" | "regular" | Strong for a label or a lead-in line. |
truncate | boolean | false | Keeps the text to one line and ends it with an ellipsis. Pass title so the full text is reachable by pointer. |
...rest | HTMLAttributes<HTMLElement> | none | Every global attribute, placed on the p, span or div. |
Anatomy
| root | .eq-text | The p, span or div, with data-size, data-tone, data-weight and data-truncate. |
|---|
Notes
- Danger and ok are colour, and colour alone does not reach everyone, so the words must say what happened.
- Faint (--eq-ink-3) meets 4.5:1 on the base surfaces at the default tokens, but keep it for metadata, not for text people must read to act.
- Truncated text stays whole in the DOM, so screen readers, copy and find in page get all of it; only the view is cut.
- There is no max-width: set the measure on the layout, around 60 to 75 characters for long copy.
- Lines are evened out with text-wrap: pretty, which avoids a lone last word; engines without it wrap normally.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/text.tsx37 lines
import type { HTMLAttributes } from "react";
import "./text.css";
export type TextProps = HTMLAttributes<HTMLElement> & {
/** The element. A paragraph by default; span for text inside a line, div to wrap other blocks. */
as?: "p" | "span" | "div";
size?: "s" | "m" | "l";
/** Quiet and faint step down the ink; danger and ok carry a meaning the words must also say. */
tone?: "default" | "quiet" | "faint" | "danger" | "ok";
weight?: "regular" | "strong";
/** Keeps the text to one line and ends it with an ellipsis. Pass title so the full text is reachable. */
truncate?: boolean;
};
export function Text({
as: Tag = "p",
size = "m",
tone = "default",
weight = "regular",
truncate = false,
className,
children,
...rest
}: TextProps) {
return (
<Tag
{...rest}
className={["eq-text", className].filter(Boolean).join(" ")}
data-size={size}
data-tone={tone}
data-weight={weight}
data-truncate={truncate || undefined}
>
{children}
</Tag>
);
}components/equip/text.css54 lines
@layer equip {
.eq-text {
--_size: var(--eq-text-m);
--_leading: 1.55;
margin: 0;
color: var(--eq-ink);
font: var(--eq-weight) var(--_size) / var(--_leading) var(--eq-font);
text-wrap: pretty;
}
.eq-text[data-size="s"] {
--_size: var(--eq-text-s);
--_leading: 1.5;
}
.eq-text[data-size="l"] {
--_size: var(--eq-text-l);
--_leading: 1.6;
}
.eq-text[data-tone="quiet"] {
color: var(--eq-ink-2);
}
.eq-text[data-tone="faint"] {
color: var(--eq-ink-3);
}
.eq-text[data-tone="danger"] {
color: var(--eq-danger);
}
.eq-text[data-tone="ok"] {
color: var(--eq-ok);
}
.eq-text[data-weight="strong"] {
font-weight: var(--eq-weight-strong);
}
.eq-text[data-truncate] {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* A span has no width to cut against until it is a box of its own. */
span.eq-text[data-truncate] {
display: inline-block;
vertical-align: bottom;
}
}