PlIconButton
A round button with a glyph in it and nothing else. Everything about it is a PlButton except the shape and the one prop that is required. The words the drawing does not say.
import { PlIconButton } from 'plass-ui';
<PlIconButton icon={<TrashIcon />} label="Delete" variant="glass" color="danger" />;import 'package:plass_ui/plass_ui.dart';
PlIconButton(
icon: const Icon(Icons.delete_outline),
label: 'Delete',
variant: PlassVariant.glass,
color: PlassColor.danger,
onPressed: remove,
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'glass' | 'ghost' | 'solid' | What the surface is made of: tinted glass, a clear sheet, or nothing |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The disc's diameter and the glyph inside it — the same ladder as PlButton |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 1 | Drop shadow depth. 0 means no shadow at all |
| icon * | ReactNode | — | The glyph. Passed bare it is sized in em against the button |
| label * | string | — | What the button does, in words. It becomes the accessible name and is never drawn |
| loading | boolean | false | Shows a spinner in place of the glyph and stops the button activating, while keeping it focusable |
| readOnly | boolean | false | Keeps its colour, goes flat and drains most of its saturation — not dimmed |
| disabled | boolean | false | Unavailable. Loses its light and its shadow, and leaves the focus order |
| render | RenderProp | — | Renders something other than a <button> (<a href="…" />) |
| Prop | Type | Default | Description |
|---|---|---|---|
| icon * | Widget | — | The glyph. Passed bare it is sized in em against the button |
| label * | String | — | What the button does, in words. It becomes the accessible name and is never drawn |
| onPressed | VoidCallback? | — | Called when the button is activated. Leaving it null disables the button |
| onLongPress | VoidCallback? | — | Called on a long press |
| variantshared | PlassVariant? | PlassVariant.solid | What the surface is made of: tinted glass, a clear sheet, or nothing |
| sizeshared | PlassSize? | PlassSize.md | The disc's diameter and the glyph inside it — the same ladder as PlButton |
| colorshared | PlassColor? | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| elevationshared | int? | 1 | Drop shadow depth. 0 means no shadow at all |
| loading | bool | false | Shows a spinner in place of the glyph and stops the button activating, while keeping it focusable |
| readOnly | bool | false | Keeps its colour, goes flat and drains most of its saturation — not dimmed |
| disabled | bool? | false | Unavailable. Loses its light and its shadow, and leaves the focus order |
| focusNode · autofocus | FocusNode? · bool | — | Drive focus from outside, or take it on insertion |
Every prop PlButton takes passes through untouched except children, startIcon and endIcon, which the glyph has taken over. Every native <button> attribute passes through as well.
Every parameter PlButton takes passes through except child, startIcon, endIcon and fullWidth. The glyph has taken the first three, and a disc that stretches is not a disc. There is no density either: it changes horizontal padding, and an icon-only button has none.
What the shared axes (variant size color density elevation) mean across the library is in prop conventions.
Examples
label
Required, and the one prop here that is.
A button whose whole label is a drawing has no accessible name at all, and "an icon button with no aria-labelsemantic label" is the single most common accessibility defect a component library ships. Making it required is the only fix that survives review. A lint rule is something a project has to install and a default of '' is something nobody notices.
It is never drawn. What a reader sees is the glyph; what everything else reads is the sentence.
The shape
A PlButton with an icon and no label already goes square, same height, same width, the house fillet cut off it. This is the other shape: a disc.
That disc is a deliberate exception to the radius rule, which holds every corner well short of the 50% that would make a control a pill. The rule is about labelled controls: the flat run along the top and bottom edge is where a line of text sits, and a glyph has no line of text. A circle with a single mark centred in it reads as a punched token rather than a moulded key.
import { PlIconButton } from 'plass-ui';
const Plus = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 5v14M5 12h14" />
</svg>
);
export default function IconButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-3">
{(['solid', 'glass', 'ghost'] as const).map((variant) => (
<PlIconButton key={variant} variant={variant} icon={<Plus />} label={`Add (${variant})`} />
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
import 'package:plass_ui_example/demos/glyphs.dart';
class IconButtonVariants extends StatelessWidget {
const IconButtonVariants({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
for (final PlassVariant variant in PlassVariant.values)
PlIconButton(
variant: variant,
icon: const PlusGlyph(),
label: 'Add (${variant.name})',
onPressed: () {},
),
],
);
}
}size
The same height ladder as PlButton, so a disc and a labelled button on one row keep their baseline. The glyph inside is sized in em against the button rather than off the standalone-icon ladder, which is what keeps it in proportion at every step.
import { PlButton, PlIconButton } from 'plass-ui';
const Plus = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 5v14M5 12h14" />
</svg>
);
export default function IconButtonSizes() {
return (
<div className="flex flex-col gap-3">
{(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((size) => (
<div key={size} className="flex items-center gap-3">
<PlIconButton size={size} icon={<Plus />} label={`Add (${size})`} />
<PlButton size={size} variant="glass">
{size}
</PlButton>
</div>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
import 'package:plass_ui_example/demos/glyphs.dart';
class IconButtonSizes extends StatelessWidget {
const IconButtonSizes({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: <Widget>[
for (final PlassSize size in PlassSize.values)
Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: <Widget>[
PlIconButton(
size: size,
icon: const PlusGlyph(),
label: 'Add (${size.name})',
onPressed: () {},
),
PlButton(
size: size,
variant: PlassVariant.glass,
onPressed: () {},
child: Text(size.name),
),
],
),
],
);
}
}color
import { PlIconButton } from 'plass-ui';
const Star = () => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="m12 3 2.6 5.6 6 .8-4.4 4.2 1.1 6.1L12 16.8 6.7 19.7l1.1-6.1L3.4 9.4l6-.8z" />
</svg>
);
export default function IconButtonColors() {
return (
<div className="flex flex-wrap items-center gap-3">
{(['primary', 'secondary', 'success', 'warning', 'danger', 'info'] as const).map((color) => (
<PlIconButton key={color} color={color} icon={<Star />} label={color} />
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
import 'package:plass_ui_example/demos/glyphs.dart';
class IconButtonColors extends StatelessWidget {
const IconButtonColors({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
for (final PlassColor color in PlassColor.values)
PlIconButton(color: color, icon: const StarGlyph(), label: color.name, onPressed: () {}),
],
);
}
}loading, readOnly and disabled
All three are PlButton's, unchanged. loading puts a spinner where the glyph was and stops the button firing while leaving it focusable; readOnly keeps the colour and drains the saturation; disabled takes the light out and leaves the focus order.
import { PlIconButton } from 'plass-ui';
const Save = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 4h11l3 3v13H5zM8 4v6h8V4M8 20v-6h8v6" />
</svg>
);
export default function IconButtonStates() {
return (
<div className="flex flex-wrap items-center gap-3">
<PlIconButton icon={<Save />} label="Save" />
<PlIconButton icon={<Save />} label="Saving" loading />
<PlIconButton icon={<Save />} label="Saved" readOnly />
<PlIconButton icon={<Save />} label="Unavailable" disabled />
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
import 'package:plass_ui_example/demos/glyphs.dart';
class IconButtonStates extends StatelessWidget {
const IconButtonStates({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
PlIconButton(icon: const SaveGlyph(), label: 'Save', onPressed: () {}),
PlIconButton(icon: const SaveGlyph(), label: 'Saving', loading: true, onPressed: () {}),
PlIconButton(icon: const SaveGlyph(), label: 'Saved', readOnly: true, onPressed: () {}),
PlIconButton(
icon: const SaveGlyph(),
label: 'Unavailable',
disabled: true,
onPressed: () {},
),
],
);
}
}Accessibility
labelis the accessible name and it is required. Nothing else here can supply one.- The glyph is decorative. It is inside a control that is already named, so a second name from the drawing would be the name read twice.
- Everything else is
PlButton's: the focus ring, the keyboard activation,aria-busywhile loading, and dropping out of the focus order only when disabled.
- The disc is still a real
<button>.render={<a href="…" />}makes it a real link instead, announced as one and followed by a crawler.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
onClick | onPressed | Flutter's name, and leaving it out is how a button is disabled. |
render | — | There is no element to swap and no link semantics to claim. |
density, fullWidth | — | Density changes horizontal padding, which an icon-only button has none of; a disc that stretches is not a disc. |
an inline style for the radius | PlButton.borderRadius | Flutter has no inline style, so PlButton carries one escape hatch and this is the widget it exists for. |
className, style | — | There is no class list and no style attribute to pass through. |
The radius is half the control height rather than a number large enough to be clamped: the paint scales a radius that is too big for its box, and a disc scaled that way stops being one at the ends.