diff --git a/src/components/BookJumpList.css b/src/components/BookJumpList.css
new file mode 100644
index 0000000..0e54246
--- /dev/null
+++ b/src/components/BookJumpList.css
@@ -0,0 +1,26 @@
+.book-jump-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ width: 100%;
+ max-width: 24rem;
+ color: #243b53;
+ font-weight: 600;
+}
+
+.book-jump-list select {
+ width: 100%;
+ min-height: 2.75rem;
+ padding: 0.625rem 0.75rem;
+ border: 1px solid #d5dde5;
+ border-radius: 0.5rem;
+ background-color: #ffffff;
+ color: #1f2933;
+ font: inherit;
+ font-weight: 400;
+}
+
+.book-jump-list select:focus-visible {
+ outline: 0.1875rem solid #2c3e50;
+ outline-offset: 0.125rem;
+}
diff --git a/src/components/BookJumpList.test.tsx b/src/components/BookJumpList.test.tsx
new file mode 100644
index 0000000..0128c75
--- /dev/null
+++ b/src/components/BookJumpList.test.tsx
@@ -0,0 +1,57 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { BookJumpList } from "./BookJumpList";
+
+// Inline fixture data keeps the tests independent of the API and network.
+const titles = ["The Hobbit", "Dune", "Pride and Prejudice"];
+
+describe("BookJumpList", () => {
+ it('renders a native select associated with the label "Jump to book"', () => {
+ // Render the component with controlled test props.
+ render();
+
+ // Find the select by its accessible role and label.
+ const select = screen.getByRole("combobox", { name: "Jump to book" });
+
+ // Verify that the accessible control is a native HTML select element.
+ expect(select.tagName).toBe("SELECT");
+ });
+
+ it("renders one option per title", () => {
+ render();
+
+ // Verify that every fixture title is rendered as an option in the same order.
+ expect(screen.getAllByRole("option")).toHaveLength(titles.length);
+ expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual(titles);
+ });
+
+ it("selects the option at currentIndex", () => {
+ render();
+
+ // currentIndex 1 should select the second title, "Dune".
+ expect(screen.getByRole("option", { name: "Dune" })).toHaveProperty("selected", true);
+ });
+
+ it("calls onSelect with the selected zero-based index", () => {
+ // Create a mock function so we can inspect how onSelect is called.
+ const onSelect = vi.fn();
+ render();
+
+ // Simulate the user selecting the option whose value is index 2.
+ fireEvent.change(screen.getByRole("combobox", { name: "Jump to book" }), {
+ target: { value: "2" },
+ });
+
+ // Verify that the component reports the selected index to its parent.
+ expect(onSelect).toHaveBeenCalledWith(2);
+ });
+
+ it("matches the rendered snapshot", () => {
+ const { container } = render(
+ ,
+ );
+
+ // Detect unexpected changes to the component's rendered HTML structure.
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/components/BookJumpList.tsx b/src/components/BookJumpList.tsx
new file mode 100644
index 0000000..b923a2c
--- /dev/null
+++ b/src/components/BookJumpList.tsx
@@ -0,0 +1,28 @@
+import "./BookJumpList.css";
+
+interface BookJumpListProps {
+ titles: string[]; // Titles available for direct navigation.
+ currentIndex: number; // Zero-based index of the currently selected book.
+ onSelect: (index: number) => void; // Notifies the parent when the user selects another book.
+}
+
+export function BookJumpList({ titles, currentIndex, onSelect }: BookJumpListProps) {
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/components/__snapshots__/BookJumpList.test.tsx.snap b/src/components/__snapshots__/BookJumpList.test.tsx.snap
new file mode 100644
index 0000000..e6b93a5
--- /dev/null
+++ b/src/components/__snapshots__/BookJumpList.test.tsx.snap
@@ -0,0 +1,31 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`BookJumpList > matches the rendered snapshot 1`] = `
+
+
+
+`;