UI/UX Design Module 15 – Portfolio and career
UX portfolio case studies
Choose two to four projects, lead each case study with the problem, your role and the result, share evidence with consent and publish an accessible page.
What you will learn
- Choose two to four projects that show your process, judgement and outcomes
- Write a case study in seven parts, from the problem to a reflection
- Show confidential, team and self-initiated work honestly, crediting others and any AI help
- Publish a case-study page that meets WCAG 2.2 AA and loads quickly on a phone
Before you start
What you’ll learn · Before you start
What you will learn
- Choose two to four projects that show your process, judgement and outcomes
- Write a case study in seven parts, from the problem to a reflection
- Show confidential, team and self-initiated work honestly, crediting others and any AI help
- Publish a case-study page that meets WCAG 2.2 AA and loads quickly on a phone
Before you start
On this page
A case study of 2,400 words and 12 images takes 11 minutes 26 seconds to read at 238 words a minute, the average speed of adults reading English non-fiction silently, and the person deciding whether to invite you may stop after the first screen. A UX portfolio: A small set of case studies that shows how a designer works: the problems, the decisions and their reasons, and what changed as a result. works when each case study is written for that reader: the problem, your role and the result first, then the decisions only you can explain.
You will choose the projects worth showing, write each case study: A written account of one design project for a reader who was not there: the problem, your role, the constraints, the evidence, the decisions, the outcome and a reflection. in seven parts, share research evidence you have permission to share, and publish a page that is itself a sample of accessible, fast design. Two programs check a draft the way a careful reader’s tools would.
Readers skim, so the first screen carries the problem, your role and the result
Whoever opens your portfolio, a recruiter, a design lead or a panel, reads the first screen of each case study to decide whether the rest is worth their time. On a phone that screen holds a short paragraph or two and perhaps one image. Put three things there: the problem and who had it, what you did, and what changed. The research, the sketches and the iterations support those three and come later.
The two openings describe the same MealBox project; MealBox, Ward Desk and every number about them are invented for this track. Count what you know after the first two sentences of each:
| Opening | What a reader knows after two sentences |
|---|---|
| My process followed the double diamond. I started with desk research on the meal-kit market, then created personas and a journey map. | The method, but not the problem, your part or any result |
| MealBox lost 31 % of new subscribers in their first week, most before the first box arrived. As the only designer in a team of five, I led six interviews and redesigned the order confirmation; in the subscription records, first-week cancellations fell to 22 % over the next two months. | The problem, who had it, your role, what changed and where the number comes from |
The strong version is barely longer, yet it answers the reader’s three questions in the order they ask them. After the first screen, show the decisions and trade-offs only you can explain, and leave out artefacts that changed nothing, however much work they took.
Opening with your process
Wrong: a first screen that lists the stages of your process, or shows a persona before the problem. Right: the problem, your role and the result first. The process is how you solved the problem, so it belongs in the middle, where a reader who is already interested goes looking for it.
Checkpoint
Check: The first screen of a case study
Two to four projects, chosen for the judgement they show
Each case study asks for a reader’s time, so two strong ones serve you better than six thin ones. Choose projects that together show process, judgement and outcomes, and that differ from each other: one weighted towards research, one towards interaction or visual design, one where a constraint forced a hard trade-off. Prefer recent work, and work you could present for 15 minutes and defend under questions, which the next lesson, Design interviews and design challenges, prepares you for.
Not every good project belongs. Answer the three questions about one project you are weighing; the table marks the kinds of project that still fit, with the reason:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Kind of project | Shows your process | Shows your decisions | Has a measured outcome | Can be shown in public |
|---|---|---|---|---|
| Shipped work in a team Typical evidence: research, releases, live measures | Good research, iterations and releases you can describe step by step | Mixed strong if you can say which decisions were yours | Good live measures before and after, when the team shares them | Mixed often confidential: ask permission or describe it in general terms |
| Shipped work on your own Typical evidence: a client or a small product | Good every step is yours to show | Good every decision was yours, with its reason | Mixed measures exist if the client or the product tracked them | Mixed ask the client before you publish their screens |
| A course project Typical evidence: a brief and a test with peers | Good a full process, often with every stage written up | Mixed the brief may have made the big decisions for you | Poor rarely more than a test with classmates | Good usually yours to show; label it as a course project |
| A volunteer project Typical evidence: real users, small scale | Good real constraints: little time, little money, real people | Good trade-offs you had to make yourself | Mixed small numbers, but real ones, from real users | Good usually shareable once the organisation agrees |
| A self-initiated redesign Typical evidence: public information only | Mixed research is limited to what is public or what you test yourself | Good every decision is yours | Poor no live measure; a test with five people at most | Mixed yes, if labelled as unaffiliated and built on public information |
| A hackathon prototype Typical evidence: a demo after two days | Poor two days leave little room for research or iteration | Mixed quick decisions, rarely tested | Poor a demo, not a measure | Good usually shareable, with the team credited |
How the answers decide
- “Did real people use the work, outside a classroom or a team demo?” No, only testers or classmates: adds a caveat to A course project, A self-initiated redesign and A hackathon prototype. Without real users the outcome is a test at best, so say what you tested and with whom, and pair it with one project that has real users.
- “Can you name at least two decisions that were yours, with the reason for each?” Not clearly: adds a caveat to Shipped work in a team, Shipped work on your own, A course project, A volunteer project, A self-initiated redesign and A hackathon prototype. A case study is a record of your decisions. Work out two that were yours, and why, apart from any team's; if you cannot, leave the project out.
- “Can you show the work in public, or get permission to?” No, it is confidential: adds a caveat to Shipped work in a team, Shipped work on your own, A course project, A volunteer project, A self-initiated redesign and A hackathon prototype. Ask for permission first. If it is refused, describe the problem and your process in general terms, with figures as ranges and no screens.
Course projects and a self-initiated project: Design work done on your own initiative, with no client or employer behind it, such as a redesign of a public product; shown in a portfolio with that label. are welcome as long as they are labelled. Pair them with at least one project that had real users, so that a reader also sees how you work when the people and the constraints are real.
Checkpoint
Check: Choosing two to four projects
Seven parts, each answering a question the reader has
A case study answers seven questions, roughly in this order. Each part can be short, but none can be missing, because a reader who meets a gap fills it with a guess:
| Part | The reader’s question |
|---|---|
| Problem | What was wrong, for whom, and why did it matter? |
| Role | What did you do, and with whom? |
| Constraints | What limited the work: time, platform, data or policy? |
| Evidence | How did you know, and from whom? |
| Decisions and trade-offs | What did you choose, over what, and what did it cost? |
| Outcome | What changed, and how was it measured? |
| Reflection | What would you do differently, and what comes next? |
The program checks a draft of the MealBox case study for those parts as h2 headings, for headings that skip a level, which the W3C headings tutorial says can confuse readers and should be avoided, and for its reading time by the rules of MySmartCoPilot’s Reading Time Calculator:
"""Check a case-study draft before it goes on a portfolio: the seven parts, the heading order and the reading time.
The draft is an invented MealBox project. Reading time follows MySmartCoPilot's Reading Time Calculator: 238 words a
minute, the average for silent reading of English non-fiction (Brysbaert, 2019), plus 12 seconds for the first image,
one second less for each next one and never under 3 seconds.
"""
from html.parser import HTMLParser
WPM = 238
LIMIT_MINUTES = 8
PARTS = { # part: words that mark its heading
"problem": ["problem"],
"role": ["role"],
"constraints": ["constraint", "limit"],
"evidence": ["research", "evidence"],
"decisions": ["decision", "trade-off"],
"outcome": ["outcome", "result"],
"reflection": ["reflection", "next time", "differently"],
}
DRAFT = """
<h1>Fewer first-week cancellations at MealBox</h1>
<p>MealBox lost 31 % of new subscribers in their first week. I was the only designer on a team of five.</p>
<h2>My role</h2>
<p>I planned and ran six interviews, mapped the first week with the support team and designed the new flow.</p>
<h2>The problem</h2>
<p>New subscribers did not know when the first box would arrive, so many cancelled before it came.</p>
<h4>What we measured</h4>
<p>Cancellations in the first seven days, from the subscription records, for every new subscriber.</p>
<h2>Research evidence</h2>
<p>Five of six people we interviewed checked their email for a delivery day that the old flow never sent.</p>
<img src="affinity-map.png" alt="Notes grouped into four themes; the largest is not knowing the delivery day">
<h2>Decisions and trade-offs</h2>
<p>We showed the delivery day on the confirmation screen and dropped a welcome video to keep the page light.</p>
<img src="flow-v2.png" alt="The new confirmation screen with the delivery day at the top">
<h2>Outcome</h2>
<p>First-week cancellations fell from 31 % to 22 % in the two months after launch, from the same records.</p>
"""
class Outline(HTMLParser):
def __init__(self):
super().__init__()
self.headings, self.words, self.images, self._level = [], 0, 0, None
def handle_starttag(self, tag, attrs):
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
self._level = int(tag[1])
self.headings.append([self._level, ""])
elif tag == "img":
self.images += 1
def handle_endtag(self, tag):
if tag.startswith("h") and tag[1:].isdigit():
self._level = None
def handle_data(self, data):
self.words += len(data.split())
if self._level is not None:
self.headings[-1][1] += data
def image_seconds(count):
return sum(max(3, 12 - i) for i in range(count))
def reading_seconds(words, images):
return words / WPM * 60 + image_seconds(images)
def mm_ss(seconds):
whole = round(seconds)
return f"{whole // 60} min {whole % 60} s"
page = Outline()
page.feed(DRAFT)
found = []
for level, text in page.headings:
for part, marks in PARTS.items():
if level == 2 and any(m in text.lower() for m in marks) and part not in found:
found.append(part)
print("Parts found, in page order:", ", ".join(found))
print("Missing:", ", ".join(p for p in PARTS if p not in found) or "none")
previous = 1
for level, text in page.headings[1:]:
if level > previous + 1:
print(f"Heading order: '{text}' is h{level} straight after an h{previous}")
previous = level
seconds = reading_seconds(page.words, page.images)
print(f"Words {page.words}, images {page.images}: {mm_ss(seconds)} to read (limit {LIMIT_MINUTES} min)")
print()
print("Words that fit in 8 minutes, by number of images:")
for images in (0, 6, 12):
room = LIMIT_MINUTES * 60 - image_seconds(images)
print(f" {images:>2} images: {image_seconds(images):>3} s for images, {room / 60 * WPM:,.0f} words")
long_read = reading_seconds(2400, 12)
print(f"A 2,400-word study with 12 images takes {mm_ss(long_read)}.") Output
Parts found, in page order: role, problem, evidence, decisions, outcome Missing: constraints, reflection Heading order: 'What we measured' is h4 straight after an h2 Words 146, images 2: 1 min 0 s to read (limit 8 min) Words that fit in 8 minutes, by number of images: 0 images: 0 s for images, 1,904 words 6 images: 57 s for images, 1,678 words 12 images: 81 s for images, 1,583 words A 2,400-word study with 12 images takes 11 min 26 s.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 case_study_check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The draft has five of the seven parts: the constraints and the reflection are missing, the role comes before the
problem, and “What we measured” drops from an h2 straight to an h4. At 146 words it reads in a minute, so length is not
its problem yet. The second half of the output is the budget for a full study: with 12 images, 8 minutes hold about
1,580 words, so the 2,400-word version needs about a third cut. Eight minutes is this lesson’s own limit; choose yours
and keep to it. Press Edit, add the two missing sections to DRAFT and run it again.
Checkpoint
Check: The seven parts and the reading time
Evidence a reader can trust, shared with consent
Evidence turns a claim into a case study: a participant’s words, a measure before and after, an earlier version next to the later one. Each kind is trusted for a different reason, and each has something to take out before it goes on a public page:
| Evidence | Trusted when | Take out before publishing |
|---|---|---|
| A participant’s quote | Short, in their words, used with consent | Name, face, voice and telling details |
| A measure before and after | It says what, over what period and from where | Figures nobody agreed to share |
| An earlier version | It sits beside the later one, with the reason | Real customer data in the screens |
| A research artefact | It shows a finding that changed a decision | Notes with personal data |
Consent is part of the evidence. The people in your research agreed to a purpose, and a public portfolio may not be one of them, so check what the consent form said about sharing, as the lesson on recruiting and consent sets out. The GOV.UK Service Manual advises using fully anonymised extracts, such as quotes, photos and clips, whenever possible, and notes that anonymised extracts can be shared widely, including in public. anonymisation: Removing or changing every detail in research data, direct or indirect, that could identify a participant, so that nobody can be identified from what is shared. goes further than pseudonymisation: Replacing names, addresses and other direct identifiers in research data with participant ids, so the data can be shared or analysed without re-identifying anyone. More in “Recruiting, consent and research ethics”: a quote under a participant id can still point to one person if it mentions a job title and a town, so read each extract for details like that.
Say how each number was measured
Next to every outcome, give the source and the period in a few words: “from the subscription records, over two months after launch”. A number with its source is evidence; a number without one is a claim.
Checkpoint
Check: Evidence shared with consent
Confidential, team and self-initiated work, labelled honestly
Much of a designer’s best work belongs to someone else, and every case has an honest way to show it. The table gives what the page should say in each:
| Situation | What the page says |
|---|---|
| Work under a non-disclosure agreement (NDA): A contract in which someone agrees not to share an organisation's confidential information, such as unreleased screens, data or plans, without its permission. | Ask permission first. If it is refused, describe the problem and your process in general terms, give figures as ranges or changes, and show no screens |
| Work done in a team | Each person’s part by name, and “I” for yours |
| Help from an AI tool | What the tool did, such as drafting a summary, and what you checked or changed |
| A self-initiated project | That it was self-initiated, and whether you tested it with anyone |
| A redesign of a real product | That you are not affiliated with the company, that you used public information only, and no logo used in a way that suggests the company endorsed it |
A label costs one sentence. A reader who learns later that a screen was a colleague’s, or that a redesign was never asked for, has a reason to doubt every other screen on the page. The developer’s version of this page, with code and READMEs, is covered in writing a project case study of the resume and portfolio track.
Checkpoint
Check: Confidential, team and self-initiated work
The page itself is a design sample
A reader judges your design from the page as much as from the work on it, so hold it to the standard you would apply to a client’s page: WCAG: The Web Content Accessibility Guidelines from W3C: testable success criteria at levels A, AA and AAA. WCAG 2.2 is the current version, and content that conforms to it also conforms to 2.1 and 2.0. More in “Inclusive design and disability” 2.2 at level AA, and fast on a mid-range phone. Three habits catch common problems on a case-study page. Text reaches a contrast ratio: A number from 1:1 to 21:1 comparing the relative luminance of two colours: the lighter plus 0.05, divided by the darker plus 0.05. WCAG 2.2 sets minimums for text and for interface parts. of at least 4.5:1 (success criterion 1.4.3). Each image’s alt text: The alt attribute of an image: a short text alternative chosen by the image's purpose. An empty alt tells assistive technology to skip the image. follows its purpose, by the W3C’s alt decision tree, and a decorative image: An image that adds no information to the page, such as a divider or a background texture; it gets an empty alt attribute so that assistive technology skips it. gets an empty alt. And headings never skip a level.
For speed, web.dev sets good values at the 75th percentile of page loads: Largest Contentful Paint (LCP): A web metric: the time from when a page starts loading until the largest image, text block or video in view is drawn. 2.5 seconds or less at the 75th percentile is good. within 2.5 seconds, Interaction to Next Paint (INP): A web metric: the slowest click, tap or key press of a visit, from the input to the next painted frame. 200 ms or less at the 75th percentile is good. More in “Memory, decisions and movement behind UI rules” of 200 milliseconds or less, and Cumulative Layout Shift (CLS): A web metric of visual stability: how much visible content moves unexpectedly while a page is open. A score of 0.1 or less at the 75th percentile is good. of 0.1 or less. The program runs the image and colour checks over the published MealBox page:
"""Check the images and text colours of a published case-study page, the way a careful reader's tools would.
Alt text follows the W3C WAI images tutorial's decision tree: decorative images get alt="", informative images a
short description, images of text the text itself, and complex images (a chart, a map) a short alt with the content
also given in the page text. Missing width and height let the page shift while it loads (web.dev), a lazy-loaded
first-screen image arrives late, and text needs a contrast ratio of 4.5:1 (WCAG 2.2, success criterion 1.4.3).
The 200 KB limit for one image is this lesson's own budget, not a standard.
"""
BUDGET_KB = 200
IMAGES = [
{"file": "hero-before-after.png", "purpose": "informative", "kb": 1840, "size": (2400, 1600),
"first_screen": True, "lazy": True,
"alt": "The order confirmation before and after: the new one shows the delivery day at the top"},
{"file": "affinity-map.jpg", "purpose": "complex", "kb": 420, "size": (1600, 1000),
"first_screen": False, "lazy": True, "alt": "affinity-map.jpg", "in_text": False},
{"file": "divider.svg", "purpose": "decorative", "kb": 2, "size": (800, 8),
"first_screen": False, "lazy": True, "alt": "decorative divider line"},
{"file": "flow-v2.png", "purpose": "informative", "kb": 160, "size": None,
"first_screen": False, "lazy": True, "alt": None},
{"file": "quote-card.png", "purpose": "text", "kb": 90, "size": (1200, 600),
"first_screen": False, "lazy": True, "alt": "Quote", "text": "I kept checking my email for the day"},
]
COLOURS = [ # (text, foreground, background)
("body text", "#374151", "#FFFFFF"),
("image captions", "#9CA3AF", "#FFFFFF"),
("links", "#2563EB", "#FFFFFF"),
]
def image_problems(image):
alt, purpose, out = image["alt"], image["purpose"], []
if alt is None:
out.append("no alt attribute, so some screen readers read out the file name")
elif alt == image["file"]:
out.append("the alt text is the file name")
elif purpose == "decorative" and alt != "":
out.append('decorative, so the alt should be empty: alt=""')
elif purpose == "text" and image["text"].lower() not in alt.lower():
out.append("an image of text: the alt should give the text itself")
if purpose == "complex" and not image.get("in_text"):
out.append("a complex image: give its content in the page text as well")
if image["size"] is None:
out.append("no width and height, so the page shifts while it loads")
if image["first_screen"] and image["lazy"]:
out.append("lazy-loaded on the first screen, so it arrives late")
if image["kb"] > BUDGET_KB:
out.append(f"{image['kb']} KB, over the {BUDGET_KB} KB budget")
return out
def luminance(hex_colour):
channels = [int(hex_colour[i:i + 2], 16) / 255 for i in (1, 3, 5)]
lin = [c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in channels]
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
def contrast(foreground, background):
light, dark = sorted([luminance(foreground), luminance(background)], reverse=True)
return int((light + 0.05) / (dark + 0.05) * 100) / 100 # cut to two decimals, never rounded up
total = 0
for image in IMAGES:
problems = image_problems(image)
total += len(problems)
print(f"{image['file']}: {'ok' if not problems else '; '.join(problems)}")
print()
for name, foreground, background in COLOURS:
ratio = contrast(foreground, background)
verdict = "meets 4.5:1" if ratio >= 4.5 else "below 4.5:1"
total += ratio < 4.5
print(f"{name}: {foreground} on {background} = {ratio:.2f}:1, {verdict}")
print(f"\n{total} problems to fix before the page goes out.") Output
hero-before-after.png: lazy-loaded on the first screen, so it arrives late; 1840 KB, over the 200 KB budget affinity-map.jpg: the alt text is the file name; a complex image: give its content in the page text as well; 420 KB, over the 200 KB budget divider.svg: decorative, so the alt should be empty: alt="" flow-v2.png: no alt attribute, so some screen readers read out the file name; no width and height, so the page shifts while it loads quote-card.png: an image of text: the alt should give the text itself body text: #374151 on #FFFFFF = 10.30:1, meets 4.5:1 image captions: #9CA3AF on #FFFFFF = 2.53:1, below 4.5:1 links: #2563EB on #FFFFFF = 5.16:1, meets 4.5:1 10 problems to fix before the page goes out.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 page_check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Ten problems, and none needs a redesign. The hero image matters most: it is the largest thing on the first screen, so it is likely to be the element that sets the page’s LCP, yet it weighs 1,840 KB and is lazy-loaded. web.dev’s analysis found that lazy loading the images of the first screen tends to make LCP slower, so load this one at once and compress it. The affinity map needs its content in the page text, since a short alt cannot carry a chart. The flow image has neither alt text nor dimensions, and web.dev advises always giving images a width and a height so the page does not shift while they load.
Image Compressor Compress the hero image and the screenshots before you publish, then compare the sizes. Page Speed Test (Lighthouse via PageSpeed Insights) Test the published case-study page and read its LCP, INP and CLS against the values above. Color Contrast Checker (WCAG 2.2) Check each text colour against its background, such as the 2.53:1 caption grey.Checkpoint
Check: The page as a design sample
Practise
The exercise hands you a first draft of a Ward Desk case-study page. Fix its parts, headings, images and colours; the sample tests apply the same rules as the two programs:
Exercise · Easy · Python
Fix a Ward Desk case-study page
solution.py holds a first draft of a case-study page for the track's Ward Desk project, as HTML in PAGE_HTML, and the page's text colours in COLOURS. Fix the draft so that:
- Seven parts: the page has an h2 for each of problem, role, constraints, research evidence, decisions and trade-offs, outcome and reflection, in that order. Write the reflection yourself, in one or two sentences.
- Headings: there is one h1, and no heading skips a level on the way down.
- Alt text: every image has an alt attribute chosen by its
data-purpose: empty for a decorative image, and for an informative image a short description of what it shows, never a file name and never starting with "image of". - Dimensions: every image has a width and a height.
- Contrast: every text colour in
COLOURSreaches 4.5:1 on its background. - Length: the page reads in 8 minutes or less at 238 words a minute, with the lesson's allowance for images.
Keep the project's facts and numbers as they are.
Starter code · solution.py
"""
A case-study page for the track's Ward Desk project, as a first draft. Fix PAGE_HTML and COLOURS so that the sample
tests pass: all seven parts as h2 headings in order, no skipped heading levels, alt text chosen by each image's
purpose (data-purpose says which), width and height on every image, text colours at 4.5:1 or more, and a reading
time of 8 minutes or less. Keep the project's facts; change the structure, the alt text and the colours.
"""
PAGE_HTML = """
<h1>Reporting a broken streetlight on Ward Desk</h1>
<p>Residents gave up on reporting broken streetlights: in a week of support logs, 14 of 40 reports had no usable
location. I was the interaction designer in a team of four, from research to the released form.</p>
<h2>The problem</h2>
<p>The old form asked for a ward number that few residents knew, so reports arrived without a place.</p>
<h2>My role</h2>
<p>I ran five sessions with residents, sketched three directions and designed the location step we shipped.</p>
<h4>Constraints</h4>
<p>The council's maintenance system accepts a street and a pole number only, and the form had to work offline.</p>
<h2>Research evidence</h2>
<p>Four of five residents looked for the pole number on the pole itself, so the form now asks for it first.</p>
<img src="pole-plate.jpg" alt="pole-plate.jpg" data-purpose="informative" width="1200" height="800">
<h2>Decisions and trade-offs</h2>
<p>We asked for the pole number before the street and dropped the map, which was slow on older phones.</p>
<img src="divider.svg" alt="divider" data-purpose="decorative" width="800" height="8">
<img src="location-step.png" data-purpose="informative">
<h2>Outcome</h2>
<p>In the month after release, reports without a usable location fell from 14 in 40 to 3 in 40, from the same logs.</p>
"""
# (what the colour is used for, text colour, background colour)
COLOURS = [
("body text", "#374151", "#FFFFFF"),
("captions", "#9CA3AF", "#FFFFFF"),
] The sample tests · test_solution.py
"""Sample tests for the write-a-case-study-page exercise. The rules match the lesson's two examples."""
import re
from html.parser import HTMLParser
from solution import COLOURS, PAGE_HTML
PARTS = {
"problem": ["problem"],
"role": ["role"],
"constraints": ["constraint", "limit"],
"evidence": ["research", "evidence"],
"decisions": ["decision", "trade-off"],
"outcome": ["outcome", "result"],
"reflection": ["reflection", "next time", "differently"],
}
WPM = 238
LIMIT_SECONDS = 8 * 60
class Page(HTMLParser):
def __init__(self):
super().__init__()
self.headings, self.images, self.words, self._level = [], [], 0, None
def handle_starttag(self, tag, attrs):
if re.fullmatch(r"h[1-6]", tag):
self._level = int(tag[1])
self.headings.append([self._level, ""])
elif tag == "img":
self.images.append(dict(attrs))
def handle_endtag(self, tag):
if re.fullmatch(r"h[1-6]", tag):
self._level = None
def handle_data(self, data):
self.words += len(data.split())
if self._level is not None:
self.headings[-1][1] += data
def parsed():
page = Page()
page.feed(PAGE_HTML)
return page
def luminance(hex_colour):
channels = [int(hex_colour[i:i + 2], 16) / 255 for i in (1, 3, 5)]
lin = [c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in channels]
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
def contrast(foreground, background):
light, dark = sorted([luminance(foreground), luminance(background)], reverse=True)
return int((light + 0.05) / (dark + 0.05) * 100) / 100
def test_seven_parts_as_h2_in_order():
order = []
for level, text in parsed().headings:
for part, marks in PARTS.items():
if level == 2 and part not in order and any(m in text.lower() for m in marks):
order.append(part)
missing = [p for p in PARTS if p not in order]
assert not missing, f"no h2 for: {missing}"
assert order == list(PARTS), f"the parts are in this order: {order}"
def test_one_h1_and_no_skipped_levels():
levels = [level for level, _ in parsed().headings]
assert levels.count(1) == 1, "use exactly one h1"
for before, after in zip(levels, levels[1:]):
assert after <= before + 1, f"an h{after} follows an h{before}: a level is skipped"
def test_alt_text_fits_each_purpose():
for image in parsed().images:
alt, purpose, src = image.get("alt"), image.get("data-purpose"), image.get("src")
assert alt is not None, f"{src} has no alt attribute"
if purpose == "decorative":
assert alt == "", f'{src} is decorative: use alt=""'
else:
assert alt.strip(), f"{src} carries meaning: describe it"
assert alt != src and not re.search(r"\.(png|jpe?g|svg|gif|webp)$", alt), f"{src}: the alt is a file name"
assert not re.match(r"(image|picture|photo) of\b", alt.lower()), f"{src}: start with what it shows"
def test_every_image_has_width_and_height():
for image in parsed().images:
assert image.get("width") and image.get("height"), f"{image.get('src')} needs width and height"
def test_text_colours_meet_4_5_to_1():
for use, foreground, background in COLOURS:
ratio = contrast(foreground, background)
assert ratio >= 4.5, f"{use}: {foreground} on {background} is {ratio:.2f}:1"
def test_reading_time_at_most_8_minutes():
page = parsed()
seconds = page.words / WPM * 60 + sum(max(3, 12 - i) for i in range(len(page.images)))
assert seconds <= LIMIT_SECONDS, f"{seconds / 60:.1f} minutes to read: cut it to 8" A hint
Run the sample tests first and fix the headings before anything else: the Constraints heading is an h4 straight after an h2, and the page has no reflection yet. Then work through the images one at a time, reading each one's data-purpose.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then take the self-check, which covers all six ideas and moves on to harder questions as you answer:
Practice that adapts to you
Practise this lesson
13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: how many case studies should a UX portfolio have, and how do you choose them?
Two to four, each strong enough to present for 15 minutes and to defend under questions. Choose them together, not one by one: between them they should show research, interaction or visual design, and at least one hard trade-off forced by a constraint. Prefer recent work and at least one project with real users. A course project or a self-initiated one is fine if it is labelled as such. Leave out a project if you cannot name two decisions that were yours, because a case study is a record of decisions and the reader is there to learn about you.
Warm-up: what belongs on the first screen of a case study?
The problem and who had it, your role, and the result with where its number comes from. Those three answer what a reader asks first: is this relevant, what did you do, did it work. Everything else, the research, the sketches, the iterations, explains how you got there and belongs further down, where an interested reader will look for it.
Two questions with model answers come from this lesson. Each asks how you would handle your own portfolio under pressure; settle on your opening sentence for each before you read the step that follows it:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
Your case study says a redesign "cut support calls by 40 %". In an interview a design lead asks how that was measured, and you realise the figure was a manager's estimate in a meeting. What do you say now, and what do you change on the page?
-
Fresher to senior
Your strongest case study runs to 4,800 words with 30 images. A design lead you hope to work with says they give each portfolio about five minutes. How do you cut it, what survives, and how do you check the result?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What should the first screen of a case study carry?
The problem, your role and the result, with where the number comes from. Readers decide from the first screen, so a list of process stages or a persona before the problem tells them the method but not why the work mattered; the process belongs in the middle.
-
How many projects belong in a UX portfolio, and how are they chosen?
Two to four that together show process, judgement and outcomes and differ from each other, such as one weighted towards research, one towards interaction or visual design and one where a constraint forced a hard trade-off. At least one should have had real users.
-
What are the seven parts of a case study?
Problem, role, constraints, evidence, decisions and trade-offs, outcome and reflection, each answering a question the reader has. A short part is better than a missing one.
-
When is an outcome in a case study evidence rather than a claim?
When it says what was measured, over what period and from where, such as from the subscription records over two months after launch. A number without its source is a claim.
-
What must be taken out of research evidence before a case study is published?
Names, faces, voices and telling details from participants' quotes, figures nobody agreed to share, real customer data in screens and notes with personal data. Quotes are used only with consent.
-
How does a case study stay honest about team work, AI help and confidential work?
Name each person's part and use I for yours; say what an AI tool did and what you checked or changed; for work under a non-disclosure agreement, ask permission first and, if it is refused, describe it in general terms with figures as ranges and no screens.
-
What must a self-initiated redesign of a real product say?
That it was self-initiated and whether you tested it with anyone, that you are not affiliated with the company and used public information only, and it uses no logo in a way that suggests the company endorsed it.
-
Which checks does a case-study page itself need to pass?
WCAG 2.2 AA: text at 4.5:1, alt text that follows each image's purpose, empty alt for decoration, and headings that never skip a level. For speed, at the 75th percentile: LCP within 2.5 s, INP of 200 ms or less and CLS of 0.1 or less, so the first-screen image is not lazy-loaded.
Key takeaways
- Readers decide from the first screen, so a case study opens with the problem, your role and the result.
- Two to four projects that together show process, judgement and outcomes beat a longer list, and at least one should have had real users.
- A case study has seven parts, problem, role, constraints, evidence, decisions, outcome and reflection, and a short part is better than a missing one.
- Evidence is trusted when it says where it came from, and research extracts go public only with consent and after anonymisation.
- Confidential work, team work, work done with AI help and self-initiated work stay honest with one clear sentence on the page.
- The page is itself a sample: text at 4.5:1, alt text by purpose, headings in order, and a page that is fast on a phone.
Terms in this lesson
- anonymisation
- Removing or changing every detail in research data, direct or indirect, that could identify a participant, so that nobody can be identified from what is shared.
- case study
- A written account of one design project for a reader who was not there: the problem, your role, the constraints, the evidence, the decisions, the outcome and a reflection.
- Cumulative Layout Shift (CLS)
- A web metric of visual stability: how much visible content moves unexpectedly while a page is open. A score of 0.1 or less at the 75th percentile is good.
- decorative image
- An image that adds no information to the page, such as a divider or a background texture; it gets an empty alt attribute so that assistive technology skips it.
- Largest Contentful Paint (LCP)
- A web metric: the time from when a page starts loading until the largest image, text block or video in view is drawn. 2.5 seconds or less at the 75th percentile is good.
- non-disclosure agreement (NDA)
- A contract in which someone agrees not to share an organisation's confidential information, such as unreleased screens, data or plans, without its permission.
- self-initiated project
- Design work done on your own initiative, with no client or employer behind it, such as a redesign of a public product; shown in a portfolio with that label.
- UX portfolio
- A small set of case studies that shows how a designer works: the problems, the decisions and their reasons, and what changed as a result.
References
- Web Content Accessibility Guidelines (WCAG) 2.2 (W3C)
- An alt Decision Tree (W3C Web Accessibility Initiative)
- Decorative Images (W3C Web Accessibility Initiative)
- Headings (W3C Web Accessibility Initiative)
- Understanding Success Criterion 1.4.3: Contrast (Minimum) (W3C)
- Web Vitals (Google (web.dev))
- Interaction to Next Paint (INP) (Google (web.dev))
- Optimize Cumulative Layout Shift (Google (web.dev))
- The performance effects of too much lazy loading (Google (web.dev))
- Managing user research data and participant privacy (Government Digital Service (GOV.UK))
- How many words do we read per minute? A review and meta-analysis of reading rate (Elsevier (Journal of Memory and Language))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress