Clamp Checkbox labels to one line, add wrap prop

Labels no longer overflow their container: by default they are clamped
to a single line with an ellipsis and the full label is shown as the
wrapper's title on hover. Pass `wrap` to let the label flow onto
multiple lines. Adds the `twui-checkbox-label` class.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-11-05 18:04:03 +01:00
co-authored by Claude Opus 5.5
parent c45f44a441
commit 66c2f6850e
3 changed files with 41 additions and 7 deletions
+9
View File
@@ -147,6 +147,15 @@ export default function FormsPage() {
title="Beta features" title="Beta features"
label="Opt into experimental components." label="Opt into experimental components."
/> />
<Checkbox
title="Long label (clamped)"
label="This label is far too long to fit on a single line, so it is clamped to one line with an ellipsis - hover it to see the full text."
/>
<Checkbox
title="Long label (wrapped)"
label="This label is far too long to fit on a single line, but the wrap prop lets it flow onto as many lines as it needs."
wrap
/>
<Row className="gap-3"> <Row className="gap-3">
<Span size="small" variant="faded"> <Span size="small" variant="faded">
Notifications are {notifications ? "on" : "off"} Notifications are {notifications ? "on" : "off"}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@moduletrace/twui", "name": "@moduletrace/twui",
"version": "0.1.8", "version": "0.1.9",
"description": "A modular skeletal React component framework for Tailwind CSS v4", "description": "A modular skeletal React component framework for Tailwind CSS v4",
"author": "Benjamin Toby", "author": "Benjamin Toby",
"type": "module", "type": "module",
+31 -6
View File
@@ -39,6 +39,12 @@ export type CheckboxProps = Omit<
clickDispatchHandler?: (value: boolean) => void; clickDispatchHandler?: (value: boolean) => void;
info?: string | ReactNode; info?: string | ReactNode;
wrapperWrapperProps?: ComponentProps<typeof Stack>; wrapperWrapperProps?: ComponentProps<typeof Stack>;
/**
* Let the label wrap onto multiple lines. By default the label is
* clamped to a single line with an ellipsis, and the full text is
* shown as the wrapper's `title` on hover.
*/
wrap?: boolean;
}; };
/** /**
@@ -46,6 +52,7 @@ export type CheckboxProps = Omit<
* @className twui-checkbox * @className twui-checkbox
* @className twui-checkbox-checked * @className twui-checkbox-checked
* @className twui-checkbox-unchecked * @className twui-checkbox-unchecked
* @className twui-checkbox-label
*/ */
export default function Checkbox({ export default function Checkbox({
wrapperProps, wrapperProps,
@@ -63,6 +70,7 @@ export default function Checkbox({
noLabel, noLabel,
title, title,
clickDispatchHandler, clickDispatchHandler,
wrap,
...props ...props
}: CheckboxProps) { }: CheckboxProps) {
const finalSize = size || 20; const finalSize = size || 20;
@@ -72,6 +80,15 @@ export default function Checkbox({
); );
const finalTitle = title; const finalTitle = title;
const labelContent = label || finalTitle;
const hoverTitle =
wrapperProps?.title ??
(typeof labelContent == "string" || typeof labelContent == "number"
? String(labelContent)
: typeof finalTitle == "string"
? finalTitle
: undefined);
React.useEffect(() => { React.useEffect(() => {
if (typeof externalChecked == "undefined") return; if (typeof externalChecked == "undefined") return;
@@ -85,12 +102,17 @@ export default function Checkbox({
return ( return (
<Stack <Stack
{...wrapperWrapperProps} {...wrapperWrapperProps}
className={twMerge("gap-1.5", wrapperWrapperProps?.className)} className={twMerge(
"gap-1.5 min-w-0 max-w-full",
wrapperWrapperProps?.className,
)}
> >
<div <div
{...wrapperProps} {...wrapperProps}
title={hoverTitle}
className={twMerge( className={twMerge(
"flex items-start md:items-center gap-2 flex-wrap md:flex-nowrap", "flex gap-2 flex-nowrap min-w-0 max-w-full",
wrap ? "items-start" : "items-center",
readOnly ? "opacity-70 pointer-events-none" : "", readOnly ? "opacity-70 pointer-events-none" : "",
wrapperClassName, wrapperClassName,
wrapperProps?.className, wrapperProps?.className,
@@ -104,7 +126,7 @@ export default function Checkbox({
<div <div
{...props} {...props}
className={twMerge( className={twMerge(
"flex items-center justify-center p-[3px] rounded-default", "flex items-center justify-center p-[3px] rounded-default shrink-0",
checked checked
? "bg-primary twui-checkbox-checked text-white outline-slate-400" ? "bg-primary twui-checkbox-checked text-white outline-slate-400"
: "dark:outline-white/50 outline-2 -outline-offset-2 twui-checkbox-unchecked", : "dark:outline-white/50 outline-2 -outline-offset-2 twui-checkbox-unchecked",
@@ -121,15 +143,18 @@ export default function Checkbox({
{checked && <CheckMarkSVG />} {checked && <CheckMarkSVG />}
</div> </div>
{!noLabel && ( {!noLabel && (
<Stack className="gap-0.5"> <Stack className="gap-0.5 min-w-0 max-w-full">
<div <div
{...labelProps} {...labelProps}
className={twMerge( className={twMerge(
"select-none whitespace-normal md:whitespace-nowrap", "select-none max-w-full twui-checkbox-label",
wrap
? "whitespace-normal break-words"
: "line-clamp-1 break-all",
labelProps?.className, labelProps?.className,
)} )}
> >
{label || finalTitle} {labelContent}
</div> </div>
</Stack> </Stack>
)} )}