PlFooter
The sheet at the end of a page. A real <footer>, which is what makes it the site's own information rather than more of the article, and it has no slots, because a footer's content is nobody's to guess.
import { PlFooter } from 'plass-ui';
<PlFooter>
<p>© 2026 Acme</p>
</PlFooter>;import 'package:plass_ui/plass_ui.dart';
PlFooter(child: const Text('© 2026 Acme'));Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | What the sheet is made of. The bar is never dyed — what is on it arrives with colours of its own |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The gutter and the air above and below the content. As on PlBox, size here is the size of the sheet |
| 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 | 0 | Drop shadow depth. 0 means no shadow at all |
| positionshared | 'static' | 'sticky' | 'fixed' | 'static' | How the bar sits in the page's scroll. static is the default and the opposite of PlHeader's: a footer is the end of the document, reached by scrolling to it |
| divider | boolean | true | Draws a hairline along the top edge. On by default: a footer is the one sheet with content directly above it and nothing below, so the line is what says the document ended |
| maxWidth | PlassResponsive<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none' | number | string> | 'none' | Holds the content to a measure and centres it while the sheet still spans the window. The same ladder PlContainer's maxWidth uses |
| padded | boolean | true | The gutter and the air above and below |
| label | string | — | The name the bar is announced by. Worth writing when a page has more than one <footer> in it |
| render | useRender.RenderProp | — | Renders something other than a <footer>. Rarely what you want: at the top level of a document that tag is the contentinfo landmark |
| children | ReactNode | — | Everything in it. A footer's content is columns of links, a copyright line, a logo — all of it the caller's, which is why there are no slots |
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | PlassVariant | PlassVariant.glass | What the sheet is made of. The bar is never dyed — what is on it arrives with colours of its own |
| sizeshared | PlassSize | PlassSize.md | The gutter and the air above and below the content. As on PlBox, size here is the size of the sheet |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding only — never the height, never the type scale |
| elevationshared | int | 0 | Drop shadow depth. 0 means no shadow at all |
| divider | bool | true | Draws a hairline along the top edge. On by default: a footer is the one sheet with content directly above it and nothing below, so the line is what says the document ended |
| maxWidth | PlassResponsive<PlContainerWidth?>? | null | How wide the content is allowed to get. PlContainerWidth.rung(PlassSize) is a rung of the ladder and PlContainerWidth.pixels(double) is an exact width. null means no limit, including as an entry |
| padded | bool | true | The gutter and the air above and below |
| semanticLabel | String? | — | The name a screen reader gives the region. Worth writing when a screen has two footers in it |
| child | Widget? | — | Everything in it. A footer's content is columns of links, a copyright line, a logo — all of it the caller's, which is why there are no slots |
Every native <footer> attribute passes straight through. color and title are excluded because both are Plass props here.
What the shared axes (variant size color density elevation) mean across the library is in prop conventions.
No slots
PlHeader has three, because a header's regions are a fixed arrangement (brand, middle, actions) worth writing once so that two pages of the same site cannot drift.
A footer is not like that. It is four columns of links on one site, a copyright line on the next, and a language switcher and an address on the third. So this component decides the sheet and nothing else: the surface, the gutter, the measure, the hairline that says the document ended, and whether the bar stays in reach.
Examples
position
static is the default, and it is the opposite of a header's. A footer is the end of the document and is reached by scrolling to it.
sticky and fixed are for the other kind of bar at the bottom of a screen, a form's save row, a cookie notice, a bulk-action strip. Inside a PlPageLayout the height a fixed one takes out of the flow is reserved, so it does not sit on top of the last paragraph.
import { PlButton, PlFooter } from 'plass-ui';
const rows = Array.from({ length: 12 }, (_, index) => `Field ${index + 1}`);
export default function FooterPosition() {
return (
<div className="relative h-64 w-full overflow-y-auto rounded-(--plass-radius-md)">
<ul className="flex flex-col text-sm">
{rows.map((row) => (
<li key={row} className="border-b px-5 py-3 [border-color:var(--plass-divider)]">
{row}
</li>
))}
</ul>
<PlFooter position="sticky" size="sm" density="compact">
<div className="flex items-center justify-end gap-2">
<PlButton size="sm" variant="ghost" color="secondary">
Cancel
</PlButton>
<PlButton size="sm">Save</PlButton>
</div>
</PlFooter>
</div>
);
}variant
The three materials, read the way a container reads them. The sheet is never dyed: what is on a footer is links and text, and they arrive with colours of their own.
divider is on by default and rules the top edge, the one that faces content. A footer is the one sheet on a page with something directly above it and nothing below, so that line is the whole of what says the document ended.
import { PlFooter, type PlassVariant } from 'plass-ui';
export default function FooterVariants() {
return (
<div className="flex w-full flex-col gap-4">
{(['solid', 'glass', 'ghost'] as PlassVariant[]).map((variant) => (
<PlFooter key={variant} size="sm" variant={variant}>
<span className="text-sm">The sheet is {variant}.</span>
</PlFooter>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class FooterVariants extends StatelessWidget {
const FooterVariants({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 480,
child: Column(
spacing: 16,
children: <Widget>[
for (final PlassVariant variant in PlassVariant.values)
PlFooter(
size: PlassSize.sm,
variant: variant,
child: Text('The sheet is ${variant.name}.'),
),
],
),
);
}
}size
size is the size of the sheet: its gutter and the air above and below whatever is in it. Nothing here is a height, a footer is as tall as its content, and nothing here touches the type scale, which the content brings with it.
density moves the padding and nothing else.
import { PlFooter, type PlassSize } from 'plass-ui';
export default function FooterSizes() {
return (
<div className="flex w-full flex-col gap-4">
{(['xs', 'sm', 'md', 'lg', 'xl'] as PlassSize[]).map((size) => (
<PlFooter key={size} size={size}>
<span className="text-sm">© 2026 Acme — {size}</span>
</PlFooter>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class FooterSizes extends StatelessWidget {
const FooterSizes({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 480,
child: Column(
spacing: 16,
children: <Widget>[
for (final PlassSize size in PlassSize.values)
PlFooter(size: size, child: Text('© 2026 Acme — ${size.name}')),
],
),
);
}
}maxWidth
Holds the content to a measure and centres it while the sheet still spans the window, on the same rem ladder PlContainer's maxWidth uses, so the last line of the page and the first line of the footer sit on one edge.
The same ladder and the same type a PlContainer takes, and one implementation behind all three, a bar whose measure did not line up with the container under it is the defect that prevents. It is responsive and takes any CSS length with it.It is responsive, and takes an exact width as well as a rung.
import { PlContainer, PlFooter } from 'plass-ui';
export default function FooterMeasure() {
return (
<div className="w-full">
<PlContainer size="sm" maxWidth="sm" className="py-4 text-sm">
The page stops at 40rem, and so does the line under it — the sheet still reaches both edges
of the frame.
</PlContainer>
<PlFooter size="sm" maxWidth="sm">
<span className="text-xs text-(--plass-muted-fg)">© 2026 Acme</span>
</PlFooter>
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class FooterMeasure extends StatelessWidget {
const FooterMeasure({super.key});
@override
Widget build(BuildContext context) {
final PlassTokens tokens = PlassTheme.of(context);
return SizedBox(
width: 560,
child: Column(
children: <Widget>[
const PlContainer(
size: PlassSize.sm,
maxWidth: PlassResponsive<PlContainerWidth?>(PlContainerWidth.rung(PlassSize.xs)),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(
'The page stops at the measure, and so does the line under it — the sheet still '
'reaches both edges of the frame.',
),
),
),
PlFooter(
size: PlassSize.sm,
maxWidth: PlassResponsive<PlContainerWidth?>(PlContainerWidth.rung(PlassSize.xs)),
child: Text('© 2026 Acme', style: TextStyle(fontSize: 12, color: tokens.mutedFg)),
),
],
),
);
}
}Differences from the React build
| React | Flutter | Why |
|---|---|---|
position | — | A fixed or sticky element has to span something. A widget goes exactly where the screen puts it, and a save row that has to stay in reach belongs in the screen's own layout. |
| registering with the layout | — | A Column has already left the band exactly what the footer did not take; there is no height to reserve. |
maxWidth: 'none' | maxWidth: null | Dart's way of saying "no measure was named". |
<footer>, the contentinfo landmark | SemanticsRole.contentInfo | The same landmark under the framework's own name, and here it is claimed always, rather than depending on where in the document the tag happens to sit. |
label | semanticLabel | Flutter's name. |
render | — | There is no tag to swap. |
className, style, native attributes | — | There is no class list and no style attribute to pass through. |
Accessibility
- It renders a real
<footer>. At the top level of a document that is thecontentinfolandmark, which is what a screen reader's landmark list and a reader mode read. labelnames the bar. Worth writing when a page has two of them, an article's own footer and the site's, because the landmark list otherwise offers "contentinfo" twice.- A footer inside an
<article>or a<section>is notcontentinfo; the browser only promotes the tag at the top level of the document. That is the tag's own rule, not this component's. - Columns of links belong in a
<nav>with a name of their own, put inside the footer. The footer names the region; the<nav>names the list.
- It claims
SemanticsRole.contentInfo, the same landmark the<footer>tag carries on the other side, and it claims it unconditionally, because a widget has no "top level of the document" to be promoted at. semanticLabelnames the region. Worth writing when a screen has two of them, because a landmark list that says "contentInfo" twice has told the reader which is which not at all. Flutter says so out loud: a duplicated landmark with no label is an error.- Columns of links belong in a
PlSidebaror a named region of their own inside the footer. The footer names the region; what is in it names itself.