Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

PasswordStrength

A password field with a live strength meter and a rule checklist underneath it, built on top of TextEntry. One control on both hosts, with the same properties and the same defaults.

  • NuGet downloads for Shiny.Maui.Controls
  • NuGet downloads for Shiny.Blazor.Controls
Frameworks
.NET MAUI
Blazor

MinimumLength is 15, breached values are refused, and every character-composition rule — uppercase, lowercase, number, symbol — is off.

That is deliberate. A “must contain a symbol” rule does not produce strong passwords; it produces Passw0rd!, which is short, memorable to nobody, and already sitting at the top of every wordlist. NIST SP 800-63B dropped composition rules for exactly that reason. Turn them on when an external policy forces your hand, not as a precaution.

MAUI

<shiny:PasswordStrength Placeholder="Passphrase"
Variant="Floating"
Password="{Binding Passphrase, Mode=TwoWay}"
IsAcceptable="{Binding CanSubmit}"
StrengthChanged="OnStrengthChanged" />
<Button Text="Create account"
Command="{Binding SubmitCommand}"
IsEnabled="{Binding CanSubmit}" />

Blazor

<PasswordStrength @ref="field"
@bind-Password="passphrase"
Placeholder="Passphrase"
StrengthChanged="OnStrengthChanged" />
<button disabled="@(field?.IsAcceptable != true)">Create account</button>
@code {
PasswordStrength? field;
string passphrase = "";
void OnStrengthChanged(PasswordStrengthChangedEventArgs e) { }
}

The two answer different questions and they genuinely disagree.

  • Score (0-100) and Level (Weak/Fair/Good/Strong) say how hard the password is to crack.
  • IsAcceptable says whether every rule in the policy is met.

A forty-character passphrase scores 100 and still fails a policy that demands a digit. A submit button bound to Score >= 80 will happily accept a password your own rules reject.

Property Type Default Description
Password string "" Current value (TwoWay / @bind-Password)
Placeholder string "Password" Placeholder / floating label
Variant TextEntryVariant Classic Passed through to the inner TextEntry
MinimumLength int 15 Shortest acceptable password
RequireUppercase bool false Require an A-Z
RequireLowercase bool false Require an a-z
RequireNumber bool false Require a digit
RequireSpecialCharacter bool false Require a character from SpecialCharacters
SpecialCharacters string printable ASCII symbols + space What counts as special
RequireNotCompromisedPassword bool true Refuse commonly breached values and their disguises
BlockedPasswords list null Extra values to refuse outright
UserInputs list null This user’s email / name — refused, and discounted when scoring
Evaluator IPasswordStrengthEvaluator? null Per-field scorer override
DebounceMilliseconds int 250 Pause before scoring; 0 scores every keystroke
Localizer PasswordStrengthLocalizer? null Replaces the wording
MeterStyle PasswordStrengthMeterStyle Segments Four blocks, or one bar filled to the score
MeterHeight double 6 Meter thickness
MeterCornerRadius double / string 3 / "3px" Meter corner radius
SegmentSpacing double 4 Gap between segments; ignored in Bar mode
TrackColor Color? / string? null Unfilled meter; null follows surface-container-highest
WeakColor FairColor GoodColor StrongColor Color? / string? null Null follows critical / caution / warning / success
RuleTextColor Color? / string? null Unsatisfied checklist row; null follows on-surface-variant
RuleFontSize double 13 Checklist font size
ShowMeter bool true Draw the meter
ShowStrengthLabel bool true Draw the Weak/Fair/Good/Strong caption
ShowRules bool true Draw the checklist
ShowWarning bool true Surface the evaluator’s warning as the field’s hint text
ShowVisibilityToggle bool true The Show/Hide button
ShowPasswordIcon HidePasswordIcon ImageSource? / string? null Toggle content; null uses the words “Show” / “Hide”
Score int 0 0-100, read-only
Level PasswordStrengthLevel None Read-only
IsAcceptable bool false Every rule met, read-only
Result PasswordStrengthResult? null Full verdict — rules, warning, suggestions

On MAUI Score, Level, IsAcceptable and Result default to BindingMode.OneWayToSource, so bind them to view-model properties. On Blazor they are plain read-only properties reached through @ref.

Member Description
StrengthChanged PasswordStrengthChangedEventArgsResult, Score, Level, IsAcceptable
Completed Return key pressed
StrengthChangedCommand MAUI only; invoked with the PasswordStrengthResult
EvaluateNowAsync() Score immediately, bypassing the debounce
IsPasswordRevealed / SetPasswordRevealed(bool) Drive the reveal from your own UI
Focus() / Unfocus() MAUI only

Blocked words and the user’s own details

Section titled “Blocked words and the user’s own details”
<shiny:PasswordStrength Password="{Binding Password}"
MinimumLength="12"
BlockedPasswords="{Binding HouseRules}"
UserInputs="{Binding UserDetails}" />

BlockedPasswords refuses exact matches, case-insensitively — the product name, the company name, whatever a policy endpoint hands you. UserInputs is different: an email address is split on its punctuation first, so ada.lovelace@example.com catches a password containing lovelace, and the matched run is discounted when scoring rather than merely flagged. Fragments shorter than four characters are ignored — they match half the dictionary and would fail every password.

Both lists are read when the parameters change. If you mutate a list in place, call EvaluateNowAsync().

public interface IPasswordStrengthEvaluator
{
ValueTask<PasswordStrengthResult> EvaluateAsync(
PasswordStrengthRequest request,
CancellationToken cancellationToken = default
);
}

Register one app-wide:

// MAUI
builder.UseShinyControls(x => x.SetCustomPasswordStrengthEvaluator<HibpEvaluator>());
// Blazor
services.AddShinyControls(x => x.SetCustomPasswordStrengthEvaluator<HibpEvaluator>());

Resolution order is Evaluator → DI → the built-in heuristic.

The interface is asynchronous and cancellable precisely so a network-backed implementation is workable. Keystrokes are debounced by DebounceMilliseconds and the previous evaluation is cancelled before the next starts, so an evaluator is not asked to answer for every character typed — but it must pass the token through, because the answer to a password the user has already changed is worthless. If a custom evaluator throws, the built-in one answers instead, so losing the network downgrades the meter rather than freezing it on a stale verdict.

DefaultPasswordStrengthEvaluator estimates entropy as collapsed length × log2(character pool), then maps it onto 0-100 where 80 bits scores 100. “Collapsed” means the characters an attacker gets for free come out first:

  • runs of the same character — aaaaaaaaaaaaaaaa is worth about two characters, not sixteen
  • ascending and descending sequences — 123456, abcdef
  • repeated blocks — everything past the first repetition of abcabcabc

Any word from the built-in list of commonly breached passwords is then charged a flat 11 bits instead of its length, and a password that is one of those values — seen through case, leet substitution and a bolted-on year, so P@ssw0rd2024 is caught along with password — is scored as if it were the bare word.

It needs no network and no data files. The list is deliberately short rather than a bundled ten-million-line wordlist: a package every app links has no business carrying tens of megabytes, and the long tail is better served by a real breach corpus behind the interface above. CommonPasswords.IsCompromised(string) and CommonPasswords.FindLongestMatch(string) are public, so a custom evaluator can reuse the list.

control.Localizer = text => text.Key switch
{
PasswordStrengthTextKey.LevelWeak => "Faible",
PasswordStrengthTextKey.LevelStrong => "Fort",
// Argument carries the number, so the sentence can be rebuilt rather than patched
PasswordStrengthTextKey.RuleMinimumLength => $"Au moins {text.Argument} caractères",
_ => null // anything not translated keeps the default
};

Every string the control paints has a key. Returning null keeps the default, so a localizer only has to know about the strings it actually translates.

On Blazor the meter is a role="progressbar" carrying aria-valuenow and an aria-valuetext of the current level, the strength caption is aria-live="polite", and each checklist row appends a visually hidden “, met” / “, not met” — a coloured glyph on its own does not say which.

The Show/Hide toggle defaults to the words “Show” and “Hide” rather than an eye glyph. An eye emoji renders at a different size on every platform, some Android system fonts do not have one, and neither reads correctly to a screen reader. Set ShowPasswordIcon / HidePasswordIcon to use your own icon font.

claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View shiny-controls Skill