diff --git a/lms/static/scripts/frontend_apps/components/StatusChip.tsx b/lms/static/scripts/frontend_apps/components/StatusChip.tsx
new file mode 100644
index 0000000000..8fca7370b1
--- /dev/null
+++ b/lms/static/scripts/frontend_apps/components/StatusChip.tsx
@@ -0,0 +1,33 @@
+import classnames from 'classnames';
+import type { ComponentChildren } from 'preact';
+
+export type StatusChipProps = {
+ /**
+ * Determines the aspect of the status chip, being "top" the one representing
+ * the most successful status, and "bottom" the one representing a total
+ * failure.
+ */
+ variant: 'top' | 'high' | 'medium' | 'low' | 'bottom';
+
+ children?: ComponentChildren;
+};
+
+/**
+ * A status chip (AKA label or tag) that visually represents the success level
+ * on a scale of 1 to 5
+ */
+export default function StatusChip({ variant, children }: StatusChipProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/lms/static/scripts/frontend_apps/components/dashboard/GradeStatusChip.tsx b/lms/static/scripts/frontend_apps/components/dashboard/GradeStatusChip.tsx
new file mode 100644
index 0000000000..62efea77de
--- /dev/null
+++ b/lms/static/scripts/frontend_apps/components/dashboard/GradeStatusChip.tsx
@@ -0,0 +1,42 @@
+import { useMemo } from 'preact/hooks';
+
+import type { StatusChipProps } from '../StatusChip';
+import StatusChip from '../StatusChip';
+
+export type GradeStatusChipProps = {
+ /**
+ * A grade, from 0 to 100, that will be used to render the corresponding
+ * StatusChip.
+ *
+ * 100 - top
+ * 80-99 - high
+ * 50-79 - medium
+ * 1-49 - low
+ * 0 - bottom
+ */
+ grade: number;
+};
+
+/**
+ * A StatusChip where the corresponding variant is calculated from a grade
+ * from 0 to 100
+ *
+ * See {@link StatusChip}
+ */
+export default function GradeStatusChip({ grade }: GradeStatusChipProps) {
+ const variant = useMemo((): StatusChipProps['variant'] => {
+ if (grade === 100) {
+ return 'top';
+ } else if (grade >= 80 && grade < 100) {
+ return 'high';
+ } else if (grade >= 50 && grade < 80) {
+ return 'medium';
+ } else if (grade >= 1 && grade < 50) {
+ return 'low';
+ }
+
+ return 'bottom';
+ }, [grade]);
+
+ return {grade}%;
+}
diff --git a/lms/static/scripts/ui-playground/components/GradeStatusChipPage.tsx b/lms/static/scripts/ui-playground/components/GradeStatusChipPage.tsx
new file mode 100644
index 0000000000..9199c13bbc
--- /dev/null
+++ b/lms/static/scripts/ui-playground/components/GradeStatusChipPage.tsx
@@ -0,0 +1,37 @@
+import Library from '@hypothesis/frontend-shared/lib/pattern-library/components/Library';
+
+import StatusChip from '../../frontend_apps/components/StatusChip';
+import GradeStatusChip from '../../frontend_apps/components/dashboard/GradeStatusChip';
+
+export default function GradeStatusChipPage() {
+ return (
+
+
+
+ It can render any kind of content. The color can be customized via{' '}
+ variant
prop.
+
+
+ Top
+ High
+ Medium
+ Low
+ Bottom
+
+
+
+
+ It renders a StatusChip
by automatically calculating the
+ right variant, based on a grade from 0 to 100.
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/lms/static/scripts/ui-playground/index.ts b/lms/static/scripts/ui-playground/index.ts
index a0299e2895..6a3b1ae3c5 100644
--- a/lms/static/scripts/ui-playground/index.ts
+++ b/lms/static/scripts/ui-playground/index.ts
@@ -2,8 +2,16 @@
import { startApp } from '@hypothesis/frontend-shared/lib/pattern-library';
import type { CustomPlaygroundRoute } from '@hypothesis/frontend-shared/lib/pattern-library/routes';
+import GradeStatusChipPage from './components/GradeStatusChipPage';
+
// LMS prototype pages should be defined here
-const extraRoutes: CustomPlaygroundRoute[] = [];
+const extraRoutes: CustomPlaygroundRoute[] = [
+ {
+ component: GradeStatusChipPage,
+ route: '/grade-status-chip',
+ title: 'Grade status chip',
+ },
+];
startApp({
baseURL: '/ui-playground',