Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "pnpm exec storybook dev -p ${PORT:-56789}"],
"port": 56789,
"autoPort": false
"autoPort": true
}
]
}
7 changes: 4 additions & 3 deletions __docs__/design-components/active-filters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ The choice to use active filters is determined by the need to provide immediate

## What generates an active filter

Not every control in the filter bar produces a chip. The test is whether the control's own display already shows the complete, specific value at a glance. If a selection is short and fully legible on the control itself (like "Last 30 days"), a chip would just repeat it. If the control has to truncate long text or summarize multiple selections as a count (like "School: Addison Middle..." or "Grades: 2 selected"), a chip is what actually tells the user the full, specific value — one chip per value when multiple are selected.
Not every control in the filter bar produces a chip. The deciding factor is who owns the control's starting value. If the product supplies a default and the control can never be cleared to empty, a value is always present regardless of what the user does — a chip couldn't distinguish an actual choice from the baseline, so none appears. If the user is the one introducing a value at all — the control starts empty or inactive until they act — a chip appears as soon as they do, since its presence signals a real, removable choice.

| Control type | Generates an active filter? | Why |
| --- | --- | --- |
| Standard dropdown or inline filter button | Yes | Long or multi-value selections get truncated or summarized as a count on the control itself (e.g. "Addison Middle...", "2 selected") — chips restore full legibility, one per value when multiple are selected. |
| Embedded filter dropdown (e.g. Date, defaulting to Last 30 days) | No | Preset values are designed to stay short and complete on the control itself (e.g. "Last 30 days," "Last 7 days"), so nothing needs to be hidden or truncated. |
| Standard dropdown or inline filter button | Yes | Starts empty or inactive — any value present means the user chose it. Long or multi-value selections also get truncated or summarized as a count on the control itself (e.g. "Addison Middle...", "2 selected"); chips restore full legibility, one per value when multiple are selected. |
| Embedded filter dropdown (e.g. Date, defaulting to Last 30 days) | No | The product supplies a default and the control can't be cleared to empty — a value is always present regardless of user action, so a chip couldn't signal an actual choice. |
| Date inline filter button | Yes | No date is pre-applied — the control starts empty until the user adds one. Its presence, even when short and fully legible on the control itself (e.g. "Nov 1 – Nov 30"), signals a real, removable choice the user made. |
| Search field | No | The typed term is fully visible in the field itself. |
| Toggle | No | Its on/off state is a complete, unambiguous indicator, always visible on the control. |
| Segmented control | No | The selected segment stays fully visible and highlighted on the control itself. |
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/active-filters/copy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/active-filters/states.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/checkbox-group/copy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/checkbox-group/placement-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/clear-filter/placement.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/clear-filter/states-hidden.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/date-filter/copy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/date-filter/date-filter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/date-filter/interaction.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/date-filter/placement-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/date-filter/placement-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/date-filter/variants-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/date-filter/variants-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/khanductor/placement-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/khanductor/placement-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/progress-stepper/copy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __docs__/design-components/assets/text-field/states-1.png
Binary file modified __docs__/design-components/assets/text-field/states-2.png
Binary file modified __docs__/design-components/assets/text-field/states-3.png
Binary file modified __docs__/design-components/assets/text-field/states-4.png
Binary file modified __docs__/design-components/assets/text-field/states-5.png
Binary file modified __docs__/design-components/assets/text-field/states-6.png
Binary file modified __docs__/design-components/assets/text-field/states-7.png
Binary file modified __docs__/design-components/assets/text-field/text-field.png
Binary file modified __docs__/design-components/assets/toggle/copy.png
Binary file modified __docs__/design-components/assets/toggle/interaction-1.png
Diff not rendered.
Binary file modified __docs__/design-components/assets/toggle/placement.png
4 changes: 2 additions & 2 deletions __docs__/design-components/checkbox-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ The choice between exposing a checkbox group directly or wrapping it in a dropdo

8 or fewer options — expose the checkbox group directly. More than 8 options — wrap in a dropdown to prevent the list from becoming too long to scan.

<Figure type="do" explanation="8 or fewer, exposed directly" src={placement1Img} alt="A filter set with 8 or fewer options exposed directly, not wrapped in a dropdown" />
<Figure type="do" caption="8 or fewer, exposed directly" src={placement1Img} alt="A filter set with 8 or fewer options exposed directly, not wrapped in a dropdown" />

### Top filter bar

Always wrap in a dropdown regardless of option count. Horizontal space is constrained and exposed groups would break the layout.

<Figure type="do" explanation="top filter bar, always dropdown" src={placement2Img} alt="A top filter bar where grade options are wrapped in a dropdown" />
<Figure type="do" caption="top filter bar, always dropdown" src={placement2Img} alt="A top filter bar where grade options are wrapped in a dropdown" />

## Interaction

Expand Down
6 changes: 1 addition & 5 deletions __docs__/design-components/clear-filter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {NeutralBadge} from "@khanacademy/wonder-blocks-badge";

# Clear filter

<NeutralBadge label="Bulk state reset" />
<NeutralBadge label="State reset" />

Ensures users can return to a neutral dataset instantly without manual deselection, regardless of how many components are currently in use.

Expand Down Expand Up @@ -53,7 +53,3 @@ Position the "Clear filters" button directly following the list of active filter
It should sit between the filter bar (controls) and the data (results), providing a clear bridge between the two.

<img src={placementImg} alt="Placement" />

---

[Figma source](https://www.figma.com/design/G9Yxa0SOlALS078wtq87cM/Design-system-guidelines---Thunderblocks?node-id=12444-38560&t=WAQP6xuZw16xrb5c-4)
3 changes: 3 additions & 0 deletions __docs__/design-components/date-filter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The choice to use a date filter is determined by the need to filter data based o
The product pre-applies a date. Data is always scoped to a time period — the user never sees an unfiltered state. They can change the selection, but cannot clear it entirely.
Use this when there are pages where showing all-time data would be overwhelming or meaningless — e.g. a teacher dashboard where "Today" is a sensible default. The product owns the initial state.
There is always a value in the trigger. If no user selection has been made, the product default shows (e.g. Today).
Because the product supplies this default and the control can never be cleared to empty, changing the selection does not generate an active filter chip (see Active filters).

<img src={variants1Img} alt="A basic example of a Date filter opened with the first `Today` option selected, followed by `This week` and `Last 30 days`, each with the actual accompanying date or date range as subtext. At the bottom is a `Custom Range` affordance after a horizontal separator." />

Expand All @@ -50,6 +51,7 @@ There is always a value in the trigger. If no user selection has been made, the
No date is pre-applied. Data defaults to all time. The user chooses to add a date filter — clicking the inactive inline filter button opens the same predefined options. Once applied, the button shows the active range and gains a clear (×) control.
Use this when there are pages where all-time data is a valid and expected default — e.g. a roster or gradebook where the user may or may not want to narrow by date. The user owns the initial state.
The × control clears back to all-time (unfiltered). This is the key distinction from the standard variant — this one can be cleared.
Because no date is pre-applied, any value shown here was chosen by the user. Applying one generates an active filter chip, even though the range is already visible on the button itself (see Active filters).

<img src={variants2Img} alt="Shows the transition of an empty `Date` filter being filled in. Once filled in, a calendar icon appears at the start of the input followed by the range and then a `clear` icon affordance" />

Expand All @@ -62,6 +64,7 @@ The × control clears back to all-time (unfiltered). This is the key distinction
| Can be cleared | No – always has a value | Yes – X returns to all-time |
| Trigger appearance | Full-width dropdown with current value | Inactive until user adds filter, once selected becomes active state of inline filter button |
| Options on open | Today / This week / Last 30 days / Custom range | Today / This week / Last 30 days / Custom range |
| Generates an active filter chip | No | Yes |

## Placement

Expand Down
4 changes: 3 additions & 1 deletion __docs__/design-components/khanductor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {NeutralBadge} from "@khanacademy/wonder-blocks-badge";

# Khanductor

<NeutralBadge label="AI assessment proctor" /> Khanductor is an AI persona specifically designed for the formal assessment product. While Khanmigo supports daily learning, Khanductor serves as a neutral proctor during standardized benchmark windows.
<NeutralBadge label="AI assessment proctor" />

Khanductor is an AI persona specifically designed for the formal assessment product. While Khanmigo supports daily learning, Khanductor serves as a neutral proctor during standardized benchmark windows.

## When to use

Expand Down
12 changes: 5 additions & 7 deletions __docs__/design-components/text-field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {NeutralBadge} from "@khanacademy/wonder-blocks-badge";

# Text field

<NeutralBadge label="Free-form input" /> Text fields allow users to enter and edit text into a UI. They typically appear in forms and dialogs. This is a primary mechanism for gathering user input and data across the platform.
<NeutralBadge label="Free-form input" />

Text fields allow users to enter and edit text into a UI. They typically appear in forms and dialogs. This is a primary mechanism for gathering user input and data across the platform.

## When to use

Expand Down Expand Up @@ -45,7 +47,7 @@ Label is positioned outside and above the input box. The input box is empty unti

### Placeholder text

Serves as a brief, transient hint inside an input field to help guide user input. It is best used to demonstrate specific formatting like MM/DD/YYYY, clarify ambiguous labels with style examples such as "Ms. Z," or indicate the scope of a search field.
Serves as a brief, transient hint inside an input field to help guide user input. It is best used to demonstrate specific formatting like MM/DD/YYYY, or clarify ambiguous labels with style examples such as "Ms. Z."
Because this text disappears when a user begins typing, it must never replace a persistent external label.

<img src={states2Img} alt="States 2" />
Expand All @@ -58,7 +60,7 @@ Context to replace asterisks and instead say Required to indicate necessary fiel

### Helper text

Used for any helpful description text that would otherwise be hidden in a placeholder. This information is more useful when persistent.Reminder that auto-complete overlays will cover things under the input when they display, so for things like names, phone #s, addresses, email addresses, etc. where autocomplete can be used, helper text should be displayed above the input.
Used for any helpful description text that would otherwise be hidden in a placeholder. This information is more useful when persistent, and sits below the label and above the input.

<img src={states4Img} alt="States 4" />

Expand All @@ -80,7 +82,3 @@ This state requires explicit helper text (e.g., "Only teachers can edit this") t
Primarily used when a prerequisite action has not been fulfilled. Once the dependency is met, it no longer displays as disabled, indicating it can now be used.

<img src={states7Img} alt="States 7" />

---

[Figma source](https://www.figma.com/design/G9Yxa0SOlALS078wtq87cM/Design-system-guidelines---Thunderblocks?node-id=12444-22778)
56 changes: 39 additions & 17 deletions __docs__/design-components/toggle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ import {Meta} from "@storybook/addon-docs/blocks";
import toggleImg from "./assets/toggle/toggle.png";
import placementImg from "./assets/toggle/placement.png";
import interaction1Img from "./assets/toggle/interaction-1.png";
import interaction2Img from "./assets/toggle/interaction-2.png";
import interaction2DoImg from "./assets/toggle/interaction-2-do.png";
import interaction2DontImg from "./assets/toggle/interaction-2-dont.png";
import copyImg from "./assets/toggle/copy.png";
import preferencesPlacementImg from "./assets/toggle/preferences-placement.png";
import preferencesInteractionImg from "./assets/toggle/preferences-interaction.png";
import preferencesCopyImg from "./assets/toggle/preferences-copy.png";
import {NeutralBadge} from "@khanacademy/wonder-blocks-badge";
import Figure from "../components/Figure";

<Meta title="Design Guidelines / Components / Inputs / Toggle" />

<img src={toggleImg} alt="Toggle" />

# Toggle

<NeutralBadge label="Binary/Boolean filtering" /> A simple on/off mechanism used for a quick, either-or choice to include/exclude items based on a single boolean attribute.
<NeutralBadge label="Binary/Boolean control" /> A simple on/off mechanism used for a quick, either-or choice — filtering results by a single boolean attribute, or switching a setting or preference on and off.

## When to use

Expand All @@ -21,47 +26,64 @@ The choice to use a toggle is determined by the need for an immediate, singular
| Use when | Example use |
| --- | --- |
| The choice is strictly binary | On/Off, Include/Exclude, True/False |
| The action takes effect immediately without confirmation | Toggling "Show only completed tasks" instantly filters the list |
| The state is always visible and needs to be persistent | A global constraint that remains active across the session until the user turns it off |
| The action takes effect immediately without confirmation | Toggling "Show only completed tasks" instantly filters the list; toggling "Receive email from Khan Academy" instantly saves the preference |
| The state is always visible and needs to be persistent | A global constraint that remains active across the session until the user turns it off; a notification preference that stays set until changed in account settings |

## When NOT to use

| Dont use when | Recommended alternative | Rationale |
| Don't use when | Recommended alternative | Rationale |
| --- | --- | --- |
| There are 3 or more possible states | Checkbox group | Toggles are only appropriate for binary decisions. More states need a different selection pattern. |
| Multiple options can be selected at the same time | Checkbox group | Toggles act on a single attribute. Additive filtering across multiple options requires checkboxes. |
| The action requires an Apply button to commit | Checkbox or form input + apply button | Toggles activate immediately. If the user needs to review before confirming, the pattern breaks. Exception: filter modal — see Interaction. |
| Multiple options can be selected at the same time | Checkbox group | Toggles act on a single attribute. Selecting multiple options at once — whether additive filtering or enabling several related settings — requires checkboxes. |
| The action requires an Apply button to commit | Checkbox or form input + apply button | Toggles activate immediately. If the user needs to review before confirming, the pattern breaks. Exception: filter modal — see Filtering. |

## Placement
## Filtering

### Placement

Place the toggle after primary filters (like Date or Course). This positions it as a high-level global constraint applied after the core dimensions have been selected.
Never place a toggle before dropdowns or inline within a list of results.

<img src={placementImg} alt="Placement" />

## Interaction

### No filter chips

Toggles do not generate filter chips in the filter bar. Because the toggle is always visible and explicit in the UI, a chip would be redundant.

<img src={interaction1Img} alt="Interaction 1" />
<Figure type="dont" caption="toggle state should not appear as a chip" src={interaction1Img} alt="A filter bar where an active toggle has incorrectly generated a filter chip" />

### Filter modal

When a toggle lives inside a filter modal, it does not apply immediately. The effect is deferred until the user applies the modal filters. This is the only context where a toggle does not produce an instant result.
Inside a filter modal, the label sits to the left of the toggle, not inline to the right as it does in the filter bar. An optional short description can sit below the label to add context when the toggle's purpose isn't self-evident from the label alone.
Only one toggle should be visible in the filter bar at a time. If additional boolean filters are needed, they belong inside the filter modal accessed via "More filters."

<img src={interaction2Img} alt="Interaction 2" />
<Figure type="do" caption='multiple toggles grouped inside "More filters" modal' src={interaction2DoImg} alt="Multiple toggles grouped under Display options inside the More filters modal" />

<Figure type="dont" caption="multiple toggles exposed directly in the filter bar" src={interaction2DontImg} alt="Multiple toggles exposed directly in the filter bar" />

## Copy
### Copy

The label must always be visible next to the toggle. It should clearly communicate what happens when the toggle is on — not what state it is in.
Keep labels concise and action-oriented. Avoid vague words like "Filter" or labels that describe the off state.
The label must always be visible next to the toggle. It should clearly communicate what happens when the toggle is on — not what state it is in. Keep labels concise and action-oriented, describing the effect (e.g., "Show only completed tasks"). Avoid vague words like "Filter" or labels that describe the off state.

<img src={copyImg} alt="Copy" />

---
## Preferences

### Placement

The toggle sits to the left of its label, with the label inline to the right — the same order as the filter bar, and the reverse of the filter modal, where the label sits to the left of the toggle.

<img src={preferencesPlacementImg} alt="Preferences placement" />

### Instant save

Preference toggles save instantly on change. There is no deferred or confirm step — unlike the filter modal exception in Filtering, every preference toggle takes effect immediately.

<img src={preferencesInteractionImg} alt="Preferences interaction" />

### Copy

The label names the setting itself as a noun phrase (e.g., "New assignment notifications," "Receive email from Khan Academy") — not an instruction to turn it on (avoid "Enable new assignment notifications" or "Turn on..."). It should still be clear what turning the toggle on enables.

[Figma source](https://www.figma.com/design/G9Yxa0SOlALS078wtq87cM/Design-system-guidelines---Thunderblocks?node-id=12444-33996)
<img src={preferencesCopyImg} alt="Preferences copy" />
Loading