Skip to content

[material-ui] Render the theme.focusVisible ring on inputs - #49152

Open
siriwatknp wants to merge 5 commits into
mui:masterfrom
siriwatknp:worktree-filled-input-focus-visible
Open

siriwatknp wants to merge 5 commits into
mui:masterfrom
siriwatknp:worktree-filled-input-focus-visible

Conversation

@siriwatknp

@siriwatknp siriwatknp commented Sep 17, 2026

Copy link
Copy Markdown
Member

Follow-up to #48743.

Summary

theme.focusVisible covered every ButtonBase-derived component, but nothing on the input side. That left real gaps: a bare InputBase has no focus indicator at all, and a filled or standard text field with disableUnderline loses the underline that was its only one — in a theme whose whole point is that nothing is left without an indicator.

This PR gives InputBase the ring, following the same shape ButtonBase already uses: the base draws it, and each wrapper opts out when it already has an indicator of its own.

const theme = createTheme({ focusVisible: true });

<InputBase placeholder="Search" />                                            // ring
<TextField variant="filled" slotProps={{ input: { disableUnderline: true } }} /> // ring
<TextField variant="standard" slotProps={{ input: { disableUnderline: true } }} /> // ring

<TextField variant="filled" />   // underline, unchanged
<TextField variant="standard" /> // underline, unchanged
<TextField variant="outlined" /> // notched outline, unchanged
Component On focus
InputBase Nothing of its own — the ring.
Input, FilledInput with disableUnderline No underline to mark focus — the ring.
Input, FilledInput The underline marks focus — opts out.
OutlinedInput The notched outline recolors — opts out.

Focus ring across InputBase and every text field variant, showing the ring on InputBase and the disableUnderline fields and the unchanged indicators elsewhere

The bottom three rows are the control: the components that already had an indicator are untouched, so there is no double ring anywhere. And the whole variant is a no-op when focusVisible is not set on the theme, so this is zero visual diff for anyone not opted in.

The full focus visible demo gains rows for InputBase and both disableUnderline variants, and the Row helper now takes a secondary caption so a variant qualifier reads as supporting text rather than swelling the bold label.

For Reviewers

The shape mirrors ButtonBase. InputBaseRoot draws the ring behind a internalDisabledThemeFocusVisible variant, the private prop is destructured with the other private props and carried on ownerState — the same three touch points ButtonBase uses, and the same name, so the two halves of the feature read alike. There are exactly three consumers of InputBaseRoot (Input, FilledInput, OutlinedInput) and each now states its decision at the InputBase call site, next to the other props it forwards.

An earlier revision put the ring directly on FilledInput and Input instead. Inverting it is better: it covers the bare InputBase, it puts the decision where a reader looks for it, and any future wrapper gets the ring by default and has to say so to decline.

Since the prop is destructured it never reaches other, so it cannot leak to the DOM — there is a test for that.

Why it keys on Mui-focused, not a keyboard-only state. Two reasons, one principled and one practical.

The principled one: on the wrappers this ring is a substitute for the underline it replaces, and the underline has always been Mui-focused-driven. Keying the replacement on a different state would mean disableUnderline silently changes when the field reports focus, not just how it looks.

The practical one: the feature is class-driven throughout — that is what lets the visual-regression fixtures render already-focused, and what lets users force the state in their own tests. InputBase tracks Mui-focused and nothing finer. A keyboard-only ring would mean introducing a Mui-focusVisible state on InputBase — a new public class, a new API surface, and a second focus-tracking path through a component many others compose. That is a larger change than this gap warrants, and it can be layered on later without breaking anything here.

The trade-off worth naming: a filled Select with disableUnderline shows the ring on mouse click, where native :focus-visible would not. For a text field it makes no practical difference — browsers match :focus-visible on text inputs regardless of how focus arrived.

On the standard variant specifically. It has no surface of its own, so the ring is the only box the field gets and a focused field ends up reading close to the outlined variant. That is a real cost, and it is called out in the guide. It still beats no indicator at all, and an author who dislikes the shape can pass a custom focusVisible — which is the whole point of the theme key.

No outsetFocusRing here. Worth flagging since every other consumer of the ring spreads it. Outset is already the fallback (var(--_focusVisible-offset, 1)), so that helper is purely a reset for inherited inset vars — it earns its place on ButtonBase, Checkbox, Slider and friends because those genuinely nest inside a clip-prone root (an IconButton inside a MenuItem, a Checkbox inside a ListItemButton). The inset vars are only ever set by Tab, MenuItem, ListItemButton, CardActionArea, BottomNavigationAction and the Autocomplete option — all interactive leaf roots, so a text input inside one would be nested-interactive markup. Verified by removing it: the offset still computes to 2px.

Tests. Computed-style tests on InputBase (ring on focus; suppressed when a wrapper opts out; the prop does not reach the DOM), FilledInput and Input (ring with disableUnderline, none without), and OutlinedInput (never a ring). The negative cases are the ones that matter — they pin the opt-out, which is the only way this change could regress a default path.


🤖 Generated with Claude Code

https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP

disableUnderline drops the only focus indicator, so the theme ring takes
its place. Keyed on Mui-focused, same state the underline it replaces uses.

Docs: new full-demo row for the filled + disableUnderline TextField, and
Row gains a secondary caption under the label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
@code-infra-dashboard

code-infra-dashboard Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+286B(+0.05%) 🔺+72B(+0.05%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

Same gap as FilledInput: the underline is the only focus indicator, so
disableUnderline leaves the standard input with none.

Drop outsetFocusRing from both. Outset is already the var fallback; the
reset only matters for components that nest inside a clip-prone root, and
a text input inside a Tab/MenuItem/ListItemButton is nested-interactive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
@siriwatknp siriwatknp changed the title [material-ui] Render the focus ring on FilledInput with disableUnderline [material-ui] Render the focus ring on inputs with disableUnderline Sep 17, 2026
Move the ring to InputBase, mirroring ButtonBase: the base draws it, and
internalDisabledThemeFocusVisible lets each wrapper decide.

A bare InputBase has no indicator, so it gets the ring. Input and
FilledInput opt out while the underline is drawn. OutlinedInput always
opts out; its notched outline recolors on focus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
@siriwatknp siriwatknp changed the title [material-ui] Render the focus ring on inputs with disableUnderline [material-ui] Render the theme.focusVisible ring on inputs Sep 17, 2026
The dashed box and radius were demo chrome that made the ring look
rounder than a bare InputBase is. Keep only the placeholder, so the row
shows the real default: nothing at rest, square ring on focus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
Comment thread packages/mui-material/src/OutlinedInput/OutlinedInput.js Outdated
inputProps: inputPropsProp = {},
inputRef: inputRefProp,
/* eslint-disable react/prop-types */
// private prop to let a wrapper (like OutlinedInput) draw its own focus indicator instead

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// private prop to let a wrapper (like OutlinedInput) draw its own focus indicator instead
// private prop to let a wrapper draw its own focus indicator instead

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACTuQZLtuDTErvbWTCuMaP
@zannager zannager added customization: theme Higher level theming customizability. scope: input Changes related to the input. labels Sep 18, 2026
<InputBase placeholder="Test" />
</Row>
<Row label="TextField" secondary="(filled + disableUnderline)">
<TextField

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

should we leave a bit of space between the floating label and the outline?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another choice is to wrap the whole container in the outline, including the label.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe the label overlapping the outline, like the Autocomplete example. in any case, I'm advocating for consistency here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

customization: theme Higher level theming customizability. scope: input Changes related to the input.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants