WordPress 中的虛假功能暗示:介面承諾的操作未必能實現
False Affordance in WordPress (4wp.dev Methodologies): When UI Promises What Code Doesn't Deliver
作者以自己作品集上無效的「Show more」按鈕為例,解釋 WordPress 中的虛假功能暗示,並示範在經典 PHP 模板及區塊主題(FSE)中修正。經典模板按隱藏項目數量決定是否輸出按鈕;FSE 則由 JavaScript 根據篩選後的項目和可見數量更新按鈕,並預設將其隱藏。
A "Show more" button on my own portfolio showed nothing, and the console stayed clean. This post is the write-up: what a false affordance is, why it slips into WordPress themes and plugins, and how to fix it in a classic PHP template and in a block theme (FSE) with a Query Loop + JS. It is part of the Methodologies series on 4wp.dev.
Related on 4wp.dev: Full Site Editing and theme.json · Component-Driven Development · Atomic Design · TDD
What a false affordance is
A false affordance is an interface element that suggests an action is possible, but the action does not actually happen. A button that does nothing. A toggle with no effect. A setting that is saved and never applied.
The term comes from Don Norman's The Design of Everyday Things: a door handle you are supposed to push is a false affordance, because the handle invites you to pull.
It sounds like a design topic, but in WordPress it is mostly an engineering one. The design shows the promise. The code is what breaks it. And it is not limited to the UI: the same thing happens in APIs, hooks and function signatures.
A Real Case: The "Show More" That Shows Nothing
I will start with my own site, because it is the most honest example I have.
On the home page of anatoliy.dovgun.com there is a Projects block: a list of projects, and under it two controls, "Show more" and "All projects →".
Before:
- All projects were already rendered on the page.
- "Show more" was still there.
- Clicking it did one thing: the button hid itself. Nothing new appeared.
- The browser console was clean. No errors, no warnings.
That last point is what makes this kind of bug dangerous. Nothing failed, so nothing reported a failure. The button simply promised an action that did not exist. It is a silent bug, and it lived on my own portfolio.
Why it happened
The block came from the prototype / UI kit as one piece: "list + Show more button". The limit (how many items are visible before "Show more") was taken from the prototype, not from the data. When the static prototype became a real query over real projects, the list changed, but the button stayed exactly as it was drawn.
So the button rendered always, regardless of the data. Nobody asked the one question that matters for this control: are there any hidden items right now?
In other words, nobody clicked through every state. The designed state ("more projects than the limit") worked in the head of whoever drew it. The real state ("all projects already visible") was never clicked. A curious QA, or a developer with open eyes, would have clicked it in the first five minutes.
The Rule
Render the control only when the action is possible.
The rule is the same everywhere. Where you apply it depends on who renders the list.
Scenario A: classic PHP template
In a classic theme or a server-rendered template, PHP renders the list and knows the total. So PHP decides. If there are no hidden items, the button is not in the HTML at all:
<?php if ( $total > $visible_limit ) : ?>
<button class="more" type="button">Show more</button>
<?php endif; ?>
<a class="readall" href="/projects/">All projects →</a>
No hidden items, no button. Nothing is promised that cannot be delivered.
Scenario B: block theme (FSE), Query Loop + JS
In a block theme it works differently, and this is where people coming from classic templates get caught.
The Query Loop block renders the items. The "Show more" button usually lives in the template or pattern as static markup, a Custom HTML block or a button block. No PHP condition wraps it. The limit is applied in the browser by JavaScript: show the first N items, hide the rest, reveal more on click.
That means JS must own the button state, and the state must be derived from the data:
-
Default state is "no button". Put
hiddenon it in the markup. Without JS, or before JS runs, the page promises nothing. - JS counts the matching items (taking active filters into account) and shows the button only if there is something left to show.
- Every click and every filter change recalculates. When nothing is left, the button hides again.
<div class="pfoot">
<button class="more" id="more" type="button" hidden>Show more</button>
<a class="readall" href="/projects/">All projects →</a>
</div>
const batchSize = 5;
let visibleCount = batchSize;
function renderProjects() {
const matched = matchingItems(); // respects the active filter
items.forEach( ( li ) => { li.hidden = true; } );
matched.forEach( ( li, index ) => { li.hidden = index >= visibleCount; } );
const hasMore = matched.length > visibleCount;
more.hidden = ! hasMore;
}
more.addEventListener( 'click', () => {
visibleCount += batchSize;
renderProjects();
} );
If the button sits at the start of a flex row with justify-content: space-between, give the link margin-left: auto. Otherwise "All projects →" jumps to the left when the button disappears.
FSE gotcha. Once a template has been saved in the Site Editor, WordPress uses the copy stored in the database, not the file in your theme. Fix the pattern file, reload, and nothing changes. When a fix "doesn't apply" in a block theme, check whether the template was customized in the Site Editor. If it was, fix it there or reset it to the theme version. More on how block themes are put together: Full Site Editing and theme.json.
After
On the fixed version of my site:
- The home page shows the first 5 projects.
- "Show more" loads the next 5.
- When everything is visible, the button is gone.
- "All projects →" always stays. It is a real action and real navigation.
The Same Pattern Elsewhere
Once you see it, you see it everywhere:
- "Read more / Show less" on cards where the text is already shown in full, for example an "Our story" timeline where the toggle only changes its own label.
- Pagination that shows "Next" on the last page.
- A "Load more" that silently returns zero items.
- Filter chips that produce an empty list with no message.
The fix is always the same question: what happens to this control when there is no data, too little data, or an error?
Why It Happens More Now: the AI Factor
AI-generated code makes this worse, for a simple reason. Give a model a mockup and it reproduces all the UI states that are drawn: the list, the button, the link. What it does not have is the data context: is the list empty, short, missing, filtered?
The generated code looks plausible, it runs, the console is clean, so the review becomes superficial. Nobody asks what the button does when there is nothing to show, because the code "obviously" handles the button.
If you work with AI on WordPress code, add one question to every review of UI code: "What happens if there is no data?"
Not Only Design: Two Types of Interfaces
False affordances are not limited to buttons. Any interface makes promises.
External interfaces (UI):
- Buttons or toggles with no effect.
- Admin settings that are saved but never applied.
- An admin notice "Dismiss" that comes back on the next page load.
Internal interfaces (for developers):
- A hook or filter is documented, but
apply_filters()is never called with that name. - A shortcode or block attribute is accepted and ignored.
- A function parameter is accepted but never used.
- A REST endpoint returns
successbut does nothing.
The second group is worse in a way: the "user" is another developer, who builds on a feature that does not exist and only finds out in production.
The Cost
- Lost trust. A button that does nothing teaches users that your interface lies. They stop clicking.
- Support tickets. "I clicked Show more and nothing happened" is a real ticket someone has to answer.
- Reviews. On WordPress.org, a setting that does nothing is a one-star review.
- Open source. Other developers build on a documented hook that never fires.
Checklist
-
No data → no element. Render controls conditionally: on the server in PHP templates, in JS with a
hiddendefault in block themes. - Every control has a visible effect. If clicking it can do nothing, it should not be there.
-
Every documented option or hook actually works. Search for the
apply_filters()/do_action()call before you document it. - Test the edge states: empty, short, exactly at the limit, long, filtered, error.
- Ask the AI-review question: "What happens if there is no data?"
Testing approaches that make edge states a habit, not an afterthought: TDD and BDD on 4wp.dev.
Conclusion
A small detail with a big effect. One button on my own portfolio kept promising something, and the console never said a word.
An honest interface promises only what it delivers. Click through every state, including the boring ones, and let the data decide which controls exist.
More methodologies for WordPress teams on 4wp.dev.
來源:DEV Community · dev.to