The answer is that you are always getting the last row in the result set.
Consider your current code. You query, then in a loop fetch every row from the result set and assign the values from the row to various temporary variables of the same name and purpose.
First off, not to fix your problem, but --- there is no reason to do this. You get an associative array with the values nicely indexed by column name. Don't make a bunch of temporary variables when you don't need to. Just use $row['field'] when you need to display the value.
Yes, you absolutely need to change your query to take the id passed to the script. According to what you provided that should be $_GET['id'].
I don't know why that hasn't worked for you yet, but that's the correct way to do this, although, you should be using a prepared statement and bind variable rather than trying to interpolate the variable. Do it the right way.
Consider the interpolation example you provided:
SELECT * FROM users WHERE id = '$id' "
This is incorrect if id is an integer, which we have to assume it is, since this is a numeric id. So you should not put quotes around it, because it is not a string. With that said, the mysqli_ binding probably allow this but it's sloppy and incorrect SQL.
In summary, there may be an issue with the id, so make sure you debug that you are getting the value you expect from the $_GET array. (This also assumes you reach this page via an anchor href). We don't have the code to know for sure what you are doing.
if (empty($_GET['id']) {
// this page shouldn't be entered, because no valid id was passed
// maybe redirect?
exit("invalid");
}
$id = (int)$_GET['id'];
$sql = "SELECT * FROM users WHERE id=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();